1.线程基础

线程发起

线程发起意味着启动一个线程,C++11在引入thread库后,定义一个线程的回调函数之后,创建线程变量,例如:std::thread t1(thread_worker,hellostr);在创建完线程对象之后,就意味着启动该线程

线程对象传入参数,第一个为回调函数的地址,在C++编译环境中,会对普通函数进行优化,普通函数的函数名就是函数的实际的地址

线程等待

线程等待的目的就是为了确保线程执行结束时能够全部回收线程资源

线程等待的方式:挂起,后台运行

thread.join();
thread.deatch();

如果没有进行线程等待

  1. 主线程创建完线程对象后,就算线程完成了自身的所有任务,如果最后并没有进行线程资源的回收:thread.join()或者thread.deatch()则一定会触发terminate的异常
  2. 如果主线程在子线程运行时提前退出,并没有回收子线程,也会触发terminate的异常

terminate触发情形

  1. 当线程对象被销毁(即超出作用域时),未显示的调用joindeatch时,则会导致std::terminate的调用,该函数会触发assert断言,使得程序异常终止
  2. 当尝试线程之间的复制时(比如通过错误的指针操作)尝试复制或移动一个线程对象,会导致未定义行为,触发std::terminate

join

join方法就是让主线程去等待对应子线程结束阻塞在那里,当子线程任务完成后,join会清理与线程相关的所有资源,包括线程的堆栈和其他系统资源。此时,std::thread对象的状态变为不可连接,并且该对象可以被销毁而不会导致未定义行为。

deatch

  1. 当调用deatch后,该子线程会从原有的std::thread对象中分离,并允许该线程在后台继续执行。而管理该线程的std::thread对象将不再管理该线程的生命周期
  2. 当调用deatch后,将不能够再调用join,否则会触发std::terminate
  3. 分离的线程会独立于创建它的线程继续执行。即使创建它的线程结束,分离的线程仍然会继续运行,直到它完成其任务(主线程退出不会影响到他
  4. 在调用 detach() 之后,std::thread 对象的状态将变为“可分离”,且不能再用于任何其他线程操作。你可以使用 std::thread::joinable() 方法检查一个线程是否可以被加入(即是否仍然是可连接的)

仿函数作为线程参数

任何定义了函数调用运算符的对象都是函数对象,也就是仿函数

class background_task{
public:
    void operator()(){
        std::cout << "background_task\n";
    }
    static void show(){
     std::cout << "background_task\n";
    }
};
    background_task task;
    std::thread t1{background_task()};//竟然涉及到了两次构造函数的调用,三次析构
    t1.join();
    std::thread t2(background_task::show);

对于仿函数对象作为线程任务,如果直接传入编译器会把线程对象认为是返回值是thread,参数是一个函数指针的函数指针

解决方案就是使用C++11的{}统一初始化,或者再套一层括号

lambda表达式作为函数参数

std::thread t4(
[](std::string str){
    cout << str << endl;    
},
helloStr
);
t4.join();

detach的隐患

当线程使用detach进行线程分离时,如果使用了局部变量的引用,当局部变量被销毁时,会发生未定义的行为

struct func{
    int& _i;
    func(int i ) : _i(i){}
    void operator()(){
        for(int i = 0 ; i < 4 ; i ++)
        {
            _i = i;
        }
    }
};
void opp(){
    int x = 0;
    func f(x);
    std::thread funcThread(f);
    f.deatch();
}

异常

void catch_exception() {
    int some_local_state = 0;
    func myfunc(some_local_state);
    std::thread  functhread{ myfunc };
    try {
        //本线程做一些事情,可能引发崩溃
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }catch (std::exception& e) {
        functhread.join();
        throw;
    }
    functhread.join();
}

如果主线程抛出异常,子线程需要执行完才能退出,如果主线程在子线程完成之前崩溃,整个程序的进程会被操作系统终止,子线程也会随之终止,而不会执行到join

线程的RAII

class thread_guard {
private:
    std::thread& _t;
public:
    explicit thread_guard(std::thread& t):_t(t){}
    ~thread_guard() {
        //join只能调用一次
        if (_t.joinable()) {
            _t.join();
        }
    }
    thread_guard(thread_guard const&) = delete;
    thread_guard& operator=(thread_guard const&) = delete;
};

void auto_guard() {
    int some_local_state = 0;
    func my_func(some_local_state);
    std::thread  t(my_func);
    thread_guard g(t);
    //本线程做一些事情
    std::cout << "auto guard finished " << std::endl;
}

线程慎用隐式类型转换

void print_str(int i , std::string s){
    cout << i << " " << s << "\n";
}
void danger_oops(int val){
    char buffer[1024];
    sprintf(buffer,"%i",val);
    std::thread t(print_str,3,buffer);
    t.detach();
}

char* 的buffer 确实能够隐式转变为string,但是线程参数的传递是有一个中间过程的,也就是传入的参数会保存到thread中封装的参数里面,只有真正线程调用的时候才会完成参数类型转换。所以会存在一个问题,就是当主线程结束太快,并且子线程采用分离的方式(deatch)运行的话,子线程可能会访问到销毁的局部变量。所以为了避免这个问题,可以显示的将buffer转变为string来传入,即:

std::thread t(print_str,3,string(buffer));,因为thread的构造函数会把所有的参数转变为右值保存起来

左值和右值

左值和右值是变量值类别的概念

左值

左值是可以出现在赋值语句左侧的表达式,表示一个持久的对象,可以被引用并且由一个明确的内存地址

  • 可以取地址(使用&运算符)。
  • 可以被修改(如果不是常量)。
  • 通常是变量、对象、数组元素等

右值(Rvalue)

右值是指不能出现在赋值语句左侧的表达式。它通常表示一个临时对象或一个值,没有持久的存储位置

  • 不能取地址(取地址会导致编译错误)。
  • 通常是字面量、临时对象、返回值

thread原理

thread构造函数

template <class _Fn, class... _Args, enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>, int> = 0>
_NODISCARD_CTOR_THREAD explicit thread(_Fn&& _Fx, _Args&&... _Ax) {
    _Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
}
template <class _Fn, class... _Args>
void _Start(_Fn&& _Fx, _Args&&... _Ax) {
    using _Tuple                 = tuple<decay_t<_Fn>, decay_t<_Args>...>;
    auto _Decay_copied           = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
    constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{});

    _Thr._Hnd =
        reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id));

    if (_Thr._Hnd) { // ownership transferred to the thread
        (void) _Decay_copied.release();
    } else { // failed to start thread
        _Thr._Id = 0;
        _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);
    }
}

