一、线程管理
启动线程
std::thread my_thread(background_task());
挂起线程
my_thread.detach();
struct func
{
int& i;
func(int& i_) : i(i_) {
}
void operator() ()
{
for (unsigned j=0 ; j<1000000 ; ++j)
{
do_something(i); // 1. 潜在访问隐患:悬空引用
}
}
};
void oops()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.detach(); // 2. 不等待线程结束
} // 3. 新线程可能还在运行
不等待线程结束,新线程中的函数可能还在运行,如果线程还在运行,它就会去调用do_something(i)函数①,这时就会访问已经销毁的变量
等待线程结束
my_thread.join()
使用RAII等待线程完成
class thread_guard
{
std::thread& t;
public:
explicit thread_guard(std::thread& t_):
t(t_)
{
}
~thread_guard()
{
if(t.joinable()) // 1
{
t.join(); // 2
}
}
thread_guard(thread_guard const&)=delete; // 3
thread_guard& operator=(thread_guard const&)=delete;
};
struct func; // 定义在清单2.1中
void f()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread t(my_func);
thread_guard g(t);
do_something_in_current_thread();
} // 4
二、向线程传递参数
守护线程
守护线程(daemon threads),UNIX中守护线程是指,且没有任何用户接口,并在后台运行的线程。这种线程的特点就是长时间运行;线程的生命周期可能会从某一个应用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化
使用分离线程去处理其他文档
void edit_document(std::string const& filename)
{
open_document_and_display_gui(filename);
while(!done_editing())
{
user_command cmd=get_user_input();
if(cmd.type==open_new_document)
{
std::string const new_name=get_filename_from_user();
std::thread t(edit_document,new_name); // 1
t.detach(); // 2
}
else
{
process_user_input(cmd);
}
}
}
如果用户选择打开一个新文档,为了让迅速打开文档,需要启动一个新线程去打开新文档①,并分离线程②。与当前线程做出的操作一样,新线程只不过是打开另一个文件而已。所以,edit_document函数可以复用,通过传参的形式打开新的文件。
注意
std::thread 无法进行类型转换,需要进行显示声明
void f(int i,std::string const& s);
void not_oops(int some_param)
{
char buffer[1024];
sprintf(buffer,"%i",some_param);
std::thread t(f,3,std::string(buffer)); // 使用std::string,避免悬垂指针
t.detach();
}
这种情况也会发生在引用的使用上
void update_data_for_widget(widget_id w,widget_data& data); // 1
void oops_again(widget_id w)
{
widget_data data;
std::thread t(update_data_for_widget,w,data); // 2
display_status();
t.join();
process_widget_data(data); // 3
}
解决方法是:
std::thread t(update_data_for_widget,w,std::ref(data));
其他用法:
class X
{
public:
void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work,&my_x); // 1
这段代码中,新线程将my_x.do_lengthy_work()作为线程函数;my_x的地址①作为指针对象提供给函数
可以使用std::move将一个动态对象转移到另一个线程中去
void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object,std::move(p));
在std::thread的构造函数中指定std::move§,big_object对象的所有权就被首先转移到新创建线程的的内部存储中,之后传递给process_big_object函数
三、转移线程所有权
1.t1已有关联线程,再使用std::move将会是赋值操作,会使程序崩溃
void some_function();
void some_other_function();
std::thread t1(some_function); // 1
std::thread t2=std::move(t1); // 2
t1=std::thread(some_other_function); // 3
std::thread t3; // 4
t3=std::move(t2); // 5
t1=std::move(t3); // 6 赋值操作将使程序崩溃
2.std::thread支持移动,就意味着线程的所有权可以在函数外进行转移,就如下面程序一样。
std::thread f()
{
void some_function();
return std::thread(some_function);
}
std::thread g()
{
void some_other_function(int);
std::thread t(some_other_function,42);
return t;
}
3.以下代码对thread_guard类进行了升级,支持移动线程操作
scoped_thread的用法
class scoped_thread
{
std::thread t;
public:
explicit scoped_thread(std::thread t_): // 1
t(std::move(t_))
{
if(!t.joinable()) // 2
throw std::logic_error(“No thread”);
}
~scoped_thread()
{
t.join(); // 3
}
scoped_thread(scoped_thread const&)=delete;
scoped_thread& operator=(scoped_thread const&)=delete;
};
struct func; // 定义在清单2.1中
void f()
{
int some_local_state;
scoped_thread t(std::thread(func(some_local_state))); // 4
do_something_in_current_thread();
} // 5
4.量产线程,等待结束
void do_work(unsigned id);
void f()
{
std::vector<std::thread> threads;
for(unsigned i=0; i < 20; ++i)
{
threads.push_back(std::thread(do_work,i)); // 产生线程
}
std::for_each(threads.begin(),threads.end(),
std::mem_fn(&std::thread::join)); // 对每个线程调用join()
}
我们经常需要线程去分割一个算法的总工作量,所以在算法结束的之前,所有的线程必须结束。上面代码说明线程所做的工作都是独立的,并且结果仅会受到共享数据的影响。
四、运行时决定线程数量
std::thread::hardware_concurrency()在新版C++标准库中是一个很有用的函数。这个函数将返回能同时并发在一个程序中的线程数量。
原生并行版的std::accumulate
template<typename Iterator,typename T>
struct accumulate_block
{
void operator()(Iterator first,Iterator last,T& result)
{
result=std::accumulate(first,last,result);
}
};
template<typename Iterator,typename T>
T parallel_accumulate(Iterator first,Iterator last,T init)
{
unsigned long const length=std::distance(first,last);
if(!length) // 1 如果输入的范围为空,就会得到init的值。
return init;
unsigned long const min_per_thread=25;
unsigned long const max_threads=
(length+min_per_thread-1)/min_per_thread; // 2
unsigned long const hardware_threads=
std::thread::hardware_concurrency();
unsigned long const num_threads= // 3 计算量的最大值和硬件支持线程数中,较小的值为启动线程的数量
std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
unsigned long const block_size=length/num_threads; // 4
std::vector<T> results(num_threads);
std::vector<std::thread> threads(num_threads-1); // 5
Iterator block_start=first;
for(unsigned long i=0; i < (num_threads-1); ++i)
{
Iterator block_end=block_start;
std::advance(block_end,block_size); // 6 advance() 函数用于将迭代器前进(或者后退)指定长度的距离,
threads[i]=std::thread( // 7
accumulate_block<Iterator,T>(),
block_start,block_end,std::ref(results[i]));
block_start=block_end; // 8
}
accumulate_block<Iterator,T>()(
block_start,last,results[num_threads-1]); // 9
std::for_each(threads.begin(),threads.end(),
std::mem_fn(&std::thread::join)); // 10
/* 函数模板std :: mem_fn生成指向成员的指针的包装对象,该对象可以存储,复制和调用指向成员的指针。 调用std :: mem_fn时,可以使用对象的引用和指针(包括智能指针)。
例子:*/
return std::accumulate(results.begin(),results.end(),init); // 11
}
五、识别线程
第一种,可以通过调用std::thread对象的成员函数get_id()来直接获取
第二种,当前线程中调用std::this_thread::get_id()(这个函数定义在头文件中)也可以获得线程标识
标准库也提供std::hash< std::thread::id >容器,所以std::thread::id也可以作为无序容器的键值。
主线程与子线程分开工作
std::thread::id master_thread;
void some_core_part_of_algorithm()
{
if(std::this_thread::get_id()==master_thread)
{
do_master_thread_work();
}
do_common_work();
}