飞道的博客

C++(标准库):45---并发之(底层接口thread()、promise、packaged_task)

433人阅读  评论(0)
  • 除了前一篇文章介绍的高级接口async()和(shared)future,C++标准库还提供了一个启动及处理线程的底层接口

一、thread

thread概述

  • thread可以用来启动一个线程,其参数也接受一个callable object(函数、成员函数、函数对象、lambda)
  • callable object的传参方式与async()一样,并且也有传值调用和传引用调用的方式,详情可以参阅前一篇async()的文章:https://blog.csdn.net/qq_41453285/article/details/105486757
  • 例如:

   
  1. std:: thread t(doSomething);
  2. //...
  3. t.join(); //等待线程的结束

thread与async()的区别

  • 相比于async(),thread()不提供下面的性质:
    • ①thread没有所谓的发射策略。C++标准库永远试着将目标函数启动于一个新的线程中。如果无法做到会抛出std::system_error并带有差错码resource_unavailable_try_agin
    • ②没有接口可以处理线程结果。唯一可获得的是独一无二的线程ID
    • ③如果发生异常,但为被捕捉于线程之内,程序会立刻终止并调用std::terminate()。如果想要将异常传播到线程外的某个context,必须使用exception_ptr
    • ④你必须声明是否“想要等待线程结束(调用join())”或打算“将它分离,使其运行于后台而不受任何控制(调用detach())”。如果你在thread object生命周期前不这么做,或如果它发生了一次move assignment,程序会终止并调用std::terminate()
    • ⑤如果你让线程运行于后台而main()结束了,所有线程会被鲁莽而硬性地终止

二、thread演示案例


  
  1. #include <iostream>
  2. #include <thread>
  3. #include <future>
  4. #include <random>
  5. #include <chrono>
  6. #include <exception>
  7. using namespace std;
  8. void doSomething(int num, char c);
  9. int main()
  10. {
  11. try {
  12. //开启一个线程(不分离)
  13. std:: thread t1(doSomething, 5, '.');
  14. std:: cout << "- started fg thread " << t1.get_id() << std:: endl;
  15. //开启5个线程(分离)
  16. for ( int i = 0; i < 5; ++i)
  17. {
  18. std:: thread t(doSomething, 10, 'a' + i);
  19. std:: cout << "-detach started bg thread " << t.get_id() << std:: endl;
  20. t.detach();
  21. }
  22. //等待输入
  23. cin.get();
  24. //等待t1线程结束
  25. std:: cout << "- join fg thread " << t1.get_id() << std:: endl;
  26. t1.join();
  27. }
  28. catch ( const exception& e) {
  29. std:: cerr << "EXCEPTION: " << e.what() << std:: endl;
  30. }
  31. }
  32. void doSomething(int num, char c)
  33. {
  34. try {
  35. std:: default_random_engine dre(42 * c);
  36. std::uniform_int_distribution< int> id( 10, 1000);
  37. for ( int i = 0; i < num; ++i)
  38. {
  39. this_thread::sleep_for( std::chrono::milliseconds(id(dre)));
  40. std:: cout.put(c).flush();
  41. }
  42. }
  43. catch ( const exception& e) { //处理exception异常
  44. std:: cerr << "THREAD-EXCEPTION (thread " << this_thread::get_id() << "):" << e.what() << std:: endl;
  45. }
  46. catch (...) { //捕获其他所有异常
  47. std:: cerr << "THREAD-EXCEPTION (thread " << this_thread::get_id() << ")" << std:: endl;
  48. }
  49. }
  • 我们在打印了“acd”之后按下回车,结果如下所示

