线程(thread)

线程(译注:大约是C++11中最激动人心的特性了)是一种对程序中的执行或者计算的表述。跟许多现代计算一样,C++11中的线程之间能够共享地址空间。从这点上来看,它不同于进程:进程一般不会直接跟其它进程共享数据。在过去,C++针对不同的硬件和操作系统有着不同的线程实现版本。如今,C++将线程加入到了标准件库中:一个标准线程ABI。

许多大部头书籍以及成千上万的论文都曾涉及到并发、并行以及线程。在这一条FAQ里几乎不涉及这些内容。事实上,要做到清楚地思考并发非常难。如果你想编写并发程序,请至少看一本书。不要依赖于一本手册、一个标准或者一条FAQ。

在用一个函数或者函数对象(包括lambda)构造std::thread时,一个线程便启动了。


#include <thread>

void f();

struct F {
    void operator()();
};

int main()
{
    std::thread t1{f};    // f() 在一个单独的线程中执行
    std::thread t2{F()};    // F()() 在一个单独的线程中执行
}

然而,无论f()和F()执行任何功能,都不能给出有用的结果。这是因为程序可能会在t1执行f()之前或之后以及t2执行F()之前或之后终结。我们所期望的是能够等到两个任务都完成,这可以通过下述方法来实现:

int main()
{
    std::thread t1{f};    // f() 在一个单独的线程中执行
    std::thread t2{F()};    // F()()在一个单独的线程中执行

    t1.join();    // 等待t1
    t2.join();    // 等待t2
}

上面例子中的join()保证了在t1和t2完成后程序才会终结。这里”join”的意思是等待线程返回后再终结。

通常我们需要传递一些参数给要执行的任务。例如:

void f(vector<double>&);

struct F {
vector<double>& v;
F(vector<double>& vv) :v{vv} { }
void operator()();
};

int main()
{
    // f(some_vec) 在一个单独的线程中执行
    std::thread t1{std::bind(f,some_vec)};   

    // F(some_vec)() 在一个单独的线程中执行
    std::thread t2{F(some_vec)};        

    t1.join();
    t2.join();
}

上例中的标准库函数bind会将一个函数对象作为它的参数。

通常我们需要在执行完一个任务后得到返回的结果。对于那些简单的对返回值没有概念的,我建议使用std::future。另一种方法是,我们可以给任务传递一个参数,从而这个任务可以把结果存在这个参数中。例如:

void f(vector<double>&, double* res);    // 将结果存在res中

struct F {
    vector<double>& v;
    double* res;
    F(vector<double>& vv, double* p) :v{vv}, res{p} { }
    void operator()();    //将结果存在res中
};

int main()
{
    double res1;
    double res2;

    // f(some_vec,&res1) 在一个单独的线程中执行
    std::thread t1{std::bind(f,some_vec,&res1)};    
    // F(some_vec,&res2)() 在一个单独的线程中执行
    std::thread t2{F(some_vec,&res2)};        

    t1.join();
    t2.join();

    std::cout << res1 << " " << res2 << ‘\n’;
}

但是关于错误呢?如果一个任务抛出了异常应该怎么办?如果一个任务抛出一个异常并且它没有捕获到这个异常,这个任务将会调用std::terminate()。调用这个函数一般意味着程序的结束。我们常常会为避免这个问题做诸多尝试。std::future可以将异常传送给父线程(这正是我喜欢future的原因之一)。否则,返回错误代码。

除非一个线程的任务已经完成了,当一个线程超出所在的域的时候,程序会结束。很明显,我们应该避免这一点。

没有办法来请求(也就是说尽量文雅地请求它尽可能早的退出)一个线程结束或者是强制(也就是说杀死这个线程)它结束。下面是可供我们选择的操作:

  • 设计我们自己的协作的中断机制(通过使用共享数据来实现。父线程设置这个数据,子线程检查这个数据(子线程将会在该数据被设置后很快退出))。
  • 使用thread::native_handle()来访问线程在操作系统中的符号
  • 杀死进程(std::quick_exit())
  • 杀死程序(std::terminate())

这些是委员会能够统一的所有的规则。特别地,来自POSIX的代表强烈地反对任何形式的“线程取消”。然而许多C++的资源模型都依赖于析构器。对于每种系统和每种可能的应有并没有完美的解决方案。

线程中的一个基本问题是数据竞争。也就是当在统一地址空间的两个线程独立访问一个对象时将会导致没有定义的结果。如果一个(或者两个)对对象执行写操作,而另一个(或者两个)对该对象执行读操作,两个线程将在谁先完成操作方面进行竞争。这样得到的结果不仅仅是没定义的,而且常常无法预测最后的结果。为解决这个问题,C++0x提供了一些规则和保证从而能够让程序员避免数据竞争。

  • C++标准库函数不能直接或间接地访问正在被其它线程访问的对象。一种例外是该函数通过参数(包括this)来直接或间接访问这个对象。
  • C++标准库函数不能直接或间接修改正在被其它线程访问的对象。一种例外是该函数通过非const参数(包括this)来直接或间接访问这个对象。
  • C++标准函数库的实现需要避免在同时修改统一序列中的不同成员时的数据竞争。

除非已使用别的方式做了声明,多个线程同时访问一个流对象、流缓冲区对象,或者C库中的流可能会导致数据竞争。因此除非你能够控制,绝不要让两个线程来共享一个输出流。

你可以

同时可参考:

  • Standard: 30 Thread support library [thread]
  • 17.6.4.7 Data race avoidance [res.on.data.races]
  • ???
  • H. Hinnant, L. Crowl, B. Dawes, A. Williams, J. Garland, et al.:

    Multi-threading Library for Standard C++ (Revision 1)

    N2497==08-0007

  • H.-J. Boehm, L. Crowl:

    C++ object lifetime interactions with the threads API

    N2880==09-0070.

  • L. Crowl, P. Plauger, N. Stoughton:

    Thread Unsafe Standard Functions

    N2864==09-0054.

  • WG14:

    Thread Cancellation N2455=070325.

(翻译:Yibo Zhu)