第24課 std::thread線程類及傳參問題

一. std::thread類ios

(一)thread類摘要及分析編程

class thread { // class for observing and managing threads
public:
    class id;

    using native_handle_type = void*;

    thread() noexcept : _Thr{} { // 建立空的thread對象,實際上線程並未被建立!
    }

private:
    template <class _Tuple, size_t... _Indices>
    static unsigned int __stdcall _Invoke(void* _RawVals) noexcept { // enforces termination
        //接口適配:將用戶的可調用對象與_beginthreadex的接口進行適配。

        //子線程從新擁有從主線程轉讓過來的保存着thread參數副本的tuple堆對象的全部權。
        const unique_ptr<_Tuple> _FnVals(static_cast<_Tuple*>(_RawVals));
        _Tuple& _Tup = *_FnVals;
        _STD invoke(_STD move(_STD get<_Indices>(_Tup))...); //注意,因爲tuple中保存的都是副本,所以全部的參數都以右值的方式被轉發出去。
        _Cnd_do_broadcast_at_thread_exit(); // TRANSITION, ABI
        return 0;
    }

    template <class _Tuple, size_t... _Indices>
    _NODISCARD static constexpr auto _Get_invoke(
        index_sequence<_Indices...>) noexcept { // select specialization of _Invoke to use
        return &_Invoke<_Tuple, _Indices...>;   //這裏返回特化的_Invoke函數指針
    }

public:
    template <class _Fn, class... _Args, class = enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>>>
    explicit thread(_Fn&& _Fx, _Args&& ... _Ax) { // construct with _Fx(_Ax...)
        using _Tuple                 = tuple<decay_t<_Fn>, decay_t<_Args>...>; //將傳入thread的全部參數保存着tuple

        //在堆上建立tuple以按值保存thread全部參數的副本,指針用unique_ptr來管理。
        auto _Decay_copied = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...); //建立tuple的智能指針
        constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{}); //獲取線程函數地址

        //在Windows系統中,會調用_beginthredex來建立新線程。其中,_Invoker_proc爲線程函數地址,它要求的參數爲tuple的指針,即_Decay_copied.get()
        //注意:線程建立後即當即運行(第5個參數爲0),原生的線程id保存在_Thr._Id中,句柄保存在_Thr._Hnd。
        _Thr._Hnd =
            reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id));
        if (_Thr._Hnd == nullptr) { // failed to start thread
            _Thr._Id = 0;
            _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);
        }
        else { // ownership transferred to the thread
            (void)_Decay_copied.release(); //轉讓tuple的全部權給新的線程。
        }
    }

    ~thread() noexcept { // clean up
        if (joinable()) {  //注意,std::thread析構時,若是線程仍可joinable,則會調用terminate終止程序!
            _STD terminate();
        }
    }

    thread(thread&& _Other) noexcept : _Thr(_STD exchange(_Other._Thr, {})) { // move from _Other
    }

    thread& operator=(thread&& _Other) noexcept { // move from _Other

        if (joinable()) {
            _STD terminate();
        }

        _Thr = _STD exchange(_Other._Thr, {});
        return *this;
    }

    thread(const thread&) = delete;    //thread對象不能被複制
    thread& operator=(const thread&) = delete; //thread對象不能被拷貝賦值

    void swap(thread& _Other) noexcept { // swap with _Other
        _STD swap(_Thr, _Other._Thr);
    }

    _NODISCARD bool joinable() const noexcept { // return true if this thread can be joined
        return _Thr._Id != 0; //原生的線程id不爲0,表示底層的線程己經建立
    }

    void join() { // join thread
        if (!joinable()) {
            _Throw_Cpp_error(_INVALID_ARGUMENT);
        }

        if (_Thr._Id == _Thrd_id()) {
            _Throw_Cpp_error(_RESOURCE_DEADLOCK_WOULD_OCCUR);
        }

        if (_Thrd_join(_Thr, nullptr) != _Thrd_success) {
            _Throw_Cpp_error(_NO_SUCH_PROCESS);
        }

        _Thr = {}; //注意調用join之後,原生線程id被清零,意味着join只能被調用一次!
    }

    void detach() { // detach thread
        if (!joinable()) {
            _Throw_Cpp_error(_INVALID_ARGUMENT);
        }

        _Check_C_return(_Thrd_detach(_Thr)); //線程被分離,成爲後臺線程
        _Thr = {};  //注意調用detach之後,原生線程id被清零,意味着detach也只能被調用一次!
    }

    _NODISCARD id get_id() const noexcept;

    _NODISCARD static unsigned int hardware_concurrency() noexcept { // return number of hardware thread contexts
        return _Thrd_hardware_concurrency();
    }

    _NODISCARD native_handle_type native_handle() { // return Win32 HANDLE as void *
        return _Thr._Hnd;
    }

