c++ 多线程编程std::thread, std::shared_mutex, std::unique_lock

发布时间 2023-03-31 11:29:08作者: _Explosion!

在C++11新标准中,可以简单通过使用thread库,来管理多线程,使用时需要#include <thread>头文件。

简单用例如下:

1 std::thread(Simple_func);
2 std::thread t(Simple_func);
3 t.detach();

第一行是直接启动一个新线程来执行Simple_func函数,而第二行先声明一个线程函数t(返回类型为thread),然后用detach方法启动线程。

C++11有两种方式来等待线程结束:

  • detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。前面代码所使用的就是这种方式。
    • 调用detach表示thread对象和其表示的线程完全分离;
    • 分离之后的线程是不在受约束和管制,会单独执行,直到执行完毕释放资源,可以看做是一个daemon线程;
    • 分离之后thread对象不再表示任何线程;
    • 分离之后joinable() == false,即使还在执行;
  • join方式,等待启动的线程完成,才会继续往下执行。假如前面的代码使用这种方式,其输出就会0,1,2,3,因为每次都是前一个线程输出完成了才会进行下一个循环,启动下一个新线程。
    • 只有处于活动状态线程才能调用join,可以通过joinable()函数检查;
    • joinable() == true表示当前线程是活动线程,才可以调用join函数;
    • 默认构造函数创建的对象是joinable() == false;
    • join只能被调用一次,之后joinable就会变为false,表示线程执行完毕;
    • 调用 ternimate()的线程必须是 joinable() == false;
    • 如果线程不调用join()函数,即使执行完毕也是一个活动线程,即joinable() == true,依然可以调用join()函数;

向线程传递参数

向线程调用的函数传递参数也是很简单的,只需要在构造thread的实例时,依次传入即可。例如

int Simple_func(int a, int b);
std::thread t(Simple_func,1,2);

需要注意的是,默认的会将传递的参数以拷贝的方式复制到线程空间,即使参数的类型是引用,如果在线程中使用引用来更新对象时,就需要注意了。默认的是将对象拷贝到线程空间,其引用的是拷贝的线程空间的对象,而不是初始希望改变的对象。例如:

int ChangeNum(int &a);
int num=0;
std::thread t(ChangeNum,num);
t.join();

在线程内,将对象的字段a和b设置为新的值,但是在线程调用结束后,这两个字段的值并不会改变。这样由于引用的实际上是局部变量num的一个拷贝。

若想通过线程改变对象值,需调用std::ref,将node的引用传入线程,如:

std::thread t(ChangeNum,std::ref(num));

thread是可移动的(movable)的,但不可复制(copyable)。可以通过move来改变线程的所有权,灵活的决定线程在什么时候join或者detach。

std::thread也可以去包装一个类,前提是该类对()操作符进行了重载,使其相当于拥有了函数的性质。(此处类似于std::bind的绑定)


 

在多线程编程的时候,资源竞争是很常见的问题,因此需要引入互斥锁。c++11中提供了std::mutex,而在C++17开始,标准库提供了shared_mutex类。

对于shared_mutex,可以理解为共享锁,允许多个线程同时对同一资源进行操作。而lock_guard、unique_lock可以理解为独占锁,只允许一个线程对资源进行操作。

在一些只读函数中可以用std::shared_mutex,而在写操作函数中需用std::unique_lock。

std::shared_mutex是c++17中引入的,不支持std::mutex,需用std::shared_mutex声明互斥信号量。

 

参考文章:https://immortalqx.github.io/2021/12/04/cpp-notes-3/