- 前言
- 线程启动
- 线程任务
- 线程结束
- 加入式
- 分离式
前言
总结一下多线程在开发中的应用,属于偏工程化的知识,因此不会有太多关于语法的介绍。但是作为c++11多线程的入门是完全没问题的。这里尽量不考虑源码和实现,而更关注实际应用,当然也希望能写的深入一点。
这一节主要有关头文件<thread>,相关知识参考:https://cppreference.cn/w/cpp/thread/thread
线程启动
当我们构造了 std::thread对象并为线程指定任务时,就会启动一个新的线程。每个std::thread对象都管理一个执行线程,并且每一个执行线程同一时间只能被一个std::thread对象管理,也就是说执行线程的所有权是可移动而不可复制的。
void do_some_work();
std::thread my_thread1(do_some_work); //传递一个函数,线程构造并启动执行do_some_work()
std::thread mymy_thread2;//无参构造,线程不会启动
当把函数对象传入到线程构造函数中时,如果传递了一个临时变量,而不是一个命名的变量;C++编译器会将其解析为函数声明,而不是类型对象的定义。
class background_task{public:void operator(){//...}
};
std::thread my_thread(background_task());
这里相当与声明了一个名为my_thread的函数,这个函数带有一个参数,返回一个std::thread对象的函数,而非启动了一个线程。
因此推荐使用花括号 {} 来构建对象。
线程任务
对于传参构建,我们需要传递给线程它要做的任务和参数:
template< class F, class... Args >
explicit thread( F&& f, Args&&... args );
对于启动的新线程来说,它会将提供的任务构造函数将传入的可调用对象和所有参数“移动”或“拷贝”到新线程独立拥有的内存中。具体流程是:
- 构造函数首先创建一系列 decay-copy 的 f 和 args...,意味着它会“衰减”掉引用和 cv 限定符(const, volatile),使函数和数组衰退为指针,并生成这些参数的副本。
- 对于左值参数,会调用其拷贝构造函数来创建副本;
- 对于右值参数,会调用其移动构造函数来“移动”资源,
这也就是说即使原来F中定义了参数传递方式为引用,在新线程中的操作也不会改变原始变量,
因此,如果希望线程函数直接使用原始对象,那么必须使用std::ref或std::cref来包装参数,或者也可以传递参数的地址然后通过解指针来操作,同时保证这些参数在线程的生命周期内是有效的,从而避免悬垂指针。
线程结束
在原始线程中成功创建并执行新线程后,新线程的执行进度、结束时机都将与主线程无关。先创建的线程,未必就先运行;新线程也未必就是在主线程结束之后。因此对于传递的引用和指针参数必须十分小心。这一节只做简单的引入。
加入式
如果新线程使用了原始线程中的某些资源,原始线程需要等待新线程结束,std::thread对象可以在原始线程中调用join(),这会将原始线程阻塞在此处,直到新线程结束。
此外,执行join() 还会显式要求操作系统在此处清理新线程的相关资源,而不是由操作系统自行决定。这也是为什么jion()只能调用一次。注意join() 不会清理目标线程中创建的线程局部存储,这是由线程自身完成。
分离式
如果希望将新线程托管给操作系统,调用detach() 可以将线程与 std::thread 对象分离,允许它独立运行。其 OS 资源将在该线程自行结束后由操作系统自动回收。
