《c++ concurrency in action》读书笔记2--线程管理
一、线程的启动
1. 每个c++程序至少有一个线程,是由C++ runtime启动的
2. 在c++11中,通过一个std::thread 对象启动线程。可以向std::thread传递一个函数,或者实现了调用操作符的类实例, 或者一个lambda表达式。
// 方法一 void do_some_work(); std::thread my_thread(do_some_work); // 方法二 class background_task { public: void operator()() const { do_something(); do_something_else(); } }; background_task f; std::thread my_thread(f); // 方法三 std::thread my_thread([]( do_something(); do_something_else(); }); // 方法四 class X { public: void do_lengthy_work(); }; X my_x; std::thread t(&X::do_lengthy_work,&my_x);
传递给std::thread的函数对象,会被拷贝到线程对象的上下文中,所以需确保拷贝后的对象和源对象一致。
注意,当使用临时对象初始化std::thread对象时:
std::thread my_thread(background_task()); // 声明了一个函数,而不是初始化了一个线程对象。 // 正确的方法: std::thread my_thread(background_task()); std::thread my_thread(background_task{});
3. 线程启动后,需要显示的决定是等待其执行完成(join) 或者让它自己运行(detach()), 如让线程自己运行,需确保线程访问的数据在结束之前是一直有效的。不然会出现未定义的行为。调用join()函数后,std::thread对象会清理所有存储的变量,所以对一个线程来说,join()函数只能调用一次,可以通过joinable()函数来检查是否可以调用join.
4. 为了避免程序终止,需要调用join/detach, 对于detach来说,可以在std::thread对象初始化后就调用,而对于join来说,则需要注意需要确保所有退出通路都调用join(), RAII(Resource Acquisition Is Initialization) 原则,则是一个不错的方法。
class thread_guard { std::thread& t; public: explicit thread_guard(std::thread& t_): t(t_) {} ~thread_guard() { if (t.joinable()) { t.join(); } } thread_guard(thread_guard const&)=delete; thread_guard& operator=(thread_guard const&)=delete; }; struct func; 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(); }
上面的scoped_thread类的构造函数使用的是引用,而使用引用可能存在一个问题,即std::thread的对象可以在多个地方被析构,而一个std::thread对象只能被move不能被copy,所以这里可以传一个非引用
class scoped_thread { std::thread t; public: explicit scoped_thread(std::thread t_): t(std::move(t_)) { if(!t.joinable()) throw std::logic_error(“No thread”); } ~scoped_thread() { t.join(); } scoped_thread(scoped_thread const&)=delete; scoped_thread& operator=(scoped_thread const&)=delete; }; struct func; void f() { int some_local_state; scoped_thread t(std::thread(func(some_local_state))); do_something_in_current_thread(); }
5. 调用detach()后,线程会在背景运行。一个线程对象,一旦是detach状态,就不能通过std::thread对象来管理它,detach后线程的控制权将会转移到c++运行库,c++运行库会确保线程退出时相应的资源被正确的释放。而对于没有运行的std::thread对象,是不能调用detach()和join()的。
6. 一个detach使用的例子
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); t.detach(); } else { process_user_input(cmd); } } }
二、向线程函数传递参数
1. 向线程函数传递参数是非常简单的,即将额外的参数传递到std::thread的构造函数中。需要注意的是,默认情况下,参数将被拷贝到线程内部的存储中,
void f(int i,std::string const& s); std::thread t(f,3,"hello");
在上面的例子中,char const* 类型会在线程的上下文中完成向std::string对象的转化。
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,buffer); 这种方法可能存在 not_oops函数先退出,然后 buffer才被转化为std::string的情况,这种情况的行为是未定义的。应该显示转为string std::thread t(f,3,std::string(buffer));
t.detach(); }
当希望将参数做为引用传递时,使用std::ref
void update_data_for_widget(widget_id w,widget_data& data); void oops_again(widget_id w) { widget_data data; std::thread t(update_data_for_widget,w,std::ref(data)); display_status(); t.join(); process_widget_data(data); }
2. 对于只能move的对象, 对于临时对象,会自动使用move, 对于非临时对象,则需要显示的调有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));
三、转移线程对象的所有全
1. std::thread对象只支持move操作,不支持copy操作。
void some_function(); void some_other_function(); std::thread t1(some_function); std::thread t2=std::move(t1); t1=std::thread(some_other_function); std::thread t3; t3=std::move(t2); t1=std::move(t3); // return from a function 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; } // pass to a function void f(std::thread t); void g() { void some_function(); f(std::thread(some_function)); std::thread t(some_function); f(std::move(t)); }
2. 等待多个线程完成
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::mem_fn -> 生成指向成员指针的包装对象 std::for_each(threads.begin(),threads.end(), std::mem_fn(&std::thread::join)); }
四 运行时选择线程数量
1. std::thread::hardware_concurrency() 返回一个执行程序真正可以并发的数量。(比如,在一个多核系统中,返回CPU的核数)但这只是一个提示,函数的返回值有可能为0。
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) return init; unsigned long const min_per_thread=25; unsigned long const max_threads= (length+min_per_thread-1)/min_per_thread; unsigned long const hardware_threads= std::thread::hardware_concurrency(); unsigned long const num_threads= std::min(hardware_threads!=0?hardware_threads:2,max_threads);
unsigned long const block_size=length/num_threads; std::vector<T> results(num_threads); std::vector<std::thread> threads(num_threads-1); 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); threads[i]=std::thread( accumulate_block<Iterator,T>(), block_start,block_end,std::ref(results[i])); block_start=block_end; } accumulate_block<Iterator,T>()( block_start,last,results[num_threads-1]); std::for_each(threads.begin(),threads.end(), std::mem_fn(&std::thread::join)); return std::accumulate(results.begin(),results.end(),init); }
五 获取线程标示
1. 可以通过 std::this_thread::get_id() 获取线程标示(为std::thread::id对象,如果两个std::thread::id对象相等,则表示同一个线程或者 “not any thread”.
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(); }