02.26 c++11多线程

多线程挺简单

c++11提供了方便的线程管理类std::thread,位于#include <thread>头文件中,下面是个简单的示例:

<code>void func(){  std::cout << "hello multi-thread! " << std::endl;}int main (){  for(int i = 0 ; i < 4; i++)  {       std::thread t(func);       t.detach();  }    return 0;}/<code>

上面的例子中,创建了4个线程用于输出“hello multi-thread”。多线程初体验 - 多线程的创建就是这么简单。

多线程的组成

在多线程编程中,每个应用程序至少有一个进程,每个进程至少有一个主线程。除了主线程之外,可以在一个进程中创建多个线程,每个线程都有入口函数,其中主线程的入口函数就是main函数。当入口函数执行结束时,线程随之退出。在c++11中,使用std::thread类可以创建并启动一个线程,该thread对象负责管理启动的线程(执行/挂起等)。下面是使用std::thread创建线程的简单示例:

<code>void func(int tid) {   std::cout << "cur thread \\  id is [%d] !" <<  tid << std::endl; }std::thread t(func, tid);/<code>

上面的示例中,创建了一个thread对象,就会启动一个线程(线程对象创建即启动,不许额外的操作)。与第一个示例不同的一点是,这里的入口函数需要传入一个参数,即thread构造函数的第二个参数。

入口参数的类型

std::thread类的构造函数是使用可变参数模板实现的,也就是说,可以传递任意个参数,第一个参数是线程的入口函数,而后面的若干个参数是该函数的参数。其中,入口参数的类型为可调用对象(Callable Objects),一般包含以下几种类型:

  • 函数指针,即传入函数名(c类型)
  • 重载了operator()运算符的类对象,即函数对象
  • lambda表达式(匿名函数)
  • std::function,其实上述3种类型都可以用std::function表示,不算是单独的一类

函数指针的示例不再赘述,关于lambda表达式和重载运算符以及std::function作为入口函数

,下面是简单的示例:

<code>// lambda表达式作为现成的入口函数 - 打印数字for(int i = 0; i < 4; i++){    std::thread t([i]{            std::cout << "cur number is: "                   << i << std::endl;            });    t.detach();}/<code>

上面的例子中,启动4个线程,并使用lambda表达式作为入口函数,实现数字打印的功能。

<code>// 重载运算符的实例作为入口函数class Test{    public:        void operator()(int i)       {             std::cout << "cur \\         number is:" << i << std::endl;       }}int main(){    for (int i = 0; i < 4; i++)    {        Test tmp;         std::thread t(tmp, i);         t.detach();    }}/<code>