private:
    _Thrd_t _Thr;
};
std::thread類摘要

  1. 構造std::thread對象時,若是不帶參則會建立一個空的thread對象但底層線程並無真正被建立。若是帶參則會建立新線程,並且會被當即運行。通常用於接受其餘std::thread對象,經過move移入其中。ide

  2. 在建立thread對象時,std::thread構建函數中的全部參數均會按值以副本的形式保存成一個tuple對象該tuple由調用線程(通常是主線程)在堆上建立,並交由子線程管理,在子線程結束時同時被釋放函數

  3. joinable():用於判斷std::thread對象聯結狀態,一個std::thread對象只可能處於可聯結或不可聯結兩種狀態之一。this

  (1)可聯結:當線程己運行或可運行、或處於阻塞時是可聯結的。注意,若是某個底層線程已經執行完任務可是沒有被join的話仍然處於joinable狀態。即std::thread對象與底層線程保持着關聯時,爲joinable狀態。spa

  (2)不可聯結:操作系統

    ①當不帶參構造的std::thread對象爲不可聯結,由於底層線程還沒建立。線程

    ②己移動的std::thread對象爲不可聯結。3d

    ③己調用join或detach的對象爲不可聯結狀態。由於調用join()之後,底層線程己結束,而detach()會把std::thread對象和對應的底層線程之間的鏈接斷開。指針

  4. std::thread對象析構時,會先判斷是否可joinable(),若是可聯結,則會程序會直接被終止。這意味着建立thread對象之後,要在隨後的某個地方調用join或detach以便讓std::thread處於不可聯結狀態

  5. std::thread對象不能被複制和賦值,只能被移動。

(二)線程的基本用法

  1. 獲取當前信息

  (1)線程ID:t.get_id();  //其中t爲std::thread對象。

  (2)線程句柄:t.native_handle() //返回與操做系統相關的線程句柄。

  (3)獲取CPU核數:std::thread::hardware_concurrency(),失敗時返回0。

  2.線程等待和分離

  (1)join():等待子線程,調用線程處於阻塞模式

  (2)detach():分離子線程,與當前線程的鏈接被斷開,子線程成爲後臺線程,被C++運行時庫接管。

  (3)joinable():檢查線程是否可被聯結。

(三)std::this_thread命名空間中相關輔助函數

  1. get_id(); //獲取線程ID:

  2. yield(); //當前線程放棄執行,操做系統轉去調度另外一線程。

  3. sleep_until(const xtime* _Abs_time):線程休眠至某個指定的時刻(time point),該線程才被從新喚醒。

  4. sleep_for(std::chrono::seconds(3));//睡眠3秒後才被從新喚醒,不過因爲線程調度等緣由,實際休眠時間可能比 sleep_duration 所表示的時間片更長。

【編程實驗】std::thread的基本用法

#include <iostream>
#include <thread>
#include <chrono>  //for std::chrono::seconds
#include <ctime>   //for std::time_t
#include <iomanip> //for std::put_time

using namespace std;
using namespace std::chrono;   

void thread_func(int x)
{
    cout <<"thread_func start..." << endl;
    cout << "x = " << x << endl;
    cout << "child thread id: " << std::this_thread::get_id() << endl;

    std::this_thread::yield(); //當前線程放棄執行

    cout <<"thread_func end." << endl;
}

