Thread线程库的基本使用
C++11
标准的发布是一个里程碑,它首次将线程支持纳入了标准库。这意味着开发者终于可以编写跨平台的、标准化的多线程程序了。
在使用 std::thread 之前,必须包含头文件 #include <thread>
创建和启动线程
在 C++11 中,创建一个 std::thread 对象的同时,新线程就已经开始执行了, 同时主线程立即、不阻塞地继续往下执行,新线程和主线程并行执行(这就有可能导致两个线程流同时执行而引发的资源竞争问题)
std::thread
的构造函数接受一个可调用对象(如函数、Lambda表达式、函数对象等)作为线程的入口点,后面跟着传递给该可调用对象的所有参数,
即std::thread t(function_name, args...); 例如,
下面的例子传入thread的是一个函数
1 |
|
我们也可以直接传入lambda表达式
1 |
|
等待线程完成 (join)
当你需要确保一个线程在主线程继续执行之前完成它的所有工作时,就需要使用 join() 方法。join() 的行为是:阻塞调用它的线程(例如主线程),直到被调用的线程(例如 t)执行结束。
使用阻塞的好处是, 一方面可以实现同步:确保子线程的工作成果在主线程的后续步骤中是可用的; 另一方面也确保资源安全:防止主线程退出导致整个进程结束,而子线程可能还在执行,从而被强行终止。
1 |
|
检查线程是否可加入 (joinable)
joinable()方法返回一个布尔值,如果线程可以被 join()或 detach(),则返回true,否则返回 false。如果我们试图对一个不可加入的线程调用 join()或 detach(),则会抛出一个 std::system_error异常。
下面是更安全的做法:
1 |
|
分离线程 (detach)
如果你不关心线程何时结束,也不需要等待它的结果,你可以选择“分离”它。
detach() 的行为是:将 std::thread 对象与实际执行的线程“断开连接”。 之后,这个线程会在后台独立运行,而 std::thread 对象本身不再代表这个线程。
其优点在于, 主线程无需等待,可以立即继续执行自己的任务。
但是同样具有风险:你失去了对该线程的控制。你无法再 join 它。更重要的是,如果主线程(或整个程序)退出了,所有分离的线程都会被操作系统粗暴地终止,不管它们是否完成了任务。这可能导致资源泄露或数据损坏。
例如, 你有一个分离的线程,它的任务是向一个文件中写入 1000 行日志
1 | void log_writer_task() { |
再例如, 一个分离的线程在堆上分配了内存,或者获取了一个数据库连接。
1 | void resource_task() { |
因此,使用 detach 的基本原则是:只对那些执行非常简单、不访问共享数据、不涉及需要清理的资源,并且允许被随时终止的“辅助性”任务使用。 对于任何核心的、需要确保完成的任务,都应该使用 join()。
RAII 与 std::thread 的所有权
一个非常重要的规则是:一个 std::thread 对象在析构时,如果它仍然是 joinable()(即既没有被 join 也没被 detach),那么程序的行为是调用 std::terminate(),导致程序崩溃。
这是为了防止开发者忘记处理线程而导致的资源泄露和未定义行为。
1 | // 错误示例:将导致程序崩溃 |
thread_local 变量
C++11 引入了 thread_local 关键字,用于创建线程私有的、具有静态生命周期的变量。每个线程都会有自己独立的 thread_local 变量实例,互不干扰。
线程私有性 (Thread Privacy): 这是 thread_local 最核心的特性。每个线程都拥有变量的独立实例。一个线程对它的 thread_local 变量进行任何修改,都绝对不会影响到其他线程中的同名变量。
静态存储期 (Static Storage Duration): thread_local 变量的生命周期与它所在的线程绑定。它在线程首次使用该变量时被创建和初始化,并在线程结束时被销毁。这意味着,对于一个特定的线程,这个变量的值在函数调用之间是持久的。
使用范围 (Usage Scope): thread_local 可以用于修饰命名空间作用域的变量(全局变量)、文件静态变量、函数静态变量以及类的静态成员变量。它不能用于修饰非静态的局部变量或类的非静态成员。
它非常适合用于解决那些“需要全局访问,但又不希望线程间共享”的场景,例如:
- 线程安全的计数器或日志记录器。
- 每个线程独立的随机数生成器(避免锁和种子问题)。
- 线程级别的缓存或错误码。
在 thread_local 出现之前,我们在多线程编程中处理变量时面临一个两难的境地:
全局变量/静态变量:
优点:生命周期是整个程序,可以在任何函数中访问。
缺点:所有线程共享同一个实例。如果多个线程同时修改它,就会产生竞争条件 (Race Condition),必须使用互斥锁 (std::mutex) 等同步机制来保护,这会增加代码复杂性并降低性能。
局部变量:
优点:位于函数栈上,是线程私有的,不存在竞争问题。
缺点:生命周期仅限于函数的单次调用。函数返回后,变量就被销毁了,无法在多次函数调用之间为同一个线程保持状态。
这时就出现了一个需求空白:如果我需要一个变量,它能像全局变量一样在多次函数调用间保持自己的状态,但又希望它像局部变量一样是线程私有的,避免加锁,该怎么办?
1 |
|
上述代码中,虽然所有线程都访问同一个 thread_local 变量 thread_id,但每个线程都有自己独立的实例。线程 1 设置 thread_id 为 1,线程 2 设置为 2,线程 3 设置为 3。它们互不干扰,输出结果会显示每个线程的独立值。
同时, 虽然 thread_local 名称中有 “local” 一词, 但它并不是局部变量。它的作用域仍然是声明它的文件或函数内,但它的生命周期是整个线程的运行期间。每个线程在第一次访问 thread_local 变量时,都会初始化它,并且在该线程结束时销毁它。