菜鸟笔记
提升您的技术认知

std::thread线程类及传参问题-ag真人游戏

一. 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 
    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 
    _nodiscard static constexpr auto _get_invoke(
        index_sequence<_indices...>) noexcept { // select specialization of _invoke to use
        return &_invoke<_tuple, _indices...>;   //这里返回特化的_invoke函数指针
    }
public:
    template , thread>>>
    explicit thread(_fn&& _fx, _args&& ... _ax) { // construct with _fx(_ax...)
        using _tuple                 = tuple, 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(_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)线程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 
#include 
#include   //for std::chrono::seconds
#include    //for std::time_t
#include  //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: " <

二. 传递参数的方式

(一)传参中的陷阱:

  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 
#include 
#include 
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 = "<
网站地图