【轉】std::thread線程類及傳參問題


一、std::thread類

(一)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;
};

  1、構造std::thread對象時:如果不帶參則會創建一個空的thread對象,但底層線程並沒有真正被創建,一般可將其它std::thread對象通過move移入其中;如果帶參則會創建新線程,而且會被立即運行

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

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

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

  (2)不可聯結:

    ①當不帶參構造的std::thread對象為不可聯結,因為底層線程還沒創建。

    ②已移動的std::thread對象為不可聯結。

    ③已調用join或detach的對象為不可聯結狀態。因為調用join()以后,底層線程已結束,而detach()會把std::thread對象和對應的底層線程之間的連接斷開。

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

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

(二)線程的基本用法

  1、獲取當前信息

1 // t為std::thread對象
2 t.get_id();                          // 獲取線程ID
3 t.native_handle();                   // 返回與操作系統相關的線程句柄
4 std::thread::hardware_concurrency(); // 獲取CPU核數,失敗時返回0

  2、線程等待和分離

1 join();     //等待子線程,調用線程處於阻塞模式
2 detach();    //分離子線程,與當前線程的連接被斷開,子線程成為后台線程,被C++運行時庫接管。
3 joinable();    //檢查線程是否可被聯結

  3、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)
*/

 轉自《std::thread線程類及傳參問題


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM