1. 项目概述:当多线程遇上阻塞与中断
在C++的后端开发、游戏服务器或者高性能计算领域里,多线程编程几乎是绕不开的坎。但真正让开发者头疼的,往往不是创建几个线程那么简单,而是当线程因为I/O操作、锁竞争或者条件等待而“卡住”——也就是阻塞时,我们该如何优雅地控制它,特别是在需要让线程安全退出的场景下。标题里的“中断处理”,指的就是这种能力:告诉一个正在“忙”或者“等”的线程,别干了,该结束了。然而,C++标准库本身并没有提供像Java那样的Thread.interrupt()机制,这让很多从Java转过来的朋友,或者初次深入C++并发编程的开发者感到困惑。我最初也在这个问题上栽过跟头,一个后台服务线程因为等待网络数据而阻塞,主线程想关闭它,却发现它“不听使唤”,最终只能粗暴地terminate,导致资源泄露,程序状态一团糟。
所以,今天我想分享的,就是我在实际项目中,如何利用C++现有的工具和设计模式,构建一套可靠的多线程阻塞控制与安全中断机制。这套方案不依赖于任何平台特定的API,核心思想是利用条件变量、原子标志和可中断的等待来模拟中断行为。它不仅能解决网络I/O、定时等待中的阻塞问题,还能扩展到处理那些因互斥锁而可能永久阻塞的场景。无论你是正在开发一个需要长时间运行任务的服务,还是一个对响应速度有苛刻要求的实时系统,理解并实现这套模式,都能让你的程序在面对“停止”指令时,表现得更加从容和健壮。
2. 核心思路:为什么C++没有原生线程中断?
在深入解决方案之前,我们必须先理解问题的根源。C++11引入了强大的<thread>库,提供了std::thread、互斥量、条件变量等基础组件,但它故意没有提供强制中断另一个线程的接口。这不是设计上的疏忽,而是出于安全性和确定性的深刻考量。
想象一下,如果一个线程在执行到一半时被外部强行终止(类似于Unix的pthread_cancel),会发生什么?它可能正持有某个互斥锁(std::mutex),锁还没来得及释放就被终结了,这会导致其他等待该锁的线程永久死锁。它可能正在操作一个复杂的数据结构,更新到一半,留下一个处于不一致、崩溃状态的对象。这种“非协作式”的中断是灾难性的,会破坏程序的不变量,导致资源泄露和不可预知的行为。
因此,C++标准委员会的选择是:线程中断必须是协作式的。这意味着,一个线程不能从外部被强行杀死,它必须周期性地检查一个“中断请求”标志,并主动、安全地清理资源后退出。这虽然把责任交给了开发者,但换来了程序的确定性和可靠性。我们的任务,就是设计一套机制,让线程能够方便、高效地进行这种协作式检查,尤其是在它被阻塞(无法主动检查)的时候,如何唤醒它并让它“看到”中断请求。
我的核心设计模式基于以下三个支柱:
- 一个原子布尔标志(
std::atomic<bool>):作为中断请求信号。主线程设置它,工作线程检查它。 - 条件变量(
std::condition_variable):作为唤醒机制。当工作线程在等待某些条件(如数据到达、超时)而阻塞时,我们可以通过条件变量通知它,让它从阻塞中返回,从而有机会去检查中断标志。 - 可中断的等待封装:我们将标准的等待操作(如
condition_variable::wait,this_thread::sleep_for)封装在一个循环里,这个循环同时检查条件是否满足和中断标志是否被设置。
3. 基础构建:实现一个可中断线程类
让我们从最基础的开始,实现一个InterruptibleThread类。这个类封装了线程生命周期和中断逻辑。
3.1 类定义与成员变量
首先,我们定义这个类。它需要管理一个线程对象、一个中断标志,以及一个用于唤醒的条件变量(及其关联的互斥锁)。这里我选择将条件变量和互斥锁也作为成员,因为它们对于实现可中断的等待至关重要。
#include <atomic> #include <thread> #include <mutex> #include <condition_variable> #include <functional> #include <iostream> class InterruptibleThread { public: InterruptibleThread(); ~InterruptibleThread(); // 启动线程,传入要执行的任务函数 template<typename Function, typename... Args> void start(Function&& f, Args&&... args); // 请求中断线程 void interrupt(); // 等待线程结束(可超时) bool join(unsigned int timeout_ms = 0); // 检查当前线程是否被请求中断 static bool is_interrupted(); // 可中断的睡眠 static void sleep_for_ms(long ms); private: std::thread worker_thread_; // 底层线程对象 std::atomic<bool> interrupt_requested_; // 中断请求标志 std::mutex cv_mutex_; // 用于条件变量的互斥锁 std::condition_variable interrupt_cv_; // 用于唤醒阻塞线程的条件变量 // 线程本地存储,指向当前线程的中断标志 static thread_local std::atomic<bool>* this_thread_interrupt_flag; // 内部线程执行函数 void run(std::function<void()> task); };关键点解析:
interrupt_requested_是原子变量,确保在多线程环境下对其的读写是安全的,无需额外的锁。interrupt_cv_和cv_mutex_是配对使用的。当线程因为等待业务条件而阻塞时,我们可能无法直接唤醒它。但我们可以通过interrupt_cv_.notify_all()来唤醒所有在该条件变量上等待的线程,这给了我们一个插入中断检查的机会。thread_local变量this_thread_interrupt_flag是精髓所在。每个线程都有自己的一份拷贝,它指向该线程所属的InterruptibleThread实例的interrupt_requested_标志。这样,在线程的静态函数is_interrupted()中,就能快速访问到自己的中断标志,而无需通过复杂的线程ID映射。
3.2 线程启动与本地标志绑定
接下来是start方法和run方法的实现。start方法使用完美转发来接受任何可调用对象和参数。
template<typename Function, typename... Args> void InterruptibleThread::start(Function&& f, Args&&... args) { // 确保线程没有在运行 if (worker_thread_.joinable()) { throw std::runtime_error("Thread is already running."); } interrupt_requested_.store(false); // 将任务打包成std::function auto task = std::bind(std::forward<Function>(f), std::forward<Args>(args)...); // 启动新线程,执行run方法 worker_thread_ = std::thread(&InterruptibleThread::run, this, task); } void InterruptibleThread::run(std::function<void()> task) { // 关键步骤:将当前线程的中断标志指向本对象的标志 this_thread_interrupt_flag = &interrupt_requested_; try { task(); // 执行用户任务 } catch (const std::exception& e) { std::cerr << "Thread task exited with exception: " << e.what() << std::endl; } catch (...) { std::cerr << "Thread task exited with unknown exception." << std::endl; } // 线程结束,清空本地标志指针 this_thread_interrupt_flag = nullptr; } // 定义thread_local变量 thread_local std::atomic<bool>* InterruptibleThread::this_thread_interrupt_flag = nullptr;为什么需要run包装函数?直接让std::thread执行用户任务task不行吗?不行,因为我们需要一个时机来设置thread_local变量this_thread_interrupt_flag。这个设置操作必须在新线程的上下文中执行。run函数就是这个上下文,它在任务执行前绑定标志,在任务执行后(无论正常还是异常)清理标志,确保了资源的正确管理。
3.3 中断请求与状态检查
中断请求非常简单,就是设置原子标志,并通知条件变量。
void InterruptibleThread::interrupt() { interrupt_requested_.store(true); interrupt_cv_.notify_all(); // 唤醒所有可能在该条件变量上等待的线程 }is_interrupted静态方法用于在任务函数内部检查中断状态。
bool InterruptibleThread::is_interrupted() { auto flag = this_thread_interrupt_flag; return flag ? flag->load() : false; }这里有一个重要的防御性判断:flag ? flag->load() : false。如果this_thread_interrupt_flag是nullptr,说明当前线程不是一个由InterruptibleThread管理的线程(或者run函数尚未执行或已结束),那么is_interrupted()应该返回false。这避免了空指针解引用,并让非托管线程可以安全地调用此函数(总是返回未被中断)。
3.4 实现可中断的睡眠
一个常见的阻塞场景是主动睡眠,例如std::this_thread::sleep_for。我们可以实现一个自己的可中断版本。
void InterruptibleThread::sleep_for_ms(long ms) { if (ms <= 0 || is_interrupted()) return; std::unique_lock<std::mutex> lock(cv_mutex_); // 使用条件变量的wait_for,同时等待超时或中断通知 interrupt_cv_.wait_for(lock, std::chrono::milliseconds(ms), [] { return is_interrupted(); // 谓词:如果被中断,则不再等待 }); }工作原理:condition_variable::wait_for会在三种情况下返回:
- 超时:等待了指定的
ms毫秒。 - 谓词为真:在等待期间,
is_interrupted()返回了true。 - 虚假唤醒:操作系统无缘无故唤醒了线程(这是条件变量的特性)。
当主线程调用interrupt()时,会notify_all()。这会唤醒正在wait_for的sleep_for_ms函数。唤醒后,它会立即检查谓词is_interrupted(),此时因为中断标志已被设置,谓词为真,于是wait_for返回true,函数提前结束。这就实现了睡眠的中断。
注意:这里我们复用了
interrupt_cv_和cv_mutex_。这意味着所有由同一个InterruptibleThread对象管理的可中断等待(睡眠、或者其他后面会讲到的等待)都共享同一个条件变量。这通常是合理且高效的,因为中断一个线程,自然希望唤醒它所有可能的阻塞点。
4. 处理各类阻塞场景的实战策略
有了基础的可中断线程类,我们就可以针对不同的阻塞场景,设计具体的可中断方案了。这是实战中最关键的部分。
4.1 场景一:循环任务中的定期检查
这是最简单也是最常见的模式。你的线程可能在一个while循环中执行计算或处理任务。
void background_task() { while (!InterruptibleThread::is_interrupted()) { // 1. 执行一部分工作 do_some_work(); // 2. 在循环的间隙检查中断 // 如果这里检查到中断,循环条件为假,退出循环 } // 3. 安全清理资源 cleanup(); }实操要点:
- 检查频率:检查中断的频率需要权衡。检查太频繁(比如在循环最内层)会影响性能;检查太少会导致中断响应延迟。通常放在一个逻辑阶段完成后检查是合理的。
- 不可分割的操作:如果
do_some_work()中包含对共享数据结构的复杂修改,你需要确保这个修改是原子的,或者在被中断后,数据结构仍处于一致状态。有时,可能需要实现事务性的操作,或者将工作设计成更小的、可回滚的步骤。
4.2 场景二:等待条件变量(生产者-消费者)
这是阻塞问题的核心难点。假设你有一个经典的生产者-消费者队列,消费者线程在队列为空时等待。
template<typename T> class InterruptibleQueue { private: std::queue<T> queue_; std::mutex mutex_; std::condition_variable data_cond_; std::atomic<bool> interrupted_{false}; // 队列级别的中断标志 public: bool pop(T& value, InterruptibleThread& owner_thread) { std::unique_lock<std::mutex> lock(mutex_); // 关键:复合等待条件 bool success = data_cond_.wait_for(lock, std::chrono::milliseconds(100), [this, &owner_thread]() { return !queue_.empty() || owner_thread.is_interrupted() || interrupted_.load(); }); if (owner_thread.is_interrupted() || interrupted_.load()) { return false; // 因中断而返回 } if (!success) { // 超时,可以继续等待或做其他事情 return false; } // 成功等到数据 value = std::move(queue_.front()); queue_.pop(); return true; } void interrupt_all() { interrupted_.store(true); data_cond_.notify_all(); // 通知所有等待的消费者 } // ... push 方法等其他实现 };设计解析:
- 复合谓词:
wait_for的谓词(lambda)检查三个条件:队列非空、线程被中断、队列本身被中断。只要任一为真,等待就结束。 - 双重中断检查:等待返回后,必须再次检查中断标志。因为
wait_for可能因超时或虚假唤醒返回,此时中断标志可能为假。再次检查确保了中断状态的准确性。 - 超时机制:这里我使用了
wait_for并设置了100毫秒超时。这有两个好处:一是防止永久阻塞(比如生产者逻辑有bug再也不生产了),二是给了线程一个定期检查其他条件(如中断)的机会。这是一种“轮询式”协作的中断实现。 - 队列级中断:
interrupted_标志允许从外部中断所有等待在这个队列上的消费者,而不需要知道具体的线程对象。这在关闭整个服务时非常有用。
4.3 场景三:网络I/O阻塞(使用select/poll)
对于套接字I/O,纯粹的阻塞read/write调用是无法被我们的条件变量唤醒的。我们需要使用非阻塞I/O结合多路复用(如select,poll, 或Linux的epoll)来实现可中断的等待。
核心思想是:创建一个管道(pipe)或事件文件描述符(eventfd),并将其加入到多路复用的监听集合中。当需要中断线程时,向这个管道写入数据。线程的select/poll会因为这个管道变得可读而返回,从而跳出阻塞,此时检查中断标志并退出。
#include <sys/select.h> #include <unistd.h> #include <fcntl.h> class InterruptibleSocketReader { private: int socket_fd_; int interrupt_pipe_[2]; // pipe[0]读端,pipe[1]写端 std::atomic<bool> interrupted_{false}; public: InterruptibleSocketReader(int sock_fd) : socket_fd_(sock_fd) { if (pipe(interrupt_pipe_) == -1) { throw std::runtime_error("Failed to create pipe"); } // 将读端设置为非阻塞(可选,但更安全) fcntl(interrupt_pipe_[0], F_SETFL, O_NONBLOCK); } ~InterruptibleSocketReader() { close(interrupt_pipe_[0]); close(interrupt_pipe_[1]); } ssize_t read_with_interrupt(char* buffer, size_t size) { fd_set readfds; FD_ZERO(&readfds); FD_SET(socket_fd_, &readfds); FD_SET(interrupt_pipe_[0], &readfds); int max_fd = std::max(socket_fd_, interrupt_pipe_[0]) + 1; // 使用select等待socket可读或中断管道可读 int ret = select(max_fd, &readfds, nullptr, nullptr, nullptr); // 无限等待 if (ret == -1) { // select错误 return -1; } if (FD_ISSET(interrupt_pipe_[0], &readfds)) { // 中断管道可读,意味着收到了中断信号 char dummy; ::read(interrupt_pipe_[0], &dummy, 1); // 清空管道数据 interrupted_.store(true); return -2; // 用特殊返回值表示被中断 } if (FD_ISSET(socket_fd_, &readfds)) { // socket可读,执行真正的读操作 return ::read(socket_fd_, buffer, size); } // 理论上不会走到这里 return -1; } void interrupt() { if (!interrupted_.exchange(true)) { char dummy = 0; // 向管道写入一个字节,唤醒select ::write(interrupt_pipe_[1], &dummy, 1); } } bool is_interrupted() const { return interrupted_.load(); } };关键技巧与避坑指南:
- 管道清空:在
select返回并检测到管道可读后,必须调用read将管道内的数据读出清空。否则,管道会一直处于可读状态,导致后续的select调用立即返回,无法再监听socket。 - 原子标志:
interrupted_标志用于避免多次写入管道。exchange(true)操作原子性地将其设置为true并返回旧值。如果已经是true,则不再写入。 - 返回值设计:
read_with_interrupt返回-2表示被中断,这与系统调用错误(返回-1)区分开来,方便上层处理。 - 平台兼容性:
pipe和select是POSIX标准,在Linux和macOS上可用。在Windows上,你需要使用_pipe和_select,或者使用socketpair创建一对本地套接字来模拟管道。
4.4 场景四:等待互斥锁(锁竞争)
如果一个线程在尝试获取一个被其他线程长期持有的锁时阻塞(std::mutex::lock),我们无法直接中断它。C++的std::mutex没有提供带超时或中断的锁操作。对于这种可能死锁的场景,预防优于治疗。
策略1:使用带超时的锁使用std::timed_mutex或std::recursive_timed_mutex的try_lock_for方法。
std::timed_mutex g_shared_mutex; void worker_with_timeout_lock() { std::unique_lock<std::timed_mutex> lock(g_shared_mutex, std::defer_lock); if (lock.try_lock_for(std::chrono::milliseconds(100))) { // 成功获取锁,执行操作 do_critical_work(); } else { // 超时未获取锁 if (InterruptibleThread::is_interrupted()) { return; // 因中断而放弃 } // 可以选择重试、记录日志或执行其他降级操作 std::cout << "Failed to acquire lock, may retry." << std::endl; } }策略2:层级锁或锁顺序通过强制规定所有线程以相同的顺序获取锁,可以预防死锁。这是解决死锁问题的根本方法之一,但它属于系统设计层面,而非运行时中断。
策略3:死锁检测与恢复这是一个更高级的主题,通常需要维护一个锁的依赖图,并使用一个独立的看门狗线程来检测循环等待。一旦检测到死锁,看门狗线程可以强制中断(通过我们上述的协作方式)其中一个参与死锁的线程。实现复杂,在一般应用中较少使用。
核心心得:对于锁阻塞,最实用的方法就是避免长时间持有锁,并优先使用带超时的锁。将临界区的代码设计得尽可能短小精悍。如果某个操作可能耗时很长,考虑是否可以将数据拷贝出来,在锁外进行处理。
5. 资源清理与线程安全退出
请求中断只是第一步,确保线程能够带着所有资源安全退出,才是真正的挑战。不恰当的退出会导致内存泄露、文件未关闭、锁未释放等问题。
5.1 使用RAII管理资源
这是C++的黄金法则。确保所有资源(动态内存、文件句柄、数据库连接、锁)都由对象管理,利用析构函数自动释放。
class DatabaseConnection { // ... 数据库连接句柄 public: DatabaseConnection(const std::string& conn_str) { /* 建立连接 */ } ~DatabaseConnection() { /* 安全关闭连接,即使发生异常 */ } // 禁用拷贝,允许移动 }; void database_task() { DatabaseConnection conn("server=localhost;..."); // RAII对象 while (!InterruptibleThread::is_interrupted()) { // 使用conn工作 conn.query("SELECT ..."); InterruptibleThread::sleep_for_ms(1000); } // 循环退出时,conn的析构函数会自动调用,关闭连接。 }即使while循环因为中断检查而退出,或者在query中抛出了异常,conn的析构函数都会被调用,资源得以释放。
5.2 在中断检查点进行回滚
对于复杂的、多步骤的事务性操作,需要在中断检查点设计回滚逻辑。
void complex_transaction() { std::vector<Data> local_changes; // 先在本地准备修改 // 步骤1:收集数据 for (...) { if (InterruptibleThread::is_interrupted()) { return; // 早期退出,local_changes会被自动清理 } local_changes.push_back(prepare_data()); } // 步骤2:获取锁并应用修改 std::lock_guard<std::mutex> lock(shared_data_mutex); // 再次检查!因为获取锁可能等待了很久 if (InterruptibleThread::is_interrupted()) { return; // 在持有锁的情况下退出?危险!需要先释放锁。 // 更好的做法:锁应该在RAII对象析构时释放,这里直接return是安全的。 // lock_guard会在栈展开时释放锁。 } for (const auto& change : local_changes) { apply_change_to_shared_data(change); } // 修改完成,锁由lock_guard自动释放 }注意:在持有锁的时候检查到中断,直接return是否安全?答案是:是安全的,但前提是你用的是std::lock_guard或std::unique_lock这样的RAII锁管理器。它们的析构函数会保证锁被释放。如果你用的是纯mutex.lock()和mutex.unlock(),就必须在return前手动unlock,否则会导致死锁。
5.3 线程join与超时控制
主线程需要等待工作线程结束。我们实现的join方法提供了超时选项。
bool InterruptibleThread::join(unsigned int timeout_ms) { if (!worker_thread_.joinable()) { return true; // 线程已结束或未启动 } if (timeout_ms == 0) { // 无限等待 worker_thread_.join(); return true; } else { // 超时等待 auto status = worker_thread_.native_handle(); // 注意:std::thread 没有标准的超时join接口。 // 一种平台相关的方法是使用 native_handle 和 pthread_join 或类似API。 // 更可移植的做法是:不提供超时join,或者用另一种模式。 // 这里展示一种思路:循环检查+短睡眠。 auto start = std::chrono::steady_clock::now(); while (std::chrono::steady_clock::now() - start < std::chrono::milliseconds(timeout_ms)) { if (!worker_thread_.joinable()) { return true; } std::this_thread::sleep_for(std::chrono::milliseconds(50)); } // 超时后,线程仍在运行 return false; } }重要警告:C++标准库的std::thread::join()是阻塞且没有超时参数的。上面的超时join实现是一种“忙等待”加短睡眠的模拟,它并不是真正的让join调用超时,而是周期性地检查线程是否可连接。在线程结束时,joinable()会变为false。这种方法在超时后,线程对象仍然存在,线程仍在运行,这可能导致资源管理问题。更健壮的做法是避免使用超时join,而是通过其他同步机制(如future/promise)来等待线程结束,或者接受无限期等待,并通过我们之前的中断机制来确保线程最终会结束。
6. 常见问题排查与性能考量
在实际使用这套机制时,你可能会遇到以下问题:
6.1 虚假唤醒与条件变量
条件变量(std::condition_variable)存在“虚假唤醒”现象:即wait操作可能在未被notify的情况下返回。这是POSIX标准和C++标准允许的,通常是为了性能优化。我们的代码必须能处理这种情况。
这就是为什么wait和wait_for总是应该使用一个谓词(predicate)来循环检查等待条件。
// 错误:可能因虚假唤醒而错误地继续执行 std::unique_lock<std::mutex> lock(mutex); while (queue.empty()) { // 只用while检查业务条件 cond.wait(lock); } // 虚假唤醒后,queue可能仍是空的,但代码会继续往下走! // 正确:将条件检查作为谓词传入wait std::unique_lock<std::mutex> lock(mutex); cond.wait(lock, [&queue] { return !queue.empty(); }); // 内部是循环检查在我们的可中断等待设计中,谓词里包含了中断检查(is_interrupted()),因此虚假唤醒只会导致多一次谓词检查,如果中断未发生且业务条件不满足,线程会再次进入等待,行为是正确的。
6.2 性能开销评估
引入中断检查必然带来开销:
- 原子操作开销:每次检查
is_interrupted()都涉及原子变量的加载(load),虽然现代CPU上开销很小,但在极高频的循环中仍需注意。 - 条件变量通知开销:
notify_all()会唤醒所有等待的线程,可能引发不必要的线程上下文切换(惊群效应)。如果只有一个线程在等待,使用notify_one()更好。 - 超时等待开销:使用
wait_for代替wait,意味着即使没有中断,操作系统也需要定时唤醒线程来检查超时,这增加了系统调用的次数。
优化建议:
- 降低检查频率:在密集计算循环中,每迭代N次(例如1000或10000次)检查一次中断,而不是每次迭代都检查。
- 使用线程本地缓存:如果中断标志很少被设置,可以让工作线程缓存这个标志的值,定期(比如每100ms)从主标志更新一次。这减少了原子操作,但牺牲了中断响应的实时性。
- 区分“紧急中断”和“优雅停止”:对于需要立即响应的中断(如用户取消),使用上述的原子标志+条件变量。对于允许完成当前工作单元后再停止的场景,可以只用一个简单的原子标志,无需条件变量唤醒。
6.3 信号安全与异步信号
在多线程程序中使用信号(如SIGINT,SIGTERM)需要格外小心。信号处理函数(signal handler)中只能调用异步信号安全的函数(如write,_exit),而printf,malloc, 以及我们使用的std::atomic::store、condition_variable::notify_all都不是异步信号安全的。
安全做法:在信号处理函数中设置一个全局的volatile sig_atomic_t标志。在主线程或一个专门的监控线程中,轮询这个标志。当发现信号标志被设置后,再通过线程安全的机制(如设置我们之前定义的原子中断标志)来通知各个工作线程。
#include <csignal> #include <unistd.h> volatile sig_atomic_t g_signal_received = 0; void signal_handler(int sig) { g_signal_received = 1; } int main() { std::signal(SIGINT, signal_handler); std::signal(SIGTERM, signal_handler); InterruptibleThread worker; worker.start(background_task); // 主循环轮询信号标志 while (true) { if (g_signal_received) { std::cout << "Signal received, interrupting workers..." << std::endl; worker.interrupt(); break; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } worker.join(); return 0; }6.4 与C++标准库其他组件的结合
std::future和std::async当你使用std::async启动异步任务时,返回的std::future也提供了有限的“中断”能力——你可以调用future.wait_for(timeout),如果超时,你可以选择放弃这个任务。但是,你无法真正取消正在底层运行的任务线程。如果任务卡住,它还是会继续运行。我们的InterruptibleThread模式提供了更底层的控制。
std::jthread(C++20)C++20引入了std::jthread(joining thread),它在其析构函数中会自动join,并且内置了一个std::stop_source/std::stop_token机制用于协作式中断。这几乎是标准库对我们上面所做工作的官方实现!如果你的项目可以使用C++20,强烈建议直接使用std::jthread和std::stop_token,它们的设计更优雅、更安全。
#include <thread> #include <chrono> #include <iostream> void task_with_stop_token(std::stop_token stoken) { while (!stoken.stop_requested()) { std::cout << "Working...\n"; std::this_thread::sleep_for(std::chrono::seconds(1)); } std::cout << "Thread interrupted, cleaning up.\n"; } int main() { std::jthread worker(task_with_stop_token); std::this_thread::sleep_for(std::chrono::seconds(3)); // 请求中断,jthread析构时也会自动请求 worker.request_stop(); // jthread析构时会自动join return 0; }如果你的环境受限不能使用C++20,那么自己实现一套类似的机制,正如本文所详述的,就是必不可少的技能。理解其原理,也能让你在使用std::jthread时更加得心应手。