void test_sleepUntil()
{
    std::cout <<"thread id " << std::this_thread::get_id() << "'s sleepUntil begin..." << endl;
    using std::chrono::system_clock;
    std::time_t tStart = system_clock::to_time_t(system_clock::now()); //to_time_t:將time_point轉爲std::time_t
    struct std::tm tm;
    localtime_s(&tm,&tStart);

    std::cout << "Current time: " << std::put_time(&tm, "%X") << std::endl; //X須大寫,若小寫輸出日期
    std::cout << "Waiting for the next minute..." << std::endl;
    
    ++tm.tm_min;
    tm.tm_sec = 0;
    std::this_thread::sleep_until(system_clock::from_time_t(mktime(&tm))); //from_time_t:將time_t轉爲time_point

    std::cout << std::put_time(&tm, "%X") <<" reach."<<  std::endl; 

    std::cout << "thread id " << std::this_thread::get_id() << "'s sleepUntil end." << endl;
}

int main()
{
    //1. 獲取當前線程信息
    cout << "hardware_concurrency: " << std::thread::hardware_concurrency() << endl; //8,當前cpu核數
    cout << "main thread id: " <<std::this_thread::get_id() << endl; //當前線程(主線程)id

    std::thread t(thread_func, 5);
    cout <<"child thread id: " <<t.get_id() << endl; //子線程id
    cout << "child thread handle: " << t.native_handle() << endl;

    //2.joinable檢查
    cout << endl;
    std::this_thread::sleep_for(std::chrono::seconds(3)); //主線程睡眠3秒,等待子線程結束

    if (t.joinable()) 
        cout << "t is joinable" << endl;   //該行打印,說明子線程己結束時,仍處於joinable狀態!!!
    else 
        cout << "t is unjoinable" << endl;

    t.join();

    //sleep_until
    cout << endl;
    std::thread t2(test_sleepUntil);
    t2.join();

    //傳入lambda
    cout << endl;
    std::thread t3([]() {cout <<"t3(thread id: " << std::this_thread::get_id()<< ") is running..." << endl; });
    t3.join();

    return 0;
}
/*輸出結果
hardware_concurrency: 8
main thread id: 17672
child thread id: 8172
child thread handle: 000000E4

thread_func start...
x = 5
child thread id: 8172
thread_func end.
t is joinable

thread id 8016's sleepUntil begin...
Current time: 23:21:25
Waiting for the next minute...
23:22:00 reach.
thread id 8016's sleepUntil end.

t3(thread id: 2880) is running...
*/

二. 傳遞參數的方式

(一)傳參中的陷阱:

  1. 向std::thread 構造函數傳參:全部參數(含第1個參數可調用對象)均按值以副本的形式保存在std::thread對象中的tuple裏。這一點的實現相似於std::bind。若是要達到按引用傳參的效果,可以使用std::ref來傳遞

  2. 向線程函數的傳參:因爲std::thread對象裏保存的是參數的副本,爲了效率同時兼顧一些只移動類型的對象,全部的副本均被std::move到線程函數,即以右值的形式傳入

(二)注意事項

  1. 一個實參從主線程傳遞到子線程的線程函數中,須要通過兩次傳遞第1次發生在std::thread構造時,這次參數按值並以副本形式被保存第2次發生在向線程函數傳遞時,這次傳遞是由子線程發起,並將以前std::thread內部保存的副本以右值的形式(std::move())傳入線程函數中的。

  2. 若是線程函數的形參爲T、const T&或T&&類型時,std::thread的構造函數能夠接受左值或右值實參。由於不論是左值仍是右值,在std::thread中均是以副本形式被保存,並在第2次向線程函數傳參時以右值方式傳入,而以上三種形參都可接受右值。

  3. 而若是線程函數的形參爲T&不論是左值仍是右值的T類型實參,都是沒法直接經std::thread傳遞給形參爲T&的線程函數,由於該實參數的副本會被std::move成右值並傳遞線程函數,但T&沒法接受右值類型。所以,須要以std::ref形式傳入(具體原理見下面《編程實驗》中的註釋)

  4. 當向線程函數傳參時,可能發生隱式類型轉換,這種轉換是在子線程中進行的。須要注意,因爲隱式轉換會構造臨時對象,並將該對象(是個右值)傳入線程函數,所以線程函數的形參應該是可接受右值類型的T、const T&或T&&類型,但不能是T&類型。此外,若是源類型是指針或引用類型時,還要防止可能發生懸空指針和懸空引用的現象。