三、线程的分离(thread.detach())

  • detached thread(卸载/分离后的线程)很容易造成问题——线程分离之后就不再收到主程序的控制,因此你就无法判断其是否还在运行。因此如果detached thread访问非局部资源的话,或者以reference方式使用变量/object,要确保让detached thread运行的时候资源的生命周期没有结束
  • 如果当程序退出之后,detached thread可能还在运行,如果其仍然在访问“已被销毁”或“正在析构”的global/static object,这会导致不可预期的后果
  • 因此,对于detached thread,其建议使用规则如下:
    • detached thread应该访问局部对象的拷贝
    • 如果detached thread中使用了global/static object,你应该:
      • 确保这些global/static object在“对它们进行操作”的所有detached thread都结束(或都不再访问它们)之前不被销毁。一种做法是使用condition variable(条件变量),它让detached thread用来发信号说它们已结束。离开main()或调用exit()之前你必须先妥善设置这些condition variable,然后发信号说可进行析构了
      • 以调用quick_exit()的方式结束程序。这个函数之所以存在完全是为了以“不调用global和static object析构函数”的方式结束程序
  • 由于std::cin、std::cout、std::cerr及其global stream object按标准来说是“在程序运行期间不会被销毁”,所以detached thread访问这些object应该不会导致不可预期的行为。然而,其他问题例如“interleaved character”却有可能发生
  • 记住一个经验法则:终止detached thread的唯一安全方法就是搭配“...at_thread_exit()”函数群中的某一个。这会“强制main thread等待detached thread真正结束”。或者你也可以选择忽略这一性质而相信某位评论家所言:“Detached thread应该被移到'危险性质'的篇章中,几乎没有人需要它”

四、线程ID

  • this_thread::get_id():你可以根据this_thread命名空间来获取当前线程的ID,不需要通过线程对象获取

  
  1. void doSomething();
  2. int main()
  3. {
  4. std:: thread t(doSomething);
  5. }
  6. void doSomething()
  7. {
  8. //打印线程ID
  9. std:: cout << "thread id: " << this_thread::get_id() << std:: endl;
  10. }
  • object.get_id():根据一个线程对象,获取其线程id

  
  1. void doSomething();
  2. int main()
  3. {
  4. std:: thread t(doSomething);
  5. //打印线程ID
  6. std:: cout << "t thread id: " << t.get_id() << std:: endl;
  7. }

std::thread::id数据类型

  • 线程ID的数据类型用std::thread::id表示,线程ID独一无二,其是一个类类型

   
  1. int main()
  2. {
  3. std:: thread t(doSomething);
  4. //保存线程ID
  5. std::thread::id tThreadId = t.get_id();
  6. //打印ID
  7. std:: cout << "t thread id: " << tThreadId << std:: endl;
  8. }
  • std::thread::id有个默认构造函数,会产生一个独一无二的ID用来表现“no thread”

   
  1. void doSomething();
  2. int main()
  3. {
  4. std:: thread t(doSomething);
  5. std:: cout << "ID of \"no thread\":" << std::thread::id() << std:: endl;
  6. }

  • 线程ID的一些特性:
    • 线程ID支持的操作只有“比较”以及调用output操作符输出至某个stream。其他的操作不支持
    • 事实上,线程ID不是在thread启动时就生成的,而是在被使用时才生成的
  • 演示案例:

  
  1. void doSomething();
  2. std::thread::id masterThreadID;
  3. int main()
  4. {
  5. std:: thread master(doSomething);
  6. masterThreadID = master.get_id();
  7. }
  8. void doSomething()
  9. {
  10. if (this_thread::get_id() == masterThreadID)
  11. {
  12. //...
  13. }
  14. }

五、std::promise<>

  • 设计promise<>的目的:
    • 在async()中,我们可以将async()的结果(正确的返回值/或异常)保存在一个future<>中,然后使用future<>.get()去获取,但是在thread中我们如何获取线程中可能产生的数据或者是异常呢?
    • 标准库设计了一个promise<>,它是future<>的配对兄弟,二者配合使用,可以保存一个shared shate(用来保存结果或异常)

