news 2026/7/18 4:19:54

C++多线程阻塞控制与安全中断机制实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++多线程阻塞控制与安全中断机制实战指南

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++标准委员会的选择是:线程中断必须是协作式的。这意味着,一个线程不能从外部被强行杀死,它必须周期性地检查一个“中断请求”标志,并主动、安全地清理资源后退出。这虽然把责任交给了开发者,但换来了程序的确定性和可靠性。我们的任务,就是设计一套机制,让线程能够方便、高效地进行这种协作式检查,尤其是在它被阻塞(无法主动检查)的时候,如何唤醒它并让它“看到”中断请求。

我的核心设计模式基于以下三个支柱:

  1. 一个原子布尔标志(std::atomic<bool>:作为中断请求信号。主线程设置它,工作线程检查它。
  2. 条件变量(std::condition_variable:作为唤醒机制。当工作线程在等待某些条件(如数据到达、超时)而阻塞时,我们可以通过条件变量通知它,让它从阻塞中返回,从而有机会去检查中断标志。
  3. 可中断的等待封装:我们将标准的等待操作(如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_flagnullptr,说明当前线程不是一个由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会在三种情况下返回:

  1. 超时:等待了指定的ms毫秒。
  2. 谓词为真:在等待期间,is_interrupted()返回了true
  3. 虚假唤醒:操作系统无缘无故唤醒了线程(这是条件变量的特性)。

当主线程调用interrupt()时,会notify_all()。这会唤醒正在wait_forsleep_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 方法等其他实现 };

设计解析:

  1. 复合谓词wait_for的谓词(lambda)检查三个条件:队列非空、线程被中断、队列本身被中断。只要任一为真,等待就结束。
  2. 双重中断检查:等待返回后,必须再次检查中断标志。因为wait_for可能因超时或虚假唤醒返回,此时中断标志可能为假。再次检查确保了中断状态的准确性。
  3. 超时机制:这里我使用了wait_for并设置了100毫秒超时。这有两个好处:一是防止永久阻塞(比如生产者逻辑有bug再也不生产了),二是给了线程一个定期检查其他条件(如中断)的机会。这是一种“轮询式”协作的中断实现。
  4. 队列级中断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)区分开来,方便上层处理。
  • 平台兼容性pipeselect是POSIX标准,在Linux和macOS上可用。在Windows上,你需要使用_pipe_select,或者使用socketpair创建一对本地套接字来模拟管道。

4.4 场景四:等待互斥锁(锁竞争)

如果一个线程在尝试获取一个被其他线程长期持有的锁时阻塞(std::mutex::lock),我们无法直接中断它。C++的std::mutex没有提供带超时或中断的锁操作。对于这种可能死锁的场景,预防优于治疗。

策略1:使用带超时的锁使用std::timed_mutexstd::recursive_timed_mutextry_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_guardstd::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++标准允许的,通常是为了性能优化。我们的代码必须能处理这种情况。

这就是为什么waitwait_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::storecondition_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::futurestd::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::jthreadstd::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时更加得心应手。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/18 4:19:31

及时做APP开发实战(十)-深浅色主题适配实现

及时做APP开发实战(十)-深浅色主题适配实现本文将详细介绍如何在HarmonyOS NEXT中实现深浅色主题适配&#xff0c;让应用跟随系统主题动态切换。一、需求背景 1.1 功能需求 现代应用需要支持深浅色主题切换&#xff0c;提供更好的用户体验&#xff1a; ┌───────────…

作者头像 李华
网站建设 2026/7/18 4:18:20

B站用户行为数据分析架构演进与ClickHouse优化实践

1. 项目背景与核心挑战在当今互联网产品运营中&#xff0c;用户行为数据分析已成为驱动业务决策的关键引擎。作为国内领先的年轻人文化社区&#xff0c;B站面临着海量用户行为数据处理的严峻挑战&#xff1a;每天产生数千亿条行为事件&#xff0c;数据增量超过10TB&#xff0c;…

作者头像 李华
网站建设 2026/7/18 4:15:08

MR-D02多组分气液动态配气仪在高校实验室的应用场景与选型建议

高校实验室配气设备选型的挑战与解决方案高校实验室在气敏传感器研究、VOC检测、环境监测、红外光谱仪研发等领域&#xff0c;需要使用配气仪制备标准混合气体。然而&#xff0c;传统的纯气体配气仪无法满足液态有机溶剂的配气需求&#xff0c;导致实验受限。多组分气液动态配气…

作者头像 李华
网站建设 2026/7/18 4:13:06

影刀RPA 文件上传与下载的自动化处理

影刀RPA 文件上传与下载的自动化处理 作者&#xff1a;林焱 文件上传和下载看着简单——不就是点个按钮选个文件、或者点个链接保存个文件吗&#xff1f;但真到了自动化里&#xff0c;这俩操作坑多到你怀疑人生。上传按钮点击后弹出来的是系统文件选择框&#xff0c;影刀的元素…

作者头像 李华
网站建设 2026/7/18 4:12:35

Android应用隐私合规检测与SDK权限监控实践

1. 隐私合规检测的必要性与挑战在移动应用生态中&#xff0c;第三方SDK已成为功能扩展的标配组件。根据行业调研数据&#xff0c;平均每个Android应用集成8.3个SDK&#xff0c;其中约37%的SDK会申请超出其功能所需的权限。这种过度索权行为不仅违反《个人信息保护法》的最小必要…

作者头像 李华