• C++多线程--std::thread


    0 引言

    在C++多线程专栏中,我讲解了std::async,std::future,std::promise以及内存序相关概念,基本上涵盖了C++事件处理相关的基本构建块。

    为了保证对C++多线程中相关线程概念的全部介绍,后期会增加相应的std::thread, std::mutex, std::lock_guard, std::unique_lock等相关介绍。

    而本文便是第一部分,std::thread的相关介绍和使用讲解。

    1 std::thread

    一个std::thread代表一个可执行单元。简单来说

    • std::thread是可执行单元的容器(可执行单元可理解成线程,std::thread拥有线程的所有权)
    • std::thread是不可拷贝和不可赋值的,但是可移动
    • std::thread所拥有的可执行单元的工作package为可调用单元(包括函数对象,lambda函数,成员函数,普通函数等)
    • 可调用单元的返回值会被忽略

    下面是四种创建线程的方式

    1. #include
    2. #include
    3. class A {
    4. public:
    5. void test_thread() {
    6. std::cout << "A member function thread\n";
    7. }
    8. };
    9. void test_thread() {
    10. std::cout << "function thread\n";
    11. }
    12. class background_task {
    13. public:
    14. void operator()() const {
    15. std::cout << "functor thread\n";
    16. }
    17. };
    18. int main() {
    19. // 1.通过类的成员函数创建线程
    20. A a;
    21. std::thread t1(&A::test_thread, a);
    22. // 2. 通过正常函数创建线程
    23. std::thread t2(test_thread);
    24. // 3. 通过函数对象创建线程
    25. std::thread t3(background_task());
    26. // 4. 通过lambda表达式创建线程
    27. std::thread t4([]() { std::cout << "lambda thread\n";});
    28. t1.join();
    29. t2.join();
    30. t3.join();
    31. t4.join();
    32. return 0;
    33. }

    若你构建上述程序会出现如下错误

    qls@qls-VirtualBox:~/cpp_learn$ g++ -o main -c a.cc -std=c++17 -lpthread
    a.cc: In member function ‘void background_task::operator()() const’:
    a.cc:19:3: error: expected ‘;’ before ‘}’ token
       19 |   }
          |   ^
    a.cc: In function ‘int main()’:
    a.cc:29:17: warning: parentheses were disambiguated as a function declaration [-Wvexing-parse]
       29 |   std::thread t3(background_task());
          |                 ^~~~~~~~~~~~~~~~~~~
    a.cc:29:17: note: replace parentheses with braces to declare a variable
       29 |   std::thread t3(background_task());
          |                 ^~~~~~~~~~~~~~~~~~~
          |                 -
          |                 {                 -
          |                                   }
    a.cc:34:6: error: request for member ‘join’ in ‘t3’, which is of non-class type ‘std::thread(background_task (*)())’
       34 |   t3.join();
          |      ^~~~

    上述便是C++中最C++’s most vexing parse相关错误,也即将声明都解释成函数声明。因此会报上述错误。

    解决方案便为 将t3改成如下方式

    1. // 3. 通过函数对象创建线程
    2. std::thread t3{background_task()};

    2 std::thread生命周期

    一个std::thread生命周期随着其可调用单元的结束而终结。为了控制生命周期,你可以选择

    1. join() // 阻塞直到子线程退出
    2. detach() // 分离子线程

    join()的使用场景如下:

    • 若某一个线程的结果依赖其他线程,则使用join

    detach()使用场景如下:

    • 若某一个线程作为后台长期运行的服务,则可以使用detach

    判断一个std::thread是否可join或者detach,则需要了解joinable相关的概念。

    joinable: 

    • 一个线程拥有一个可执行单元,则该线程是可joinable的

    使用join和detach会碰到如下两个异常

    • 执行std::terminate
    • 抛出std::system_error异常

    当一个可joinable线程即没调用join也没调用detach时,其析构函数被调用的时候,C++会调用std::terminate

    当一个可joinable线程调用起join或者detach接口超过一次,则会抛出std::system_error异常

    关于上述代码示例,本文就不再给出,感兴趣可以自己实验。

    3 线程参数

    std::thread本质上是一个可变模版。因此可以向其传递任何数量的参数(可参考​​​std::thread - cppreference.com

    其构造函数如下形式

    1. template< class Function, class... Args >
    2. explicit thread( Function&& f, Args&&... args );

    NOTE:

    默认场景下,std::thread的参数是拷贝到其内部存储中,在那里他们会被可执行线程访问,然后将其作为右值(像临时对象)传递到可执行单元。

    下面这个例子来自《C++ Concurrency in Action 2th》。

    1. void f(int i,std::string const& s);
    2. std::thread t(f,3,”hello”);

    上述新创建的线程t的内部存储的是const char*变量,在可执行线程内部将该变量转换为std::string,并传递给可调用单元f。

    由于std::thread仅仅通过拷贝相应的参数,因此在使用std::thread传递参数时,需要额外注意相应参数生命周期的问题,防止服务出现bug。

    譬如《C++ Concurrency in Action 2th》给出如下几种场景

    • std::thread的传入参数包含有局部变量的地址

    如下程序所示(我进行了相应的补全)

    1. #include
    2. #include
    3. void f(int i,std::string const& s) {
    4. std::cout << s << i << "\n";
    5. }
    6. void oops(int some_param) {
    7. char buffer[1024];
    8. sprintf(buffer, "%i",some_param);
    9. std::thread t(f,3,buffer);
    10. t.detach();
    11. }
    12. int main() {
    13. oops(10);
    14. return 0;
    15. }
    • std::thread传入的参数中,包含有不可拷贝的对象,譬如std::unique_ptr对象
    • std::thread的可调用单元想要修改相应的传入参数,此时需要使用std::ref辅助类

    4 转换可执行线程所有权

    同std::unique_ptr类似,std::thread拥有对可执行线程的所有权。

    转换std::thread的可执行线程所有权至少有如下两种使用场景

    • 通过函数创建std::thread,但将该std::thread返回给调用函数
    • 将std::thread传递到某个函数中,并在该函数中等待其完成

    下面这段代码来自《C++ Concurrency In Action》2th。

    1. void some_function();
    2. void some_other_function();
    3. std::thread t1(some_function);
    4. std::thread t2=std::move(t1);
    5. t1=std::thread(some_other_function);
    6. std::thread t3;
    7. t3=std::move(t2);
    8. t1=std::move(t3);

    上述主要工作如下

    • 线程t2拥有t1的可执行线程所有权(线程t1已经通过std::move转移其资源所有权)
    • 此时线程t1变成不可joinable
    • t1 = std::thread(some_other_function); 使得线程t1重新拥有可执行线程的所有权
    • t3获得t2所管理资源的所有权
    • 由于t1已经拥有相应的可执行线程所有权,因此t1=std::move(t3)报错

    5 std::thread接口一览

    此处接口来自std::thread – cppreference.com

    1. t.join() // 等待线程t的可执行单元的结束
    2. t.detach() // 分离线程t,使其作为后台线程运行
    3. t.joinable() // 如果线程t是可joinable,该接口返回true
    4. t.get_id()
    5. std::this_thread::get_id() // 这两个接口返回线程标识
    6. std::thread::hardware_concurrency() // 表示一个机器最多能启动多少线程
    7. std::this_thread::sleep_until(absTime) // 让线程 t 进入休眠状态,直到时间点 absTime。
    8. std::this_thread::sleep_for(relTime) // 使线程 t 在relTime 时间间隔内休眠。
    9. std::this_thread::yield() // 使系统去调度另一个线程去执行
    10. t.swap(t2)
    11. std::swap(t1, t2) // 这两个接口交换std::thread底层的可执行线程

    6 总结

    通过本文,可以初步了解并使用std::thread相关工具类。

  • 相关阅读:
    C++ //练习 10.37 给定一个包含10个元素的vector,将位置3到7之间的元素按逆序拷贝到一个list中。
    nodeJs 实现视频的转换(超详细教程)
    十七、文件(2)
    Spring Boot项目开发实战:使用STS快速构建一个生产级项目
    【教学类-12-08】20221111《连连看竖版6*6 (3套题目空心图案(中班教学)》(中班主题《》)
    13 mysql date/time/datetime/year 的数据存储
    编写一个程序,实现以下功能:(1)计算n个学生的平均成绩aver;(C语言)
    计算机专业毕业论文java毕业设计网站SSH人事管理系统|人力请假考勤工资人事奖惩[包运行成功]
    如何加速JavaScript 代码运行速度
    【计算机考研】计算机行业考研还有性价比吗?
  • 原文地址:https://blog.csdn.net/qls315/article/details/126332563