并发的两种途径:多个进程,每个进程只有一个线程(multiple single-threaded processes);每个进程有多个线程(multiple threads in a single process)。多个单线程/进程的启动和通信开销要比单一进程多线程间的启动和通信大。若不考虑共享内存可能带来的问题,多线程是主流语言(包括C++)更青睐的并发途径。此外,C++标准并未对进程间通信提供任何原生支持,如果程序使用多进程方式实现,这将会依赖与平台相关的API。(The low overhead associated with launching and communicating between multiple threads within a process compared to launching and communicating between multiple single-threaded processes means that this is the favored approach to concurrency in mainstream languages including C++, despite the potential problems arising from the shared memory.In addition, the C++ Standard doesn’t provide any intrinsic support for communication between processes, so applications that use multiple processes will have to rely on platform-specific APIs to do so.)
线程在std::thread对象创建(为线程指定任务)时启动。最简单的情况下,任务也会很简单,通常是无参无返回的函数。
std::thread可以用可调用类型构造,所以可以传递一个带有函数调用运算符的类的对象给std::thread的构造函数。
class background_task
{
public:
void operator()() const
{
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f);
代码中提供的函数对象会复制到新线程的存储空间当中,函数对象的执行和调用都在线程的内存空间中进行。
C++‘s most vexing parse 最令人头痛的语法解析
当把函数对象传入到线程构造函数中时,如果传递临时变量,而不是一个命名的变量,C++编译器会将其解析为函数声明,而不是类型对象的定义。
相当于声明了一个名为my_thread的函数,这个函数带有一个参数(函数指针指向没有参数并返回background_task对象的函数),返回一个std::thread对象的函数,而非启动一个线程。
std::thread my_thread(background_task());
使用多组括号或新同一的初始化语法来避免
std::thread my_thread((background_task()));
std::thread my_thread{background_task()};
可以传递一个lambda表达式给std::thread的构造函数。
std::thread my_thread([]{
do_something();
do_something_else();
});
必须在std::thread对象销毁之前做出等待或分离线程决定,否则程序将会终止(std::thread的析构函数会调用std::terminate())
等待线程完成(join)
调用join(),会清理线程相关的存储部分,这样std::thread对象将不再与已经完成的线程有任何关联。对于一个线程只能使用一个join(),不可对该线程再次调用join()。当对其使用joinable()会返回false。
如果打算等待对应线程,需要细心挑选调用join()的位置。当在被调用线程运行之后调用线程产生异常,并在join()调用前抛出,就意味这次调用会被跳过。
避免应用被抛出的异常所终止
struct func;
void f()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread t(my_func);
try
{
do_something_in_current_thread();
}
catch(...)
{
t.join();
throw;
}
t.join();
}
确保线程在函数之前结束-使用RAII资源获取即初始化方式
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();
}
当函数f执行结束时,局部对象就要被逆序销毁。因此,thread_guard对象g是第一个被销毁的,确保了线程被加入到原始线程中。即使do_something_in_current_thread抛出一个异常,销毁依旧会发生。
后台运行线程(detach)
使用detach()会让线程在后台运行,这意味着主线程不能与之产生直接交互。(不会等待线程结束)如果线程分离,那就不能有std::thread对象能引用它(打破了线程与std::thread对象的联系,即使std::thread对象被销毁,std::terminate()也不会调用),所以分离线程不能被加入。不过C++运行库保证,当线程退出时,相关资源能够正确回收,后台线程的归属和控制C++运行库都会处理。
std::thread t(do_background_work);
t.detach();
assert(!t.joinable());
不能对没有执行线程的std::thread对象使用detach(),当std::thread对象使用t.joinable()返回的是true,才可以使用t.detach()。
向std::thread构造函数中的可调用对象,或函数传递一个参数很简单。默认情况下参数要拷贝到线程独立内存中,即使参数是引用的形式,新线程可以进行访问。
void f(int i,std::string const& s);
std::thread t(f,3,”hello”);
字符串字面值将在新线程上下文中转换为std::string。
void f(int i,std::string const& s);
void oops(int some_param)
{
char buffer[1024];
sprintf(buffer, "%i",some_param);
std::thread t(f,3,buffer);
t.detach();
}
这个例子中,有很大可能性在buffer被转换为std::string之前函数oops已经退出,这导致了未定义行为。解决方法是在将buffer传递给std::thread构造函数之前转换为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,std::string(buffer));
t.detach();
}
如果希望传递一个引用,但整个对象被复制了,新线程无法更新一个引用传递的数据结构。
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,data);
display_status();
t.join();
process_widget_data(data);
}
std::thread构造函数无视函数期待的参数类型,盲目地拷贝已提供的变量。当线程调用update_data_for_widget函数时,传递给函数的参数是data变量内部拷贝的引用,而非数据本身的引用。当线程结束时,内部拷贝数据将会在数据更新阶段被销毁,且process_widget_data接收到的是没有修改的data变量。
可以使用std::ref将参数转换成引用形式,update_data_for_widget会接收到一个data变量的引用,而非一个data变量拷贝的引用。
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);
移动:原始对象中的数据转移给另一对象,而转移的这些数据就不再原始对象中保持了。std::unique_ptr就是这样一种类型,这种类型为动态分配的对象提供内存自动管理机制。同一时间,只允许一个std::unique_ptr指向一个给定对象,并且当这个实现销毁时,指向的对象也将被删除。移动构造函数和移动赋值操作符允许一个对象在多个std::unique_ptr实现中传递。使用移动转移原对象后,就会留下一个空指针。移动操作可以将对象转换成可接受的类型,例如:函数参数或函数返回的类型。当原对象是一个临时变量时,自动进行移动操作,但当原对象是一个命名变量,那么移动的时候就需要使用std::move()进行显示移动。
void process_big_object(std::unique_ptr);
std::unique_ptr 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函数。
std::thread每个实例负责管理一个执行线程。执行线程的所有权可以在多个std::thread实例中相互转移,这依赖于std::thread实例的可移动且不可复制性。
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);
新线程开始与t1相关联,当显式使用std::move()chu’a’g创建t2后,t1的所有权就转移给t2。这时,t1和执行线程已经没有关联。临时std::thread对象相关的线程启动,将所有权转移给t1(临时对象的移动操作将会隐式调用,不需显式调用std::move())。t3使用默认构造方式创建,与任何执行线程都没有关联。调用std::move()将与t2关联线程的所有权转移到t3中。最后一个移动操作,将some_function线程的所有权转移给t1。但是,t1已经有一个关联的线程(执行some_other_function的线程),所以这里系统直接调用std::terminate()终止程序继续运行。
线程所有权可以转移到函数内外
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;
}
void f(std::thread t);
void g()
{
void some_function();
f(std::thread(some_function));
std::thread t(some_function);
f(std::move(t));
}
当某个对象转移了线程所有权后,它就不能对线程进行加入或分离。scoped_thread构造函数就对这种情况进行了处理。
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();
}
如果容器是移动敏感的(比如,标准中的std::vector<>),那么这些移动操作同样适用于std::thread对象的容器。
void do_work(unsigned id);
void f()
{
std::vector 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));
}
将std::thread放入std::vector是向线程自动化管理迈出的第一步:并非为这些线程创建独立的变量,并且将它们直径加入,可以把它们当做一个组。
std::thread::hardware_concurrency()返回能同时并发运行在一个程序中的线程的数量。
template
struct accumulate_block
{
void operator()(Iterator first,Iterator last,T& result)
{
result=std::accumulate(first,last,result);
}
};
template
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 results(num_threads);
std::vector 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(),block_start,block_end,std::ref(results[i]));
block_start=block_end;
}
accumulate_block()(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);
}
线程标识类型是std::thread::id,可以通过两种方式进行检索。第一种,可以通过调用std::thread对象的成员函数get_id()来直接获取。如果std::thread对象没有与任何执行线程相关联,get_id()将返回std::thread::type默认构造值(这个值表示无线程)。第二种,当前线程中调用std::this_thread::get_id()(这个函数定义在头文件中)也可获得线程标识符。
线程库不会限制你去检查线程标识是否一样,std::thread::id类型对象提供相当丰富的对比操作。比如,提供为不同的值进行排序。这意味着允许程序员将其当做为容器的键值,做排序,或做其他方式的比较。标准库也提供std::hashstd::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();
}
虽然C++线程库为多线程和并发处理提供了较全面的工具,但在某些平台上提供额外的工具。为了方便地访问那些工具的同时,又使用标准C++线程库,在C++线程库中提供一个 native_handle() 成员函数,允许通过使用平台相关API直接操作底层实现。就其本质而言,任何使用 native_handle() 执行的操作都是完全依赖于平台的。