
std::thread是 C++11 引入的一个类,是 C++11 标准库中的一个关键特性,它提供了一种在 C++ 程序中创建和管理线程的方法。通过使用std::thread,可以很容易地在 C++ 程序中创建多线程应用程序。每个std::thread对象都代表了一个独立的执行线程,这些线程可以并行地执行不同的任务。本文对std::thread进行详细介绍。
使用 std::thread,可以并行地执行代码,从而利用多核处理器的优势来提高程序的性能。
要使用 std::thread,需要包含 <thread> 头文件。
#include <thread>
#include <iostream>要创建一个新的线程,需要提供一个可调用对象(如函数、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 函数的线程)会等待新创建的线程结束后再继续执行。
可以像调用普通函数一样,向 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;
}Lambda 表达式是定义匿名函数对象的简洁方式,非常适合与 std::thread 一起使用。
示例 :使用 Lambda 表达式
int main() {
std::thread t([]() {
std::cout << "Hello from lambda thread!" << std::endl;
});
t.join();
return 0;
}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;
}每个 std::thread 对象都有一个唯一的标识符,可以通过调用 get_id() 方法来获取。
std::thread t(threadFunction);
std::cout << "Thread ID: " << t.get_id() << std::endl;
t.join();可以通过 joinable() 成员函数检查一个 std::thread 对象是否代表了一个可运行的线程(即,它是否已被创建但尚未被 join() 或 detach())。
std::thread t(threadFunction);
if (t.joinable()) {
t.join();
}①等待线程结束
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;
}②分离线程
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()),且当主线程结束时,如果分离线程还在运行,程序将立即终止,这可能导致资源泄露或其他问题。
③如果不调用 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 的高级特性。
虽然 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 来自动管理锁的生命周期。
虽然 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;
}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;
}虽然 std::thread 提供了创建和管理线程的能力,但需要自己确保数据在并发环境中的安全性。通常涉及使用互斥锁、原子操作或并发数据结构(如 C++17 中的 std::shared_mutex)。
当使用 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 的引用,而不是值。这是为了确保线程能够修改原始数据。但是,这也引入了并发访问的风险,因此需要谨慎处理。
std::thread 支持移动语义,但不支持拷贝语义。意味着可以将一个 std::thread 对象的所有权转移到另一个对象,但不能直接复制一个 std::thread 对象。
std::thread t1(threadFunction);
std::thread t2 = std::move(t1); // 移动t1到t2
// 此时t1不再代表任何线程,且t1.joinable()将返回false使用 std::thread 时,需要注意以下几个方面以确保程序的正确性和稳定性。
std::thread 对象销毁之前,必须确保它已经被 join() 或 detach()。如果线程仍然是可连接的(joinable)并且没有被适当处理,std::thread 的析构函数将调用 std::terminate() 来终止程序。std::thread 的生命周期,通过将线程对象封装在类中并在析构函数中调用 join() 或 detach() 来确保资源被正确释放。std::mutex)、条件变量(std::condition_variable)等,以避免数据竞争和不一致性。std::thread 构造函数传递参数时,需要确保这些参数(特别是引用和指针)在线程函数执行期间保持有效。对于局部变量,最好通过值传递或将其封装在具有适当生命周期的对象中。std::ref 或 std::cref 来包装引用,以确保它们在线程中保持有效。join())时,如果线程函数抛出了异常且未被捕获,则这些异常将在线程被 join() 时重新抛出到主线程中。因此,主线程必须准备好处理这些异常。以下是 std::thread 使用场景的一些总结。
当程序中有多个耗时的任务需要执行,并且这些任务之间没有明显的依赖关系时,可以使用 std::thread 来并行处理这些任务。这可以显著减少程序的总运行时间,特别是当这些任务可以在多核处理器上并行执行时。
在图形用户界面(GUI)程序中,长时间运行的任务(如文件读写、数据处理等)可能会阻塞主线程,导致用户界面无响应。通过将耗时任务移至 std::thread 中执行,可以保持主线程(通常负责处理用户输入和更新界面)的响应性。
对于需要在后台执行的任务(如日志记录、定时任务、网络请求等),可以使用 std::thread 来创建一个或多个线程,这些线程在后台运行,不会干扰主线程的执行。
在并发编程中,多个线程可能需要同时访问共享资源(如全局变量、文件、数据库等)。使用 std::thread 时,需要谨慎处理线程同步问题,以避免数据竞争和死锁等并发问题。这通常涉及使用互斥锁(std::mutex)、条件变量(std::condition_variable)等同步机制。
对于需要进行大量计算的应用程序(如科学计算、图像处理、视频编码等),可以利用 std::thread 来实现并发计算。通过将计算任务分配给多个线程,可以充分利用多核处理器的计算能力,从而加速计算过程。
在某些情况下,程序可能需要执行一些异步操作(如等待用户输入、等待网络响应等)。使用 std::thread 可以创建一个线程来执行这些异步操作,并通过某种机制(如共享变量、回调函数、条件变量等)将结果通知给主线程。
在开发并发服务器时,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 的构造函数和析构函数,这里有一个简要的概述:
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 对象(即不表示任何线程),则析构函数不会执行任何操作。