std::thread详解

1. std::thread基本介绍

   1)构造std::thread对象时,如果不带参则会创建一个空的thread对象,但底层线程并没有真正被创建,一般可将其他std::thread对象通过move移入其中;

      如果带参则会创建新线程,而且会被立即运行。

   2)joinable():用于判断std::thread对象联结状态,一个std::thread对象只可能处于可联结或不可联结两种状态之一。

      a. 可联结:当线程己运行或可运行、或处于阻塞时是可联结的。注意,如果某个底层线程已经执行完任务,但是没有被join的话,仍然处于joinable状态。

         即std::thread对象(对象由父线程所有)与底层线程保持着关联时,为joinable状态。

      b. 不可联结:

     ① 当不带参构造的std::thread对象为不可联结,因为底层线程还没创建。

     ② 己移动的std::thread对象为不可联结。

     ③ 己调用join或detach的对象为不可联结状态。因为调用join()以后,底层线程己结束,而detach()会把std::thread对象和对应的底层线程之间的连接断开。

      join():等待子线程,调用线程处于阻塞模式。join()执行完成之后,底层线程id被设置为0,即joinable()变为false。

    detach():分离子线程,与当前线程的连接被断开,子线程成为后台线程,被C++运行时库接管。

   3)std::thread对象析构时,会先判断是否可joinable(),如果可联结,则程序会直接被终止出错。这意味着创建thread对象以后,要在随后的某个地方调用join或

      detach以便让std::thread处于不可联结状态。

   4)std::thread对象不能被复制和赋值,只能被移动。

  5)获取当前信息

// t为std::thread对象
t.get_id();                          // 获取线程ID
t.native_handle();                   // 返回与操作系统相关的线程句柄
std::thread::hardware_concurrency(); // 获取CPU核数,失败时返回0

   6)std::this_thread命名空间中相关辅助函数

get_id();                            // 获取线程ID
yield();                             // 当前线程放弃执行,操作系统转去调度另一线程
sleep_until(const xtime* _Abs_time); // 线程休眠至某个指定的时刻(time point),该线程才被重新唤醒
sleep_for(std::chrono::seconds(3));  // 睡眠3秒后才被重新唤醒,不过由于线程调度等原因,实际休眠时间可能比 sleep_duration 所表示的时间片更长

 

3. 传递参数的方式(2次传参)

   a. 第一次传参(向 std::thread 构造函数传参):在创建thread对象时,std::thread构建函数中的所有参数均会按值并以副本的形式保存成一个tuple对象。

      该tuple由调用线程(一般是主线程)在堆上创建,并交由子线程管理,在子线程结束时同时被释放。

      注:如果要达到按引用传参的效果,可使用std::ref来传递。

   b. 第二次传参(向线程函数的传参):由于std::thread对象里保存的是参数的副本,为了效率同时兼顾一些只移动类型的对象,所有的副本均被

      std::move到线程函数,即以右值的形式传入,所以最终传给线程函数参数的均为右值。

   现在我们根据线程参数的类型展开讨论:

   首先先给出一个用作测试的类

class Test 
{
public:
    mutable int mutableInt = 0;
    Test() : mutableInt(0) { cout << this << " " << "Test()" << endl; }
    Test(int i) : mutableInt(i) { cout << this << " " << "Test(int i)" << endl; }
    Test(const Test& w) : mutableInt(w.mutableInt) { cout << this << " " << "Test(const Test& w)" << endl; }
    Test(Test&& w)  noexcept  // 移动构造
    { 
        mutableInt = w.mutableInt; 
        cout << this << " " << "Test(Test && w)" << endl;
    }
    void func(const string& s) { cout <<"void func(string& s)" << endl; }
};

   a. 线程参数为const T&类型:这里的引用是针对于第二次传递的参数,也就是直接引用保存在tuple中的参数副本,而不是最原始的参数,即

      不是main中变量。但是你如果第一次向std::thread传参时使用std::ref,首先会创建一个std::ref(它也是一个类)临时对象,里面会保存着最原始

      变量(main中的变量)的引用,然后这个std::ref临时对象再以副本的形式保存在std::thread中,随后这个副本被move到线程函数。由于std::ref

      重载了类型转换运算符operator T&(),因此会隐式转换为Test&类型,因此起到的效果就好象main中的变量直接被按引用传递到线程函数中来。

      现在按照这个理解来分析下面的代码:

      对于std::thread t1(test_ctor, w); 首先会调用一次拷贝构造,把w存储为tuple元素;然后因为线程参数是引用,所以tuple元素给线程传参数时不会发生

      拷贝,但实际运行结果发现多输出了一次拷贝,这应该是std::thread隐藏的实现细节,需要阅读源码了。

      如果使用std::ref包装的话,内部引用了原始的w,所以不会发生拷贝,但会发生std::ref对象的拷贝。

void test_ctor(const Test& w) { cout << &w << " " << "w.matableInt = " << ++w.mutableInt << endl; }

int main()
{
    Test w;

    // std::thread默认的按值传参方式: 所有的实参都是被拷贝到std::thread对象的tuple中,即以副本形式被保存起来。
    // 注意,w是按值保存到std::thread中的,会调用其拷贝构造函数。外部的w没受影响。mutableInf仍为0。
    std::thread t1(test_ctor, w);
    t1.join();
    cout << "w.mutableInt = " << w.mutableInt << endl << endl;

    // std::thread按引用传参(std::ref), 因为w是按引用传入到std::ref对象中的,不会调用其拷贝构造函数。
    // 由于w按引用传递,mutableInf被修改为1。
    std::thread t2(test_ctor, std::ref(w));
    t2.join();
    cout << "w.mutableInt = " << w.mutableInt << endl;
    return 0;
}

// 第一部分输出如下
Test(const Test& w)  // 调用拷贝构造函数生成std::thread中的副本对象
Test(Test && w)      // std::thread中的副本移动到线程参数
w.mutableInt = 0

// 第二部分
w.mutableInt = 1

    b. 线程参数为T&类型:最终传给线程函数参数的均为右值,而T&类型是不接受右值的,使用std::ref包装后便不报错了,因为它能隐式转换成 T&。

void updateTest_ref(Test& w) { cout << &w << " " << "invoke updateTest_ref" << endl; }

int main()
{
    Test w;
    std::thread t1(updateTest_ref, w);             // 编译失败,因为std::thread内部是以右值形式向线程函数updateTest_ref(Test&)传参的,
                                                   // 而右值无法用来初始化Test&引用。
    std::thread t3(updateTest_ref, std::ref(w));   // ok, 原因类似test_ctor函数中的分析。即当线程函数的形参为T&时,一般以std::ref形式传入
    t3.join();
}

 

posted @ 2020-05-26 15:22  _yanghh  阅读(10370)  评论(0编辑  收藏  举报