演示案例


   
  1. #include <iostream>
  2. #include <thread>
  3. #include <future>
  4. #include <string>
  5. #include <exception>
  6. using namespace std;
  7. void doSomething(std::promise<std::string>& p);
  8. int main()
  9. {
  10. try {
  11. std::promise< std:: string> p;
  12. //将p以引用的方式传入doSomething中
  13. std:: thread t(doSomething, std::ref(p));
  14. t.detach();
  15. //如果p有结果返回,将结果保存到f中
  16. std::future< std:: string> f(p.get_future());
  17. //调用.get()获取返回的结果
  18. std:: cout << "result: " << f.get() << std:: endl;
  19. }
  20. catch ( const exception& e) {
  21. std:: cerr << "EXCEPTION: " << e.what() << std:: endl;
  22. }
  23. catch (...) {
  24. std:: cerr << "EXCEPTION" << std:: endl;
  25. }
  26. }
  27. void doSomething(std::promise<std::string>& p)
  28. {
  29. try {
  30. std:: cout << "read char('x' for exceptipn):";
  31. char c = std:: cin.get();
  32. if (c == 'x') {
  33. throw std::runtime_error( std:: string( "char ") + c + " read");
  34. }
  35. std:: string s = std:: string( "char ") + c + " processed";
  36. //将字符串保存到p中返回
  37. p.set_value( std::move(s));
  38. }
  39. catch (...) {
  40. //将异常保存到p中返回
  41. p.set_exception( std::current_exception());
  42. }
  43. }
  • 我们以引用的方式将promise<>传入到doSomething()函数中:
    • 如果有正确的数据,那么我们调用promise<>.set_value()将结果保存到promise<>中
    • 如果有异常,我们调用promise<>.set_exception()将异常保存到promise<>中。在此演示案例中我们将定义于<exception>内的辅助函数std::current_exception()传递给该函数,std::current_exception()会把当前异常以类型std::exception_ptr生成出来,如果没有异常,那么std::current_exception()生成nullptr
  • 与future<>的交互过程:
    • 我们可以将promise<>变量绑定到future<>上
    • 一旦promise<>调用set_...相关函数设置某个值或异常,那么shared state就存在该值或异常,此时shared state状态变为ready,于是future<>对象就可以调用get()获取shared state中promise<>返回的值或异常
    • future<>.get()会阻塞,直到shared state变为ready——有值或者异常了
  • 下面两张图是输入a和x的结果:

  • 如果想要shared state在线程结束时变成ready,以确保线程的局部对象以及其他资源在“结果被处理之前”清除,那么应该调用set_value_at_thread_exit()或set_exception_at_thread_exit()。例如:

  
  1. void doSomething(std::promise<std::string>& p)
  2. {
  3. try {
  4. //..同上
  5. p.set_value_at_thread_exit( std::move(s));
  6. }
  7. catch (...) {
  8. p.set_exception_at_thread_exit( std::current_exception());
  9. }
  10. }
  • 相关注意事项:
    • promise和future并不仅限于多线程中,在单线程中我们也可以使用promise持有一个结果值或一个异常,然后通过一个future进行处理
    • 我们不能够既存储值又存储异常。这么做会导致std::future_error并夹带差错码std::future_errc::promise_already_staisfied

六、std::packaged_task<>

  • async()接口允许你处理一个任务(task)的结果,该task会自动运行于后台
  • 然而有时候处理一个task,不需要立刻启动该task

演示案例

  • 例如,thread poll(线程池)可控制何时运行以及多个后台task同时运行
  • 我们不应该像下面这样写:

   
  1. double compute(int x, int y);
  2. int main()
  3. {
  4. std::future< double> f = std::async(compute, 7, 5);
  5. double res = f.get();
  6. }
  • 而应该使用packaged_task<>:

   
  1. double compute(int x, int y);
  2. int main()
  3. {
  4. //创建一个task
  5. std::packaged_task< double( int, int)> task(compute);
  6. std::future< double> f = task.get_future();
  7. //...执行其他事情
  8. //此时才执行该task
  9. task( 7, 5);
  10. //...执行其他事情
  11. double res = f.get();
  12. }

 


转载:https://blog.csdn.net/qq_41453285/article/details/105569549
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场