C++ 并发编程学习02 启动新线程,等待线程与分离线程。
启动一个线程,等待这个线程结束,或放在后台运行。
再看看怎么给已经启动的线程函数传递参数,以及怎么将一个线程的 所有权 从当前std::thread
对象移交给另一个。
最后,再来确定线程数,以及识别特殊线程。
02 线程管理
2.1 线程管理的基础
等待线程完成
join()
是简单粗暴的等待线程完成或不等待。调用join()
还清理了线程相关的存储部分,这样std::thread
对象将不再与已经完成的线程有任何关联,这意味着只能对 一个线程 使用 一次 join()
,一旦使用过,std::thread
对象就不能再次加入。
后台运行线程
使用detach()
会让线程在后台运行,这就意味着主线程不能与之产生直接交互。
分离线程 也被称为 守护线程(daemon threads), UNIX中守护线程是指,没有任何显式的用户接口,并在后台运行的线程。其特点就是长时间运行,线程的生命周期可能会从某一个应用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化。
调用std::thread
成员函数detach()来分离一个线程。之后,相应的std::thread
对象就与实际执行的线程无关了,并且这个线程也无法加入:
1 | std::thread t(do_background_work); |
不能对没有执行线程的std::thread
对象使用detach()或者join(),并且要用同样的方式进行检查——当std::thread
对象使用t.joinable()
返回的是true,就可以使用t.detach()
。
使用场景:
让一个文字处理应用同时编辑多个文档。无论是用户界面,还是在内部应用内部进行,都有很多的解决方法。虽然,这些窗口看起来是完全独立的,每个窗口都有自己独立的菜单选项,但他们却运行在同一个应用实例中。一种内部处理方式是,让每个文档处理窗口拥有自己的线程;每个线程运行同样的的代码,并隔离不同窗口处理的数据。如此这般,打开一个文档就要启动一个新线程。因为是对独立的文档进行操作,所以没有必要等待其他线程完成。因此,这里就可以让文档处理窗口运行在分离的线程上
2.2 向线程函数传递参数
在默认情况下,这些参数会被拷贝至 新线程 的 独立内存空间 中,以供新线程访问,并如同临时变量一样作为右值传递给可调用对象或函数。即使函数中的参数是 引用 的形式:
1 | void f(int i, std::string const& s); // 指针地址不可修改 |
如果线程函数期待传入一个引用,可以使用std::ref
将参数转换成引用的形式:1
2
3void update_data_for_widget(widget_id w,widget_data& data); //线程函数期待传入一个引用
widget_data data;
std::thread t(update_data_for_widget,w,std::ref(data));
从而,update_data_for_widget
就会接收到一个data
变量的引用,而非data
变量的拷贝副本。
上面两个例子的区别在于前者期待传入的 常量引用,而后者期待传入引用,即希望可以对其进行修改。
执行线程的所有权可以多个std::thread
实例中互相转移,这是依赖于std::threa
实例的 可移动性 和 不可复制性。不可复制性 保证了在同一时间点,一个std::thread
实例只能关联一个执行线程; 可移动性 使得开发者可以自己决定,哪个实例拥有实际执行线程的所有权。
2.3 转移线程所有权
假设要写一个在后台启动线程的函数,并想通过新线程返回的所有权去调用这个函数,而不是等待线程结束再去调用。或完全与之相反的想法:创建一个线程,并在函数中转移所有权,都必须要等待线程结束。所以,新线程的所有权都需要转移。
这就需要将 移动操作 引入std::thread
下面的例子,创建了两个执行线程,并且在std::thread
实例之间(t1, t2, t3)转移所有权。
1 | void some_function(); |
1: 新线程开始与t1相关联;
2: 显式使用std::move()
创建 t2后 , t1 的所有权就转移给了 t2 ,之后执行线程就和 t2 没有关联了;
3: 一个临时std::thread
对象相关的线程启动了。为什么不显式调用std::move()
转移所有权呢?因为,所有者是一个 临时对象 ——移动操作将会 隐式的调用;
4: 使用默认构造方式创建 t3, 没有与任何线程相关联;
5: 调用std::move()
将与t2关联线程的所有权转移到t3中。因为t2是一个命名对象,需要显式的调用std::move()
。移动操作 #5 完成后,t1与执行some_other_function的线程相关联,t2与任何线程都无关联,t3与执行some_function的线程相关联。
6: 最后一个移动操作,将some_function
线程的所有权转移给t1。不过,t1已经有了一个关联的线程(执行some_other_function
的线程),所以这里系统直接调用std::terminate()
终止程序继续运行。
2.4 运行时决定线程数量
std::thread::hardware_concurrency()
这个函数会返回能并发在一个程序中的线程数量。例如,多核系统中,返回值可以是CPU核芯的数量。返回值也仅仅是一个提示,当系统信息无法获取时,函数也会返回0。
例子:
1 |
|
输出效果:
如果 main()
改成如下:1
2
3
4
5
6
7
8
9
10
11int main()
{
std::vector<int> vi;
for (int i = 0; i < 71; ++i)
{
vi.push_back(i);
}
int sum = parallel_accumulate(vi.begin(), vi.end(), 5);
std::cout << "sum=" << sum << std::endl;
return 0;
}
结果为:
主要经历了下面的几个步骤:
2.5 标识线程
线程标识类型为std::thread::id
,可以通过两种方式进行检索,
通过调用
std::thread
对象的成员函数get_id()
来获取当前线程中调用
std::this_thread::get_id()
线程ID 可以在容器中作为键值,如,容器可以存储其掌控下每个线程的信息,或在多线程中互传信息。