[C++]C++并发编程实战Chapter2
2 线程管控
2.1 线程的基本管控
每个C++程序都含有至少一个线程,即运行main()的线程,它由C++运行时(C++ runtime)系统启动。随后,程序就可以发起更多线程,它们以别的函数作为入口(entry point)。这些新线程连同起始线程并发运行。当main()返回时,程序就会退出;同样,当入口函数返回时,对应的线程随之终结。
2.1.1 发起线程
线程通过构建 std::thread
对象启动,任何可调用类型都可以作为参数传递给该对象的构造函数。
如果在 std::thread
对象销毁的时候还没有决定汇合还是分离线程,那么析构函数会终止掉整个程序(需要注意,线程是否结束和该线程对象是否销毁没有关系,线程可能在决定汇合或者分离很久之前就已经结束运行,也可能在分离之后很久才结束运行)。
保证线程访问的外部数据有效
如果程序选择分离线程,且线程持有指针或者引用指向主线程的局部变量,如果此时主线程退出,子线程还在继续运行,会导致悬空指针or悬空引用问题。
解决方式:
- 令线程函数完全自含(self-contained),将数据复制到新线程内部,而不是共享数据。
- 汇合新线程
- 也可以使用智能指针传递参数,这样可以确保局部变量在使用期间不会被销毁掉。
2.1.2 等待线程完成
若需等待线程完成,可以在与之关联的std::thread实例上,通过调用成员函数join()实现。
只要调用了join(),隶属于该线程的任何存储空间即会因此清除,std::thread对象遂不再关联到已结束的线程。事实上,它与任何线程均无关联。对于某个给定的线程,join()仅能调用一次;只要std::thread对象曾经调用过join(),线程就不再可汇合(joinable),成员函数joinable()将返回false。
2.1.3 在出现异常的情况下等待
这个问题的本质其实是,如果要汇合线程,需要保证所有可能的退出路径都汇合线程。比较理想的做法是采用RAII思想,设计一个thread_guard类,在其析构函数中调用join.
1 |
|
如上所示,在 f
退出之前,会按照构建的逆序,销毁局部对象,因此会首先销毁thread_guard对象。
2.1.4 在后台运行线程
调用std::thread对象的成员函数detach(),会令线程在后台运行,遂无法与之直接通信。假若线程被分离,就无法等待它完结,也不可能获得与它关联的std::thread对象,因而无法汇合该线程。
然而分离的线程确实仍在后台运行,其归属权和控制权都转移给C++运行时库(runtime library,又名运行库),由此保证,一旦线程退出,与之关联的资源都会被正确回收。
UNIX操作系统中,有些进程叫作守护进程(daemon process),它们在后台运行且没有对外的用户界面;沿袭这一概念,分离出去的线程常常被称为守护线程(daemon thread)。这种线程往往长时间运行。几乎在应用程序的整个生存期内,它们都一直运行,以执行后台任务,如文件系统监控、从对象缓存中清除无用数据项、优化数据结构等。
不能凭空调用detach方法,必须确保该对象存在与其关联的线程。只有当t.joinable()返回true时,我们才能调用t.detach()。
2.2 向线程函数传递参数
若需向新线程上的函数或可调用对象传递参数,方法相当简单,直接向std::thread的构造函数增添更多参数即可。
线程具有内部存储空间,参数会按照默认方式先复制到该处,新创建的执行线程才能直接访问它们。然后,这些副本被当成临时变量,以右值形式传给新线程上的函数或可调用对象。即便函数的相关参数按设想应该是引用,上述过程依然会发生。
如果线程函数的参数是一个非常量引用,代码会无法通过编译,解决办法是用 std::ref
.
对于移动操作来说,资源所有权会先转移到线程的内部存储空间,然后再转移给线程函数。
2.3 移交线程归属权
std::thread
拥有其所关联的线程,支持移动语义。
我们可以修改上面 thread_guard
类的代码得到 scoped_thread
类,该类掌管线程,防止出现 thread_guard
对象的生命期超出其管控的线程,导致不良后果。
1 |
|
2.4 在运行时选择线程数量
C++标准库的std::thread::hardware_concurrency()函数,它的返回值是一个指标,表示程序在各次运行中可真正并发的线程数量。
1 |
|
2.5 识别线程
线程ID所属型别是std::thread::id,它有两种获取方法:
- 在与线程关联的std::thread对象上调用成员函数get_id(),即可得到该线程的ID。如果std::thread对象没有关联任何执行线程,调用get_id()则会返回一个std::thread::id对象,它按默认构造方式生成,表示“线程不存在”。
- 当前线程的ID可以通过调用std::this_thread::get_id()获得,函数定义位于头文件
<thread
内。
C++标准库容许我们随意判断两个线程ID是否相同,没有任何限制;std::thread::id型别具备全套完整的比较运算符,比较运算符就所有不相等的值确立了全序(total order)关系。