《C++并发编程实战》读书笔记(1):线程管控

打印 上一主题 下一主题

主题 930|帖子 930|积分 2790

1、线程的基本管控

包含头文件后,通过构建std::thread对象启动线程,任何可调用类型都适用于std::thread。
  1. void do_some_work();
  2. struct BackgroundTask
  3. {
  4.     void operator()() const;
  5. };
  6. //空的thread对象,不接管任何线程函数
  7. std::thread t1;
  8. //传入普通函数
  9. std::thread t2(do_some_work);
  10. //传入lambda函数
  11. std::thread t3([]() { /*do something*/ });
  12. //传入可调用对象
  13. BackgroundTask task;
  14. std::thread t4(task);
  15. //不能使用std::thread t4(BackgroundTask()),虽然本意是传入临时变量,但这会被编译器解释成函数声明。多用一对圆括号或者使用列表初始化可以解决这个问题。
  16. std::thread t5((BackgroundTask()));
  17. std::thread t6{BackgroundTask()};
复制代码
启动线程后,需要明确是等待它结束、还是任由它独自运行:

  • 调用成员函数join()会先等待线程结束,然后隶属于该线程的任何存储空间都会被清除,std::thread对象不再关联到已结束的线程。
  • 调用成员函数detach()会分离线程使其在后台运行,此后无法获得与它关联的std::thread对象。分离线程的归属权和控制权都转移给了C++运行时库,线程退出时与之关联的资源会被正确回收。
调用了join()或是detach()之后,其joinable()方法将返回false,所以也就不能再调用join()。不能对空的std::thread对象调用join()或是detach()。如果线程启动后既不调用join()也不调用detach(),那么当std::thread对象销毁时,其析构函数将调用std::terminate()终止整个程序。
2、向线程函数传递参数

若需向线程上的函数传递参数,直接向std::thread的构造函数添加更多参数即可。线程具有内部存储空间,参数会按照默认方式先复制到该处,然后这些副本被当作临时变量,以右值形式传递给线程上的函数。即使函数的相关参数按设想应该是引用,上述过程依然会发生。
  1. void f(int i, const std::string& s);
  2. std::thread t(f, 3, "hello");
复制代码
上述代码在新线程上调用f(3, "hello"),尽管f()的第二个参数是std::string类型,但字符串内容仍然以指针const char*的形式传入,直到进入新线程的上下文环境后才转换为std::string类型。所以如果参数是指针,需要特别注意其生命周期,否则可能导致严重问题,例如:
  1. void f(int i, const std::string& s);
  2. void oops(int param)
  3. {
  4.     char buffer[1024];
  5.     sprintf(buffer, "%d", param);
  6.     std::thread t(f, 3, buffer);
  7.     t.detach();
  8. }
复制代码
buffer是指向局部数组的指针,我们原本设想buffer会在新线程内转换成std::string对象,但在此完成之前,oops()函数很有可能已经退出,导致局部数组被销毁从而引发未定义的行为。这一问题的根源在于:std::thread的构造函数原样复制所提供的值,并未立即将其转换成预期的参数类型,等到转换发生时,指针可能已经失效。所以解决方法就是,在buffer传入std::thread的构造函数之前,就先把它转换成std::string对象:
  1. std::thread t(f, 3, std::string(buffer));
复制代码
除了指针外,传递引用也需要小心。例如我们想要的是非const引用:
  1. void update_widget_data(WidgetData& data);
  2. void oops()
  3. {
  4.     WidgetData data;
  5.     std::thread t(update_widget_data, data);
  6.     t.join();
  7. }
复制代码
根据update_widget_data函数的声明,参数需要以引用的方式传入,但std::thread的构造函数对此却毫不知情,它忽略了函数所期望的参数类型,直接复制了我们提供的值。然而,线程库的内部代码会把参数的副本(std::thread构造时由对象data复制得出,位于新线程的内部存储空间)以右值的形式传递给update_widget_data函数,所以这段代码会编译失败,因为不能向非const引用传递右值。解决方法就是使用std::ref()函数加以包装,这样传递给update_widget_data函数的就是指向data的引用,代码就能编译成功:
  1. std::thread t(update_widget_data, std::ref(data));
复制代码
要将某个类的成员函数设为线程函数,我们需要给出合适的对象指针作为第一个参数,成员函数的参数放在其后的位置。
  1. class X
  2. {
  3. public:
  4.     void do_lengthy_work();
  5. }
  6. X my_x;
  7. std:thread t(&X::do_lengthy_work, &my_x);
复制代码
对于只能移动、不能复制的对象,传递参数时需要使用std::move()来转移归属权。在下面的例子中,BigObject对象的归属权会发生转移,先进入新创建的线程的内部存储空间,再转移给process_big_object()函数。
  1. void process_big_object(std::unique_ptr<BigObject>);
  2. std::unique_ptr<BigObject> p(new BigObject);
  3. std::thread t(process_big_object, std::move(p));
复制代码
3、移交线程归属权

std::thread不能复制,但支持移动语义。对于一个具体的执行线程,其归属权可以在多个std::thread实例之间转移。
  1. void some_function();
  2. void some_other_function();
  3. std::thread t1(some_function);
  4. std::thread t2 = std::move(t1); //将线程的归属权显式地转移给t2
  5. t1 = std::thread(some_other_function); //线程原本与std::thread临时对象关联,其归属权随即转移给t1
  6. std::thread t3; //按默认方式构造,未关联任何线程
  7. t3 = std::move(t2); //t2原本关联的线程的归属权转移给t3
  8. //经过上面这些转移,t1与运行some_other_function的线程关联,t2没有关联线程,t3与运行some_function的线程关联
  9. t1 = std::move(t3); //在这次转移之时,t1已经关联运行some_other_function的线程,因此std::thread的析构函数中会调用std::terminate(),终止整个程序。所以只要std::thread对象还在管控着一个线程,就不能简单地向它赋新值。
复制代码
因为std::thread支持移动语义,所以只要容器同样知悉移动意图,就可以装载std::thread对象。因此我们可以写出下列代码,生成多个线程,然后等待它们运行完成。
  1. void do_work(unsigned id);
  2. void foo()
  3. {
  4.     std::vector<std::thread> threads;
  5.     for (unsigned i = 0; i < 20; ++i)
  6.     {
  7.         threads.push_back(std::thread(do_work, i));
  8.     }
  9.     for (auto& entry : threads)
  10.     {
  11.         entry.join();
  12.     }
  13. }
复制代码
4、识别线程

使用C++标准库的std::thread::hardware_concurrency()函数可以获取系统中逻辑处理器的数量。如果信息无法获取,该函数可能返回0。
线程ID的类型是std::thread::id,它有两种获取方法:

  • 在与线程关联的std::thread对象上调用成员函数get_id(),即可得到该线程的ID。如果std::thread对象没有关联任何执行线程,则调用get_id()返回的是按默认构造方式生成的std::thread::id对象,表示“线程不存在”。
  • 当前线程的ID可以通过调用std::this_thread::get_id()获取。
std::thread::id对象可以支持很多种操作:
<ul>可随意进行复制操作或比较运算。
可用作关联容器(std::set、std::map、std::multiset、std::multimap)的键值,无序关联容器(std::unordered_set、std::unordered_map、std::unordered_multiset、std::unordered_multimap)的键值,或用于排序。
写到输出流,例如std::cout
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

科技颠覆者

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表