函数对象 传入std::thread的构造函数时,要注意一个C++的语法解析错误(C++'s most vexing parse)。std::thread构造函数接受的是一个临时变量,否则就会导致语法解析错误,这是因为解释器比较“笨”,将Test()解释为了函数声明,该函数返回一个Test对象。需要说明的是,如果重载运算符有参数,则不会出现编译问题。知道原因,解决起来也就容易了,代码如下:

<code>std::thread t(Test()); // 编译出错,可以这样做:std::thread t{Test()}; 或者 std::thread t( (Test()) );/<code>

函数对象:定义了调用操作符的类对象,当用该对象调用此操作符时,其表现形式如同普通函数调用一般,因此取名叫函数对象。

<code>// std::function作为入口参数void add(int i, int j) { std::cout << i+j << std::endl; }std::function<void> func1 = add;std::function<void> func2 = [](int i, int j){ std::cout << i+j << std::endl; }std::thread t1(func1, num1, num2);std::thread t2(func2, num1, num2);/<void>/<void>/<code>

线程的join或者detach

一个线程启动之后,一定要在线程对象被销毁前确定以何种方式等待线程执行结束。等待的方式有两中join或detach。

  • join:线程启动之后,调用join阻塞主线程,等子线程执行结束后,继续执行主线程的指令;
  • detach:线程启动之后,调用detach不会影响主线程的执行,启动的子线程在后台运行;
<code>// 使用join阻塞主线程的例子int main(){auto func = [](int num){    std::cout</<code>
<code>// 使用detach等待子线程的示例int main(){auto func = [](int num){    std::cout</<code>

设置线程等待方式的注意事项

关于线程的detach等待方式,有个疑惑:会不会出现主线程执行结束,子线程仍在执行的情况?如果主线程退出,线程对象会随之销毁,子线程还能继续吗?

<code>// 使用detach等待子线程int main(){auto func = [](int num){    // Sleep(1000);    for (int i = 0; i < num; i++)    {      std::cout</<code>

上述实验代码说明,使用detach时,的确会存在主线程结束后,子线程尚未结束的情况,且会导致程序崩溃!如果真是这样,我的疑惑更大了,稍微复杂点的程序,子线程的执行时间稍微长点就可能导致上面的情况。此时貌似只能使用join,这样的话detach是否有点鸡肋,请大神不吝赐教,不胜感激!

同时也要注意到,join也不完美:使用join的难点在于,在哪里join,不合适的join可能会导致程序串行,达不到并行的效果。如果join和detach能够结合就好了,可以吗?(no idea)

既然子线程可以设置为detach模式在后台运行,需要注意:当子线程使用了局部变量,且局部变量的作用域结束,子线程尚未结束时,如果继续使用局部变量,会出现意想不到的错误,并且这种错误很难排查。但是,线程的参数传递是:“默认的会将传递的参数以拷贝的方式复制到线程空间”,不应该出问题吧?需要自行验证。

当使用join方式等待线程结束时,需要注意:当决定以detach方式让线程在后台运行时,可以在创建thread的实例后立即调用detach,这样线程就会和thread的实例分离,即使出现了异常thread的实例被销毁,仍然能保证线程在后台运行。但线程以join方式运行时,需要在主线程的合适位置调用join方法,如果调用join前出现了异常,thread被销毁,线程就会被异常所终结。为了避免异常将线程终结,或者由于某些原因,例如线程访问了局部变量,就要保证线程一定要在函数退出前完成,就要保证要在函数退出前调用join,此时一种比较好的方法是资源获取即初始化(RAII,Resource Acquisition Is Initialization),示例如下:

<code>class thread_guard{    thread &t;public :    explicit thread_guard(thread& _t) :        t(_t){}    ~thread_guard()    {        if (t.joinable())            t.join();    }    thread_guard(const thread_guard&) = delete;    thread_guard& operator=(const thread_guard&) = delete;};void func(){    thread t([]{        cout << "Hello Multi-thread" <<endl>/<code>

上面的例子中,使用了std::thread类的另一个成员函数joinable(),用于判断当前线程是否已经join。注意,无论是join还是detach方式,都只能调用一次

线程的入口函数参数 - 引用还是传值

入口函数的参数使用值传递还是引用传递?都可以,但是如果想在线程中对参数的修改传递出来(引用),你可能有失望了。因为默认的会将传递的参数以拷贝的方式复制到线程空间,即使参数的类型是引用,此时引用的也是线程空间中的对象,而不是初始希望改变的对象。<strong>(这也导致了我前面提到的疑惑:既然线程空间维护了参数变量的副本,即使参数变量对应的变量离开作用域被销毁,也不会影响到线程空间的拷贝吧。)示例如下:

<code>// 下面的代码据说g++编译会报错int main() {    auto func = [](std::string& ref_str){      ref_str.assign("goodbey");      std::cout << "ref:" << ref_str << std::endl;    }    std::string orig_str = "hello";    std::thread t(func, orig_str);    t.join();      std::cout <<  "orig:" <<orig>/<code>

如果想将更新的参数传递出来,可以在调用线程类构造函数的时候,

使用std::ref()。如下面修改后的代码:

<code>std::thread t(func,std::ref(orig_str));// 此时输出为:// ref: goodbey// orig: goodbey/<code>

线程对象只能移动不可复制

线程对象之间是不能复制的,只能移动,移动的意思是,将线程的所有权在std::thread实例间进行转移,使用std::move。示例如下:

<code>void func1(int);std::thread t1(func1, num); std::thread t2 = t1; // 编译错误,不可复制std::thread t3 = std::move(t1); // 正确,将对象t1负责管理的线程转移给t2/<code>

最后附上std::thread类的成员函数:

(constructor) Construct thread (public member function )

(destructor) Thread destructor (public member function )

operator= Move-assign thread (public member function )

get_id Get thread id (public member function )

joinable Check if joinable (public member function )

join

Join thread (public member function )

detach Detach thread (public member function )

swap Swap threads (public member function )

native_handle Get native handle (public member function )

hardware_concurrency [static] Detect hardware concurrency (public static member function )


分享到:


相關文章: