• 认真地记录技术中遇到的坑!

C++11线程库 第一节 线程管理

C/C++ Moxun 8个月前 (03-01) 193次浏览 0个评论

一、基本线程管理

1.1启动线程

    1. 包含头文件  #include
    2.使用可调用对象构造thread对象(可调用对象包括:函数、lambda表达式、重载了调用运算符的类)
    注:对于重载了调用运算符的类,有以下点需要注意,假设有一个重载了调用运算符的类A,那么正确的做法是:
        A a;
        std::thread my_thread(a);
        或者
        std::thread my_thread((A()));
        或者
        std::thread my_thread{A()};
    3.一旦启动线程后,你需要显式的决定是等待它运行完成(调用join())还是让它自行运行(调用detach),如果你在线程运行完还没有做出决定,那么程序将会被终止,进入terminate,如果不等待线程执行完成,那么你要确保通过该线程访问的数据是有效的,直到该线程完成为止。(通常你要确保在线程对象被销毁前就调用join或者detach)
    示例:
    ```cpp

struct func
{
int &i;
func(int& i_):i(i_){}
void operator()
{
for(unsigned j=0; j < 10000;++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();
}

       在上述代码中,我们使用了detach(),它的意思是不等待线程的完成,由它自行运行,那么在这种情况下,线程的运行是分离式的,当oops函数退出的时候,线程my_thread可能还在执行,那么当my_func的循环下一次执行时可能就导致了对悬空引用的访问(因为some_local_state已经被销毁了,但是线程还在使用它),如果,线程的运行方式我们选择join方式,那么就不会方式上述情况,你可以认为join时阻塞式的,函数oops会一直等待线程my_thread完成才会继续执行。join简单粗暴,要么等待线程完成要么不等待。对一个给定的线程只能调用一次join(),一旦调用了join(),那么joinable()就会返回false。调用join后,会清理与该线程关联的存储器。

## 1.2在异常环境下的等待
      detach和join的调用有一点区别,如果你决定使线程分离式运行,那么在线程启动后就可以立即调用detach,但是如果你打算等待该线程,那么你需要仔细的思考应该在代码的哪个位置选择调用join,这样就导致了一个问题,假设在线程启动之后join调用之前发生了异常,那么这意味着join调用将有可能被跳过,那么这样的join调用不是异常安全的,如果join被跳过,那么在线程对象销毁时还未调用,此时系统会自动调用terminate函数,进而导致程序崩溃。
      为了解决这种问题,有几种方案:
      A.```cpp
void oops()
{
    std::thread my_thread(my_func);
    try
    {
        ……
    }
    catch()
    {
        my_thread.join();
        throw;
    }
    my_thread.join();
}

  B.使用标准的资源获取即初始化(RAII:Resource Acquisition Is Initialization)
  注:资源获取即初始化的解释
  资源获取即初始化是指,当你获得一个资源的时候,无论这个资源是对象、内存、句柄还是文件或者其它什么,你都会在一个对象的构造函数中获得它,并在该对象的析构函数中释放它。实现这种功能的类我们说它采用了“资源获取即初始化”的方式,这样的类常常被称为封装类。
  资源可变性:如果一个封装类对其实例提供额外的功能,使得其实例能被赋予新资源,这个类表现出的这种特征即称为"可变的RAII",否则就是"不可变的RAII"。
  不可变的RAII,是使用起来最简单的一种。说它简单,是因为在这种情况下,无需在封装类中提供用于指定资源的方法--不管是新分配的资源,还是对其他资源进行拷贝。这种RAII还意味着,类的析构函数总是可以假定,被封装的资源是有效的。
  与此相反,提供可变的RAII的类,就需要实现下列功能中的绝大部分,或者全部:缺省的或者空的构造函数,拷贝构造函数,拷贝赋值操作,用于指定资源的方法。最重要的是,这样的类在析构函数和任何类似close()的方法中,释放资源前,都必须检测被封装的资源是不是null。
  资源来源:
   对于提供RAII的类来说,第二个重要的特征是,它们通过什么途径获取自己所管理的资源。以std::string为代表的类,使用的是内部初始化的RAII:它管理的资源--即内存中用于保存字符的缓冲区--是由它自己创建的,这一资源对外永远是不可见的。与此不同的是,以std::auto_ptr为代表的类表现出外部初始化的RAII行为:它所管理的资源,是使用它的客户程序(通过另外的某种方式获得之后)交给它的。
   内部初始化的RAII的封装类,一般比较容易实现,但是功能上也比较受限制,因为它们获取资源的机制是预先定义好的,并且是固定不变的。不过,这样的类用起来也容易一些,或者说,比较难被误用:因为客户代码几乎没有机会犯下能导致资源泄露的错误。

   使用局部对象管理资源的技术通常称为“资源获取就是初始化(RAII)”。 这种通用技术依赖于构造函数和析构函数的性质以及它们与异常处理的交互作用。

 资源获取即初始化是一个简单的概念。它所表达的全部意思就是:对象的初始化(即构造函数、调用),包含对它所要管理的资源的获取操作。它所隐含的另一方面意思就是:对象的析构(析构函数的调用)会自动引发资源的释放。

   RAII就是这样一个机制,它利用C++对构造函数和自动的确定性析构函数的支持,来确保跟某个封装类型的实例相关的资源能够得到确定性的释放。
   典型的RAII代码是这样的:
