c++并发编程(一)---基本线程管理

来源:互联网 发布:二战美国驱逐舰数据 编辑:程序博客网 时间:2024/06/06 01:27

1. 启动线程

线程是通过构造std::thread对象开始的,对该对象指定了线程上要运行的任务,在最简单的情况下,该任务仅仅是一个普普通通的返回void而且不接受参数的函数,这个函数在自己的线程上运行,直到返回,然后线程停止。但从另一个极端看,该任务可能是一个接收额外参数的函数对象,放它运行时,会执行一系列由某种消息机制所指定的相互独立的操作,并且只有当线程再次通过某种消息机制接收到信号才会停止。无论线程将要做什么或是从哪里启动,都需要构造std::thread对象。
 #include<thread> void do_some_work() std::thread my_thread(do_some_work);

也可以将一个带有函数调用操作符的实例(如仿函数)传递给std::thread的构造函数来进行替代。

class background_task{     public:          void operator() ()const          {               do_something();               do_something_else();          }};background_task f;std::thread my_thread(f);

还可以使用lambda表达式。

std::thread my_thread([](){               do_something();               });

一旦线程启动,你需要显式的决定等待完成还是让它自行运行,如果你在std::thread对象被销毁前还未做决定,那么你的程序会被终止(std::thread的析构函数会调用std::terminate())。因此在异常存在的情况下,确保线程正确地结合或者分离是当务之急。如果你不等待线程完成,那么你需要确保通过该线程访问的数据是有效的,直到线程完成为止。在对象销毁后还访问它是未定义行为。
如:

struct func{     int& i;     func(int& i_):i(i_){}     void operator() ()     {           for(unsigned int j=0;j<1000000;++j)           {               do_something(i);           }     }};void oops(){      int some_local_state=0;      func my_func(some_local_state);      std::thread my_thread(my_func);      my_thread.detach();    //不等待线程完成                                             //新的线程仍在运行}

在这种情况下,当oops退出时线程还在运行,但是存储在栈中的局部变量随着函数的退出被释放,此时线程中仍在访问该变量,所以会发生未定义行为。此时可在代码中加入my_thread.join()解决。使用join()需要注意一个问题,为了避免应用程序在引发异常的时候被终止,需要注意在存在异常的时候调用join(),以避免意外的生命周期问题。

struct fun;   //详见上图代码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();    //正常退出条件下}

使用try/catch块很啰嗦,而且很容易将作用域弄luan,这里可以使用资源获取即初始化(RAII)惯用方法,并提供一个类,在它的析构函数中进行join()。

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;//将拷贝构造函数和拷贝运算符标记为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首先被销毁,并且析构函数中t.join()被结合,即使函数因为do_something_in_current_thread引发异常而退出的情况下也会发生。如果无需等待线程完成,可以通过分离来避免这个异常安全问题。这打破了线程与std::thread对象联系并确保当std::thread对象被销毁时std::terminate()不会被调用,即使线程仍在后台运行。

2. 传递参数给线程函数

    传递参数给可调用对象或函数,基本上就是简单的将额外的参数传递给std::thread的构造函数,但重要的是参数会以默认的方式被复制到内部的存储空间,在那里新创建的执行线程可以访问它们,即便函数中相应参数期待着引用。
   

void f(int i,std::string const & s);std::thread t(f,3,"hello");

这里创建一个新的与t相关联的执行线程,称为f(3,”hello”)。注意即使f接收std::string作为第二个参数时,字符串字面值仅在新线程的上下文中才作为char const*传送,并转换为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();}

如果oops在buffer在转化成std::string之前退出会发生未定义行为,因此解决之道是直接在std::thread的构造函数中传入std::string(buffer);

3.转移线程所有权。

c++标准库有许多拥有资源的类型,如std::ifstream和std::unique_ptr,都是可移动的而非可复制的,而且std::thread也是其中之一,都将拷贝构造函数和拷贝运算符写入私有,或者声明为delete。一个特定执行的线程所有权可以在std::thread之间移动。如下例所示,该实例创建了两个执行线程,以及三个std::thread实例t1,t2和t3之间对那些线程的所有权进行转移。

 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关联,然后t2构建完成时所有权被转移给t2,通过调用std::move()来显式地转移所有权②。此刻t1不再拥有相关联的执行线程,运行some_function的线程现在与t2相关联。
然后,启动一个新的线程并与一个临时的std::thread对象相关联③,接下来将所以权转移到t1中,是不需要调用std::move()来显式转移所有权的,因为此处所有者是一个临时对象-从临时对象中进行移动是自动的和隐式的。
t3是默认构造的④,这意味着没有默认相关联的线程,当前t2转移给t3,t1与some_other_function()相关联,t2没有关联的线程,t3与运行的some_function()关联。
最后一次移动⑥将运行some_function()的线程的所以权转回给t1,但t1已经有一个相关联的线程,所以会调用std::terminate()来终止程序。

原创粉丝点击