【編程實驗】std::thread傳參中的陷阱

#include <iostream>
#include <thread>
#include <chrono>

using namespace std;
using namespace std::chrono;   //for std::chrono::seconds

class Widget 
{
public:
    mutable int mutableInt = 0;

    //Widget() :mutableInt(0) {}
    Widget() : mutableInt(0) { cout << "Widget(), thread id = "<< std::this_thread::get_id() << endl;}

    //類型轉換構造函數
    Widget(int i):mutableInt(i){ cout << "Widget(int i), thread id = " << std::this_thread::get_id() << endl; }

    Widget(const Widget& w):mutableInt(w.mutableInt) { cout << "Widget(const Widget& w), thread id = " << std::this_thread::get_id() << endl; }
    Widget(Widget&& w)  noexcept  //移動構造
    { 
        mutableInt = w.mutableInt; 
        cout << "Widget(Widget && w), thread id = " << std::this_thread::get_id() << endl;
    }

    void func(const string& s) { cout <<"void func(string& s),  thread id = " << std::this_thread::get_id() << endl; }
};

void updateWidget_implicit(const Widget& w)
{
    cout << "invoke updateWidget_implicit, thread id =" << std::this_thread::get_id() << endl;
}

void updateWidget_ref(Widget& w)
{
    cout << "invoke updateWidget_ref, thread id =" << std::this_thread::get_id() << endl;
}

void updateWidget_cref(const Widget& w)
{
    cout << "invoke updateWidget_cref, thread id =" << std::this_thread::get_id() << endl;
}

void test_ctor(const Widget& w) //注意這裏的w是按引用方式傳入(引用的是std::thread中保存的參數副本)
{
    cout << "thread begin...(id = " << std::this_thread::get_id() << ")" << endl;
    cout << "w.matableInt = " << ++w.mutableInt << endl;//注意,當std::thread按值傳參時,此處修改的是std::thread中
                                                        //保存的參數副本,而不是main中的w。
                                                        //而當向std::thread按std::ref傳參時,先會建立一個std::ref臨時對象,
                                                        //其中保存着main中w引用。而後這個std::ref再以副本的形式保存在
                                                        //std::thread中。隨後這個副本被move到線程函數,因爲std::ref重載了
                                                        //operator T&(),所以會隱式轉換爲Widget&類型(main中的w),所以起到
                                                        //的效果就好象main中的w直接被按引用傳遞到線程函數中來。

    cout << "thread end.(id = " << std::this_thread::get_id() << ")" << endl;
}