class Resource  

{  

public:  

         Resource() {/*分配资源*/}  

         ~ Resource() {/*释放资源*/}    

};
RAII技术应用于线程,我们可以编写这样一个类:
```cpp
class thread_guard
    {
    std::thread &amp;t;
    public:
         explicit thread_guard(std::thread &amp;t_):t(t_)
             {}
    ~thread_guard()
        {
        if(t.joinable())
            {
            t.join();
        }
    }

    thread_guard(thread_guard const &amp;) = delete;
    thread_guard &amp;operator = (thread_guard const &amp;) = delete;
}

使用thread_guard我们可以启动线程,我们可以确保在启动线程之后调用join之前发生异常时也能正常调用join。(假设这个异常中断了你程序的正常流程而不是引发了程序的崩溃),示例:

#include 
#include 
#include 
#include 
#include 

using namespace std;


class thread_guard
{
    thread &amp;t;
public:
    explicit thread_guard(std::thread &amp;t_) :t(t_) {};
    ~thread_guard()
    {
        if (t.joinable())
        {
            t.join();
        }
    }
    thread_guard(thread_guard const &amp;) = delete;
    thread_guard&amp; operator = (thread_guard const &amp;) = delete;
private:
};

int funcA(int i,int j)
{
    if (0 == j)
    {
        throw(runtime_error("除数不能为0"));//此处会中断函数的执行,直接退出函数
    }
    cout &lt;&lt; &quot;Hello World!&quot;;
    return i / j;
}
void funcB()
{
    funcA(1, 0);
}

void my_func()
{
    cout &lt;&lt; &quot;Hello Thread!&quot; &lt;&lt; endl;

}
void f()
{
    thread my_thread(my_func);
    thread_guard g(my_thread);
    funcB();
    cout &lt;&lt; &quot;Go!&quot; &lt;&lt; endl;
}

int main(int argc, char *argv[])
{
    try
    {
        f();
    }
    catch (runtime_error &amp;e)
    {
        cout &lt;&lt; e.what() &lt;&lt; endl;
    }

    getchar();
    return EXIT_SUCCESS;
}

上面这段代码如果有不理解的地方,建议先阅读异常查找链和C++异常处理机制有关的知识。

1.3在后台运行线程

  detach方法,直观的说,它解除了通过线程对象启动的线程和线程对象本身的联系,这样即使线程对象被销毁了,也不会调用std::terminate函数导致程序崩溃,此时线程在后台运行,不能直接与之通信,也不能获取一个引用它的thread对象,所以对它无法再调用join,此时线程的控制权和所有权被移交给了C++运行时库,以确保与线程相关的资源在线程退出后可以被正确回收。这种线程叫做守护线程,它的生命周期几乎贯穿整个应用程序。当然你只能在joninable()返回为true的时候才能调用detach()

1.4给线程函数传递参数

      注:进程和线程共享地址空间,但是在这个地址空间内每个线程有自己独立的内存空间,形象的说,进程只是线程的一个容器。如图:
      [![](http://uusama.com/wp-content/uploads/2018/03/2018030505172491-300x142.png)](http://uusama.com/wp-content/uploads/2018/03/2018030505172491.png)

      图中有一些留白的部分,进程和线程有一些共享的操作系统资源。线程在它自己的内存空间里运行。
       注:线程传参有一个非常重要的点需要注意,通过线程函数传递的参数并不是直接传给线程函数的而是先将这些参数转存在一个线程内部的一个中转站中,之后在函数执行的时候才会正的把参数传递给函数。另外一点非常重要的是,在通过线程对象给线程函数传参的时候,只是将参数进行简单的赋值。

       那么因为这样一种机制,就导致了给线程函数传递一个局部变量时,局部变量已被销毁,而线程还在使用它的情况,从而导致程序错误,特别是容易引发对空悬指针或者空悬引用的使用。例如下面的代码片段:
       ```cpp
void f(int i,std::string const &amp;s);
void oops(int some_param)
    {
    char buffer[1024] = {0};
    sprintf_s(buffer,"%d",some_param);
    std::thread t(f,3,buffer);
    t.deatch();
}

上述代码片段的问题在于,在线程函数真正需要将buffer转换为string时,buffer这个临时变量已经被销毁,此时访问了一个无效指针,进而引发了不可预知的错误。解决这个问题的思路是我们在线程函数执行之前事先完成好这个转换。例如:

void f(int i,std::string const &amp;s);
void not_oops(int param)
    {
    char buffer[1024] ={0};
    sprintf_s(buffer,"%d",param);
    std::thread t(f,3,std::string(buffer));
    t.detach();
}