在start函数中,_Tuple存储的是单纯的值,是通过decay_t将左值或者右值都转变为值的副本,也就是单纯的值,那么这些单纯的值,_Decay_copied存储的是值的拷贝,即内部的成员=std::forward<_Args>(_Ax)…;

在 C++ 中,std::decay_t 对于 std::string 类型的效果主要是去除引用、数组和常量/易变性修饰符,也就是衰减类型。

但是转变为右值的过程主要是_Get_invoke这个用于返回可调用对象的API,这个可调用对象的参数包括函数对象,以及外部传进的所有的参数,其内部是:

static unsigned int __stdcall _Invoke(void* _RawVals) noexcept /* terminates */ {
    // adapt invoke of user's callable object to _beginthreadex's thread procedure
    const unique_ptr<_Tuple> _FnVals(static_cast<_Tuple*>(_RawVals));
    _Tuple& _Tup = *_FnVals.get(); // avoid ADL, handle incomplete types
    _STD invoke(_STD move(_STD get<_Indices>(_Tup))...);
    _Cnd_do_broadcast_at_thread_exit(); // TRANSITION, ABI
    return 0;
}

将所有参数转变为右值来存储

绑定引用

引用折叠凡是折叠中出现左值引用(比如函数参数类型就是左值引用),优先折叠为左值引用;

原样转发:将参数的值类别(左值或右值)完美地转发到另一个函数中,而不丢失原有的特性

值类别:左值,可以取地址的对象,通常是变量;右值,临时对象,无法取地址,通常是字面量或表达式的结果;将亡值,可被移动的对象,表示一个即将销毁的对象

原样转发的实现

template<typename T>
void process(T&& arg){
    forwardToOtherFunction(std::forward<T>(arg));
}
void forwardToOtherFunction(int& x)
{
    std::cout << "Left value: " << x << std::endl;
}
void forwardToOtherFunction(int&& x) {
    std::cout << "Right value: " << x << std::endl;
}
int main() {
    int a = 10;
    process(a); // 传递左值
    process(20); // 传递右值
    return 0;
}
template<typename T>
void process(T&& arg){
    forwardToOtherFunction(std::forward<T>(arg));
}

这是一个万能引用,可以绑定到左值和右值

原样转发的目的

  1. 保留值类别

    原样转发允许我们在函数模板中保留参数的原始值类别,如果传入的是左值,转发时仍然保持左值;如果传入的是右值,则转发时保持右值

  2. 避免不必要的复制

    如果我们不使用原样转发,而是简单地传递参数,可能会导致不必要的复制。例如,如果我们传递一个右值给一个接受参数的函数,如果不使用原样转发,可能会将其复制而不是移动,这样就失去了使用右值的性能优势

绑定引用需要将参数转变为引用对象

如果你尝试直接将一个引用参数传递给 std::thread 的构造函数,而没有使用 std::ref,可能会导致编译错误。原因是 std::thread 的构造函数期望的是可以复制的参数,而引用本身不能被复制。

在C++中,使用 std::thread 创建线程时,如果你想将一个引用类型的参数传递给线程函数,不能直接传递引用,因为 std::thread 会将参数复制到新线程中。为了能够传递引用参数,我们需要使用 std::refstd::cref,它们会将引用封装成一个可复制的对象,从而在新线程中保持对原始对象的引用。

std::ref 是一个函数模板,用于创建一个 std::reference_wrapper 对象,这个对象可以被安全地复制,同时保持对原始对象的引用。它的主要作用是将一个引用包装起来,以便在需要复制的上下文中(如线程参数传递)仍然能够引用原始对象。

std::refstd::cref 的实现原理是通过一个轻量级的封装类 std::reference_wrapper 来实现的。std::reference_wrapper 内部持有一个指向原始对象的指针,并提供一个 operator T&(),使得它可以像引用一样使用。

std::move

传递给线程的参数是独占的(不支持拷贝,赋值构造),可以通过std::move来移交所有权

void test(unique_ptr<int> p)
{
    cout << *p << endl;
}

int main() {
    std::unique_ptr<int> p = std::make_unique<int>(100);
    std::thread t(test,std::move(p));
    t.join();
    return 0;
}

使用类函数

class t{
public:
    t(){}
    void show(int i){cout << i;}

};

int main() {
    t a;
    std::thread s(&t::show,&a,10);
    s.join();
    return 0;
}

使用类的成员函数,需要使用取地址符&加上类名::函数,然后需要传入对象的地址,之后是函数参数