目录
1.5. 使用 std::ref 或 std::cref 传递引用
1.8.3. 如果不调用 join() 或 detach()
2.2. 线程本地存储(Thread Local Storage, TLS)
std::thread
是 C++11 引入的一个类,是 C++11 标准库中的一个关键特性,它提供了一种在 C++ 程序中创建和管理线程的方法。通过使用 std::thread
,可以很容易地在你的 C++ 程序中创建多线程应用程序。每个 std::thread
对象都代表了一个独立的执行线程,这些线程可以并行地执行不同的任务。
下面是对 std::thread
的详细介绍,包括其基本用法、高级特性以及需要注意的事项。
一、基本用法
使用 std::thread
,可以并行地执行代码,从而利用多核处理器的优势来提高程序的性能。
以下是 std::thread
的一些基本用法。
1.1. 包含必要的头文件
要使用 std::thread
,需要包含 <thread>
头文件。
#include <thread>
#include <iostream>
1.2. 创建线程
要创建一个新的线程,需要提供一个可调用对象(如函数、lambda 表达式、函数对象、绑定表达式等)给 std::thread
的构造函数。这个可调用对象将在新的线程中执行。
示例 :使用函数指针
#include <iostream>
#include <thread>
void threadFunction() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(threadFunction);
t.join(); // 等待线程结束
return 0;
}
在这个例子中,threadFunction
函数被一个新的线程执行。t.join()
调用确保主线程(即执行 main
函数的线程)会等待新创建的线程结束后再继续执行。
1.3. 传递参数给线程函数
可以像调用普通函数一样,向 std::thread
构造函数传递参数,这些参数会被拷贝或移动到新线程中。
示例 :传递参数给线程函数
#include <iostream>
#include <thread>
void threadFunction(int x, double y) {
std::cout << "Received: " << x << ", " << y << std::endl;
}
int main() {
std::thread t(threadFunction, 42, 3.14);
t.join();
return 0;
}
1.4. 使用 Lambda 表达式
Lambda 表达式是定义匿名函数对象的简洁方式,非常适合与 std::thread
一起使用。
示例 :使用 Lambda 表达式
int main() {
std::thread t([]() {
std::cout << "Hello from lambda thread!" << std::endl;
});
t.join();
return 0;
}
1.5. 使用 std::ref
或 std::cref
传递引用
当想在线程中修改外部变量时,需要传递引用而不是值。由于 std::thread
的构造函数默认按值捕获参数,所以需要使用 std::ref
或 std::cref
来传递引用。
#include <iostream>
#include <thread>
#include <functional> // 为了 std::ref
void modifyValue(int& x) {
x = 42;
}
int main() {
int value = 0;
std::thread t(modifyValue, std::ref(value)); // 注意 std::ref 的使用
t.join();
std::cout << "Value is now: " << value << std::endl; // 输出 42
return 0;
}
1.6. 线程的 ID
每个 std::thread
对象都有一个唯一的标识符,可以通过调用 get_id()
方法来获取。
std::thread t(threadFunction);
std::cout << "Thread ID: " << t.get_id() << std::endl;
t.join();
1.7. 检查线程是否可连接
可以通过 joinable()
成员函数检查一个 std::thread
对象是否代表了一个可运行的线程(即,它是否已被创建但尚未被 join()
或 detach()
)。
std::thread t(threadFunction);
if (t.joinable()) {
t.join();
}
1.8. 线程管理
1.8.1. 等待线程结束
- 主线程可以通过调用
join()
方法来等待其他线程完成。如果不调用join()
或detach()
,则当std::thread
对象被销毁时,程序会调用std::terminate()
终止未结束的线程,这可能会导致资源泄露或其他问题。
下面是一个简单的示例,它创建了一个线程来打印一些消息,并在主线程中等待该线程完成。
#include <iostream>
#include <thread>
void threadFunction() {
for (int i = 0; i < 5; ++i) {
std::cout << "Thread is running, count: " << i << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
}
}
int main() {
std::thread t(threadFunction); // 创建并启动线程
// 在这里可以做一些其他工作,但这里我们只是等待线程完成
t.join(); // 等待线程t完成
std::cout << "Thread has finished execution." << std::endl;
return 0;
}
1.8.2. 分离线程
- 默认情况下,新创建的线程会与创建它的线程(通常是主线程)同步。可以通过调用
detach()
方法来分离线程,这样主线程就可以继续执行,而不需要等待新线程结束。 - 通过调用
detach()
方法,可以使线程独立于主线程运行。一旦线程被分离,就不能再次对其调用join()
或detach()
,并且当主线程结束时,分离的线程将继续运行,直到完成其任务。
示例 :分离线程
#include <iostream>
#include <thread>
void threadFunction() {
// 假设这里有一些耗时的操作
std::cout << "Thread is running in the background." << std::endl;
}
int main() {
std::thread t(threadFunction);
t.detach(); // 分离线程,主线程继续执行
// 注意:分离后,主线程结束时不会等待这个线程完成
// 这里的程序可能在后台线程完成之前就结束了
// 在实际应用中,你可能需要其他机制来确保所有线程都已完成
return 0;
}
注意:分离线程后,无法再与这个线程进行同步(如 join()
或 detach()
),且当主线程结束时,如果分离线程还在运行,程序将立即终止,这可能导致资源泄露或其他问题。
1.8.3. 如果不调用 join() 或 detach()
如果不调用 join()
或 detach()
,并且 std::thread
对象在作用域结束时被销毁,程序将调用 std::terminate()
终止。这是因为 C++ 标准要求,如果 std::thread
的析构函数被调用,并且线程仍然是可连接的(即,没有被 join()
或 detach()
),则必须调用 std::terminate()
。
下面是一个不安全的示例,它展示了这种情况:
#include <iostream>
#include <thread>
void threadFunction() {
// 假设这里有一些耗时的操作
}
int main() {
{
std::thread t(threadFunction); // 创建一个线程,但作用域仅限于这个块
// 注意:我们没有调用 t.join() 或 t.detach()
// 当这个块结束时,t 的析构函数将被调用,但线程仍在运行
// 这将导致 std::terminate() 被调用,从而终止程序
}
// 这段代码不会被执行,因为程序已经在上面的块结束时终止了
std::cout << "This line will not be executed." << std::endl;
return 0; // 永远不会到达这里
}
为了避免这种情况,应该在
std::thread
对象被销毁之前调用join()
或detach()
。如果知道线程何时会结束,通常使用join()
是更安全的选择,因为它可以确保主线程等待子线程完成。如果您不关心子线程的完成时间,或者想要让子线程独立于主线程运行,那么可以使用detach()
。但是,请注意,使用detach()
时需要小心管理线程的生命周期和资源共享。
二、高级特性
除了基本用法外, std::thread
还有一些高级特性和使用场景。下面将介绍一些 std::thread
的高级特性。
2.1. 线程同步
虽然 std::thread
本身不直接提供同步机制,但可以使用其他同步原语(如 std::mutex
、std::condition_variable
、std::unique_lock
等)与 std::thread
一起使用来管理线程间的同步。
示例:使用 std::mutex
同步线程
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 全局互斥锁
void print_block(int n, char c) {
mtx.lock(); // 锁定互斥锁
for (int i = 0; i < n; ++i) { std::cout << c; }
std::cout << '\n';
mtx.unlock(); // 解锁互斥锁
}
int main() {
std::thread th1(print_block, 50, '*');
std::thread th2(print_block, 50, '$');
th1.join();
th2.join();
return 0;
}
注意:在上面的示例中,虽然使用了 std::mutex
来同步线程,但这种方式可能导致死锁或不必要的性能开销。更常见的做法是使用 std::lock_guard
或 std::unique_lock
来自动管理锁的生命周期。
2.2. 线程本地存储(Thread Local Storage, TLS)
虽然 std::thread
本身不直接提供 TLS,但可以使用 thread_local
关键字来声明线程局部变量。
示例:使用 thread_local
#include <iostream>
#include <thread>
thread_local int tls_counter = 0; // 每个线程都有自己的 tls_counter 副本
void increment_tls_counter() {
++tls_counter;
std::cout << "TLS counter: " << tls_counter << std::endl;
}
int main() {
std::thread t1(increment_tls_counter);
std::thread t2(increment_tls_counter);
increment_tls_counter(); // 在主线程中调用
t1.join();
t2.join();
return 0;
}
2.3. 线程中断
C++ 标准库中没有直接提供线程中断的机制(与 Java 中的 interrupt()
方法不同)。然而,可以通过共享状态或条件变量来实现类似的功能。
示例:使用共享状态中断线程
#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>
std::atomic<bool> stop(false); // 原子变量,用于线程中断
void do_work() {
while (!stop) {
// 执行一些工作
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
std::cout << "Work stopped\n";
}
int main() {
std::thread worker(do_work);
std::this_thread::sleep_for(std::chrono::seconds(2)); // 等待2秒
stop = true; // 设置停止标志
worker.join();
return 0;
}
2.4. 线程安全和并发数据结构
虽然 std::thread
提供了创建和管理线程的能力,但需要自己确保数据在并发环境中的安全性。这通常涉及使用互斥锁、原子操作或并发数据结构(如 C++17 中的 std::shared_mutex
)。
2.5. 捕获列表和 Lambda 表达式
当使用 Lambda 表达式与 std::thread
时,可以通过捕获列表来捕获外部变量。注意,默认情况下,捕获的变量是按值捕获的,这可能导致数据竞争或不一致的视图。
示例:使用 Lambda 表达式和捕获列表
#include <iostream>
#include <thread>
#include <vector>
int main() {
std::vector<int> data = {1, 2, 3, 4, 5};
// 按值捕获 data
std::thread t1([&data]() {
for (int& val : data) {
val *= 2;
}
});
t1.join();
for (int val : data) {
std::cout << val << ' ';
}
std::cout << '\n';
return 0;
}
注意:上面的 Lambda 表达式使用了 [&data]
来捕获 data
的引用,而不是值。这是为了确保线程能够修改原始数据。但是,这也引入了并发访问的风险,因此需要谨慎处理。
2.6. 交换和移动
std::thread
支持移动语义,但不支持拷贝语义。这意味着可以将一个 std::thread
对象的所有权转移到另一个对象,但不能直接复制一个 std::thread
对象。
std::thread t1(threadFunction);
std::thread t2 = std::move(t1); // 移动t1到t2
// 此时t1不再代表任何线程,且t1.joinable()将返回false
三、注意事项
使用 std::thread
时,需要注意以下几个方面以确保程序的正确性和稳定性。
3.1. 线程生命周期管理
- join() 或 detach() 的调用:在
std::thread
对象销毁之前,必须确保它已经被join()
或detach()
。如果线程仍然是可连接的(joinable)并且没有被适当处理,std::thread
的析构函数将调用std::terminate()
来终止程序。 - RAII 管理:推荐使用 RAII(资源获取即初始化)技术来管理
std::thread
的生命周期,通过将线程对象封装在类中并在析构函数中调用join()
或detach()
来确保资源被正确释放。
3.2. 线程同步与互斥
- 避免数据竞争:当多个线程可能同时访问共享数据时,必须采取适当的同步措施,如使用互斥锁(
std::mutex
)、条件变量(std::condition_variable
)等,以避免数据竞争和不一致性。 - 死锁的预防:在设计多线程程序时,要注意避免死锁,即两个或多个线程相互等待对方持有的资源而无法继续执行的情况。
3.3. 线程参数传递
- 临时对象的生命周期:当通过
std::thread
构造函数传递参数时,需要确保这些参数(特别是引用和指针)在线程函数执行期间保持有效。对于局部变量,最好通过值传递或将其封装在具有适当生命周期的对象中。 - 引用和指针的传递:传递引用或指针时要特别小心,因为它们可能指向即将被销毁的局部变量的内存。在这种情况下,可以使用
std::ref
或std::cref
来包装引用,以确保它们在线程中保持有效。
3.4. 异常处理
- 线程内的异常处理:线程函数内部可能抛出异常,这些异常必须被捕获并适当处理,以防止它们传播到线程之外并导致未定义行为。
- 主线程中的异常处理:当主线程等待线程完成(通过
join()
)时,如果线程函数抛出了异常且未被捕获,则这些异常将在线程被join()
时重新抛出到主线程中。因此,主线程必须准备好处理这些异常。
3.5. 性能考虑
- 线程创建和销毁的开销:线程的创建和销毁是有开销的,因此应避免频繁地创建和销毁线程。在可能的情况下,使用线程池来重用线程可以减少这些开销。
- 负载均衡:在分配任务给线程时,要注意负载均衡,以避免某些线程过载而其他线程空闲的情况。
3.6. 嵌入式和特定平台注意事项
- 堆栈大小:在嵌入式系统中,线程的堆栈大小可能需要手动设置,以避免堆栈溢出。
- 线程优先级:根据应用需求,可能需要设置线程的优先级以优化系统性能。
- 平台兼容性:不同平台对多线程的支持和限制可能不同,因此在跨平台开发时需要注意这些差异。
四、使用场景总结
以下是 std::thread
使用场景的一些总结。
4.1. 并行处理耗时任务
当程序中有多个耗时的任务需要执行,并且这些任务之间没有明显的依赖关系时,可以使用 std::thread
来并行处理这些任务。这可以显著减少程序的总运行时间,特别是当这些任务可以在多核处理器上并行执行时。
4.2. 提高用户界面的响应性
在图形用户界面(GUI)程序中,长时间运行的任务(如文件读写、数据处理等)可能会阻塞主线程,导致用户界面无响应。通过将耗时任务移至 std::thread
中执行,可以保持主线程(通常负责处理用户输入和更新界面)的响应性。
4.3. 后台处理
对于需要在后台执行的任务(如日志记录、定时任务、网络请求等),可以使用 std::thread
来创建一个或多个线程,这些线程在后台运行,不会干扰主线程的执行。
4.4. 并发访问共享资源
在并发编程中,多个线程可能需要同时访问共享资源(如全局变量、文件、数据库等)。使用 std::thread
时,需要谨慎处理线程同步问题,以避免数据竞争和死锁等并发问题。这通常涉及使用互斥锁(std::mutex
)、条件变量(std::condition_variable
)等同步机制。
4.5. 并发计算
对于需要进行大量计算的应用程序(如科学计算、图像处理、视频编码等),可以利用 std::thread
来实现并发计算。通过将计算任务分配给多个线程,可以充分利用多核处理器的计算能力,从而加速计算过程。
4.6. 异步操作
在某些情况下,程序可能需要执行一些异步操作(如等待用户输入、等待网络响应等)。使用 std::thread
可以创建一个线程来执行这些异步操作,并通过某种机制(如共享变量、回调函数、条件变量等)将结果通知给主线程。
4.7. 并发服务器
在开发并发服务器时,std::thread
可以用于处理来自多个客户端的请求。每个客户端请求可以由一个单独的线程来处理,从而实现高效的并发处理。然而,在实际应用中,更常见的做法是使用线程池(std::thread
的一个高级应用)来管理线程,以避免创建和销毁线程的开销。
五、总结
std::thread
是 C++ 标准库中处理多线程的一个非常强大的工具,但它也要求程序员对多线程编程的复杂性有一定的了解,包括线程同步、数据竞争和死锁等问题。
5.1. std::thread
的常用成员函数汇总
std::thread
的常用成员函数及其功能概述汇总表:
成员函数 | 功能概述 | 注意/用途 |
---|---|---|
joinable() | 检查线程对象是否可被 join。如果线程正在执行或可执行且未被 join 或 detach,则返回 true;否则返回 false。 | 不带参构造的 std::thread 对象或已被移动的 std::thread 对象不可 join。 |
join() | 阻塞当前线程,直到被 join 的线程完成其执行。如果线程未启动或已被 join/detach,则行为未定义(通常抛出异常)。 | 确保子线程在主线程继续执行之前完成其任务。 |
detach() | 将线程与 std::thread 对象分离,允许线程在后台继续运行。分离后,线程不再与任何 std::thread 对象关联,且不能被 join。 | 一旦线程被 detach,就无法再通过 std::thread 对象控制或等待该线程。 |
get_id() | 获取线程的标识符(ID),类型为 std::thread::id 。该 ID 在线程生命周期内唯一,但结束后可能会被重用。 | 用于标识和区分不同的线程。 |
native_handle() | 获取与实现相关的本机线程句柄。具体行为和返回值取决于操作系统和 C++ 运行时库。 | 使用时需了解当前平台的线程 API,并谨慎处理句柄以避免资源泄露或安全问题。 |
hardware_concurrency() | 返回硬件支持的并发线程数量的估计值(静态成员函数)。此值是一个提示,表示系统可能支持的并行线程数,但并非绝对限制。 | 帮助开发者在创建线程时决定合适的线程数量。 |
swap() | 交换两个 std::thread 对象的状态。如果两个对象都表示活动的线程,则它们的执行不受影响;但如果一个对象是空的,则另一个对象将不再与任何线程关联。 | 在需要转移线程所有权或管理线程集合时很有用。 |
std::thread
类在 C++11 及以后的版本中用于表示一个线程。关于 std::thread
的构造函数和析构函数,这里有一个简要的概述:
5.2. std::thread
类构造函数
std::thread
类提供了几个构造函数,但最常用的是以下两个。
1. 默认构造函数
std::thread() noexcept;
- 创建一个空的
std::thread
对象,它不表示任何线程。这种类型的对象是不可 join 的,也不可被 detach。
2. 带可调用对象的构造函数
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
- 创建一个新线程,该线程将执行给定的可调用对象
f
,f
可以是函数、lambda 表达式、绑定表达式或其他可调用对象。args...
是传递给f
的参数。这个构造函数启动一个新线程,并立即返回,允许std::thread
对象被用于管理新创建的线程。
5.3. std::thread
类析构函数
std::thread
的析构函数在对象被销毁时自动调用,它执行以下操作:
~thread();
- 如果
std::thread
对象表示一个可 join 的线程(即该线程仍在执行且尚未被 join 或 detach),则析构函数会调用std::terminate()
,以终止程序。这是为了防止程序在std::thread
对象被销毁时丢失对线程的跟踪,从而可能导致资源泄露或程序状态的不一致。 - 如果
std::thread
对象表示一个已经 join 或 detach 的线程,或者是一个空的std::thread
对象(即不表示任何线程),则析构函数不会执行任何操作。