另外一个由上述参数传递机制导致的问题是,当线程函数需要的实际上是引用的时候,因为简单赋值的问题,导致传递给线程函数的实际上线程内部中转空间里保存下来的那个值的引用,而不是我们预期的实参的引用,当线程函数需要引用时,我们可以这样传递参数(使用std::ref来包装确实需要传递被引用的参数)

void f(int i,std::string const &amp;s);

void oops(int some_parma)
    {
    std::string strTest = "Hello World";
    std::thread t(f,1,std::ref(strTest));
    t.detach();
}

1.5 使成员函数做线程函数

  std::thread的构造函数和std::bind的机制是相同的(然鹅,我没有查到这方面的资料,哪位大神知道可以告诉我),但是std::bind这个东西就是个适配器,适配器什么意思呢,就像你给三项插座插个转换头就可以使用两孔插头,差不多就是这个道理。因此我们可以给thread对象传递一个成员函数的指针作为线程函数,于此同时,必须要再传递一个合适的对象指针作为第二个参数,例如:
  ```cpp

class X
{
public :
void thread_func();
};

X my_X;
std::thread t(&X::thread_func,&my_X);

在上面这个示例代码中,如果这个成员函数需要传递参数,那么把它依次放在thread构造函数的第二(成员函数的第一个参数)、第三(依次类推)个参数的位置就可以了。

     像线程函数传递类似于智能指针这样参数,示例:
     ```cpp
void process_big_object(std::unique_ptr<big>);
std::unique_ptr<big> p(new big_object);
p-&gt;prepaerdata(42);
std::thread t(process_big_object,std::move(p));

  只用std::move的原因是unique_ptr是一个动态对象,为了使智能指针的引用计数器可以正常工作,我们使用std::move把所有权和控制权移交给另外一个对象,这样原对象就变成了nullptr。

  thread的每一个实例负责管理一个执行线程,线程的所有权可以在不同的thread实例之间转移,这能确保在任意时刻只有一个thread对象和执行线程关联。

1.5转移线程的所有权

     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::move是隐式调用的
     std::thread t3;
     t3 = std::move(t2);
     t1 = std::move(t3);

     在执行t1 = std::move(t3)时会使系统自动调用terminate(),因为你不能通过向一个线程对象赋予新值而舍弃这个线程对象原本关联的对象。

     以上线程对象之间所有权和控制权的转移说明了这样一个事实,我们可以使用std::thread做函数的参数或者函数返回类型。

     在线程移动的理论基础上,我们可以对thread_guard类进行改进, 以防止thread_guard对象在引用它的线程结束后继续存在的不良影响,也就是一旦所有权转移到了该对象,那么其他所有对象都不能结合或者分离该线程。接下来这个改进类的主要目的是确保在退出一个作用域之前线程都以完成,代码片段如下:
     ```cpp

class scoped_thread
{
std::thread t;
public :
explicit scoped_thread(std::thread t_):
t(std::move(t_))
{
if(!t.joninable())
{
throw std::logic_errror(“No thread”);
}
~scoped_thread()
{
t.join();
}

    scoped_thread(scoped_thread const &amp;)  =delete;
    scoped_thread &amp; operator = (scoped_thread const &amp;) = delete;
}

}


std::thread支持移动的另外一个好处是类似std::vector这样移动感知的容器,允许你编写如下形式的代码,以生成一批线程,然后等待它们完成 ```cpp void do_work(unsigned int i); void f() { std::vector threads; for(unsigned i =0;i&lt;20;++i) { threads.push_back(std::thread(do_work,i); ) std::for_each(threads.begin(),threads.end(),std::mem_fn(&amp;std::thread::join));//轮流在每个线程 上调用join() } }
 启发:我们可以使用这种模式来设计并行算法,以提高算法的性能。

1.7在运行时选择创建线程的数量

   std::thread::hardware_currency(),这个函数返回一个对于给定程序执行时能够真正并发运行的线程数量的指示,例如,在多核CPU上它可能是CPU核心的数量。它仅仅是一个提示,如果该信息不可用那么它会返回0,但它对于在线程间分隔任务是一个很好的指南。

1.8 标识线程

  标识线程符的类型是:std::thread::id,两种获取方式:
  A.通过和与线程关联的对象通过调用get_id()成员函数来获得。如果当前std::thread没有相关联的对象,那么get_id() 返回一个默认构造的std::thread::id 对象,标识“没有线程”!
  B.当前线程的标识符还可以通过调用std::this_thread::get_id()获得,这也是定义在头文件中的。
  线程库为std::thread::id类型的对象提供了一套完整的比较操作,如果两个std::thread::id相等,说明它们要么是同一个线程要么都没有和线程关联,提供完整的比较操作意味着它可以做容器的键。另外,标准库还提供了std::hash这样,std::thread::id就可以做新的无序关联容器中作为主键来使用了。另外,线程ID可以指定数据需要和一个线程进行关联(启示:可以做线程同步用)。

喜欢 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址