int main()
{
    //1. 向std::thread構造函數傳參
    cout << "main thread begin...(id = "<<std::this_thread::get_id()<<")"<< endl;
    Widget w;
    cout << "-----------test std::thread constructor----------------------- "<< endl;
    //1.1 std::thread默認的按值傳參方式:全部的實參都是被拷貝到std::thread對象的tuple中,即以副本形式被保存起來。
    std::thread t1(test_ctor, w); //注意,w是按值保存到std::thread中的,會調用其拷貝構造函數。
    t1.join();
    cout << "w.mutableInt = " << w.mutableInt << endl; //0,外部的w沒受影響。mutableInf仍爲0。

    cout << endl;

    //1.2 std::thread按引用傳參(std::ref) 
    std::thread t2(test_ctor, std::ref(w)); //注意,w是按引用傳入到std::thread中的,不會調用其拷貝構造函數。
    t2.join();
    cout << "w.mutableInt = " << w.mutableInt << endl; //1,因爲w按引用傳遞,mutableInf被修改成1。

    cout << "------------------test thread function------------------------ " << endl;
    //2. 向線程函數傳遞參數
    //2.1 線程函數的參數爲引用時
    //2.1.1 線程函數形參爲T&
    //std::thread t3(updateWidget_ref, w); //編譯失敗,由於std::thread內部是以右值形式向線程函數updateWidget_ref(Widget&)傳
                                           //參的,而右值沒法用來初始化Widget&引用。
    std::thread t3(updateWidget_ref, std::ref(w)); //ok,緣由相似test_ctor函數中的分析。即當線程函數的形參爲T&時,
                                                   //通常以std::ref形式傳入
    t3.join();
    //2.1.2 線程函數形參爲const T&
    std::thread t4(updateWidget_cref, w); //ok,但要注意w會先被拷貝構造一次,以副本形式保存在thread中。該副本再被以右值
                                          //形式傳遞給線程函數updateWidget_cref(const Widget&),而const T&可接受右值。
    t4.join();

    //2.2 隱式類型轉換及臨時對象
    const char* name = "Santa Claus";
    //注意:
    //(1)當向std::thread傳入類成員函數時,必須用&才能轉換爲函數指針類型
    //(2)類成員函數的第1個參數是隱含的this指針,這裏傳入&w。
    //(3)本例會發生隱式類型轉換,首先name在主線程中以const char*類型做爲副本被保存在thread中,當向線程函數
    //     Widget::func(const string&)傳參時,會先將以前的name副本隱式轉換爲string臨時對象再傳入,所以線程函數的形參中
    //     須要加const修飾。同時要注意,這個隱式轉換髮生在子線程調用時,即在子線程中建立這個臨時對象。這就須要確保主線
    //     程的生命週期長於子線程,不然name副本就會變成野指針,從而沒法正確構造出string對象。
    std::thread t5(&Widget::func, &w, name); //ok。
    t5.join();  //若是這裏改爲t5.detach,而且若是主線程生命期在這行結束時,就可能發生野指針現象。

    std::thread t6(&Widget::func, &w, string(name)); //爲了不上述的隱式轉換能夠帶來的bug。能夠在主線程先構造好這個
                                                     //string臨時對象,再傳入thread中。(如左)
    t6.join();

    //如下證實隱式轉換髮生在子線程中
    cout << endl;
    std::thread t7(updateWidget_implicit, 1); //會將1隱式轉換爲Widget,這個隱式轉換髮生在子線程。由於1會先以int型的副本
                                              //保存在t7中,當向線程函數傳參時,纔將int經過Widget的類型轉換構造轉成Widget。
    t7.join();

    cout << "main thread end.(id = " << std::this_thread::get_id() << ")" << endl;

    return 0;
}
/*輸出結果:
main thread begin...(id = 8944)
Widget(), thread id = 8944
-----------test std::thread constructor-----------------------
Widget(const Widget& w), thread id = 8944 //w被按值保存std::thread中。會調用拷貝構造函數
thread begin...(id = 17328)
w.matableInt = 1       //只是修改std::thread中w副本的值。
thread end.(id = 17328)
w.mutableInt = 0       //main中的w沒被修改

thread begin...(id = 5476)
w.matableInt = 1         //按std::ref傳遞既修改std::thread中w副本的值,也修改了main中w的值。
thread end.(id = 5476)
w.mutableInt = 1
------------------test thread function------------------------
invoke updateWidget_ref, thread id =17828
Widget(const Widget& w), thread id = 8944
invoke updateWidget_cref, thread id =2552
void func(string& s),  thread id = 11332
void func(string& s),  thread id = 17504

Widget(int i), thread id = 8996 //隱式轉換髮生在子線程8996中
invoke updateWidget_implicit, thread id =8996
main thread end.(id = 8944)
*/
相關文章
相關標籤/搜索