C++线程如何安全停止与唤醒:std::jthread、stop_token与condition_variable

C++线程的唤醒与安全停止

  • [1. 前言](#1. 前言)
  • [2. 先区分:唤醒不等于停止](#2. 先区分:唤醒不等于停止)
  • [3. C++11:condition_variable 实现等待、唤醒和退出](#3. C++11:condition_variable 实现等待、唤醒和退出)
    • [3.1 为什么 wait 要带判断条件](#3.1 为什么 wait 要带判断条件)
  • [4. C++20:jthread 和 stop_token 协作式停止](#4. C++20:jthread 和 stop_token 协作式停止)
  • [5. C++20:停止正在等待的线程](#5. C++20:停止正在等待的线程)
  • [6. 常见误区](#6. 常见误区)
    • [6.1 把 notify 当成消息队列](#6.1 把 notify 当成消息队列)
    • [6.2 修改共享状态时不加锁](#6.2 修改共享状态时不加锁)
    • [6.3 工作期间一直占用互斥量](#6.3 工作期间一直占用互斥量)
    • [6.4 认为 request_stop 会立即中断一切](#6.4 认为 request_stop 会立即中断一切)
    • [6.5 忘记 std::thread 的生命周期](#6.5 忘记 std::thread 的生命周期)
  • [7. 如何选择](#7. 如何选择)
  • [8. 一页速查](#8. 一页速查)
  • [9. 总结](#9. 总结)

1. 前言

最近遇到线程控制问题时,我才发现"让线程停下来"和"把正在等待的线程唤醒"并不是同一件事。以前很容易把它们理解成直接暂停、恢复或者杀死线程,但在标准 C++ 中,更推荐的方式是:共享状态负责表达意图,条件变量负责唤醒,线程自己在安全的位置退出。

这篇文章记录 C++11 的 condition_variable,以及 C++20 的 jthread、stop_token 和 request_stop 的用法,方便以后遇到类似问题时快速查阅。

2. 先区分:唤醒不等于停止

操作 解决的问题 常见接口
等待 暂时没有任务时,避免线程空转 condition_variable::wait
唤醒 状态发生变化,通知等待线程重新检查 notify_one、notify_all
请求停止 告诉线程应该结束工作 request_stop、stop_token
等待结束 等待线程函数真正返回 join

notify_one() 只唤醒一个等待者,notify_all() 会唤醒全部等待者。它们不会自动让线程退出,也不会替我们保存"发生过一次通知"这个状态。

3. C++11:condition_variable 实现等待、唤醒和退出

cpp 复制代码
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

std::mutex g_mutex;
std::condition_variable g_cv;
bool g_hasTask = false;
bool g_stopping = false;

void worker()
{
    while (true)
    {
        std::unique_lock<std::mutex> lock(g_mutex);
        g_cv.wait(lock, [] {
            return g_hasTask || g_stopping;
        });

        if (g_stopping)
        {
            break;
        }

        g_hasTask = false;
        lock.unlock();
        std::cout << "worker: 开始处理任务" << std::endl;
    }

    std::cout << "worker: 安全退出" << std::endl;
}

int main()
{
    std::thread t(worker);

    {
        std::lock_guard<std::mutex> lock(g_mutex);
        g_hasTask = true;
    }
    g_cv.notify_one();

    std::this_thread::sleep_for(std::chrono::milliseconds(100));

    {
        std::lock_guard<std::mutex> lock(g_mutex);
        g_stopping = true;
    }
    g_cv.notify_all();

    t.join();
    return 0;
}

3.1 为什么 wait 要带判断条件

推荐写法:

cpp 复制代码
g_cv.wait(lock, [] {
    return g_hasTask || g_stopping;
});

而不是只写 g_cv.wait(lock)。原因有两个:

  1. 条件变量允许"虚假唤醒",线程醒来不代表条件一定成立。
  2. 通知本身不保存状态。如果先 notify,线程之后才开始 wait,就可能错过通知。

真正可靠的是受互斥量保护的状态,而不是通知本身。

4. C++20:jthread 和 stop_token 协作式停止

C++20 引入了 std::jthread。与 std::thread 相比,它的优点是:

  • 析构时会自动请求停止并等待线程结束;
  • 线程函数可以接收 std::stop_token;
  • 外部可调用 request_stop() 发出停止请求;
  • 不容易因为忘记 join() 而触发 std::terminate()。
cpp 复制代码
#include <chrono>
#include <iostream>
#include <stop_token>
#include <thread>

void worker(std::stop_token token)
{
    while (!token.stop_requested())
    {
        std::cout << "worker: 正在运行" << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(300));
    }

    std::cout << "worker: 收到停止请求" << std::endl;
}

int main()
{
    std::jthread t(worker);

    std::this_thread::sleep_for(std::chrono::seconds(1));
    t.request_stop();

    // jthread 析构时会自动 join
    return 0;
}

request_stop() 不会粗暴地杀死线程,它只是把停止状态设置为 true。线程必须主动检查 stop_requested(),然后自行清理资源并返回。

5. C++20:停止正在等待的线程

如果线程正阻塞在普通 condition_variable::wait() 上,仅调用 request_stop() 不能让普通 wait 自动返回。C++20 可以使用 condition_variable_any 支持 stop_token 的重载,把"等待任务"和"等待停止"合并起来。

cpp 复制代码
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <stop_token>
#include <thread>

std::mutex g_mutex;
std::condition_variable_any g_cv;
bool g_hasTask = false;

void worker(std::stop_token token)
{
    std::unique_lock<std::mutex> lock(g_mutex);

    while (!token.stop_requested())
    {
        const bool hasTask = g_cv.wait(lock, token, [] {
            return g_hasTask;
        });

        if (!hasTask)
        {
            break; // 因停止请求而返回
        }

        g_hasTask = false;
        lock.unlock();

        std::cout << "worker: 处理一次任务" << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(200));

        lock.lock();
    }

    std::cout << "worker: 已停止" << std::endl;
}

int main()
{
    std::jthread t(worker);

    {
        std::lock_guard<std::mutex> lock(g_mutex);
        g_hasTask = true;
    }
    g_cv.notify_one();

    std::this_thread::sleep_for(std::chrono::seconds(1));
    t.request_stop();

    return 0;
}

编译时启用 C++20:

bash 复制代码
g++ main.cpp -std=c++20 -pthread

6. 常见误区

6.1 把 notify 当成消息队列

条件变量只负责通知,不保存任务。任务队列或停止标志仍需要自己维护,并在互斥量保护下读写。

6.2 修改共享状态时不加锁

共享状态可能被多个线程访问,必须正确同步。通常先持锁修改状态,释放锁后再调用 notify_one() 或 notify_all()。

6.3 工作期间一直占用互斥量

从共享队列取出任务后,应尽快释放锁,再执行耗时工作,否则生产者和其他工作线程都会被阻塞。

6.4 认为 request_stop 会立即中断一切

request_stop 是协作式停止。它不能强制中断 sleep_for、普通条件变量等待、阻塞式 I/O 或第三方库中的长时间调用。遇到这些场景,需要使用支持取消的等待接口、设置超时,或由业务代码定期检查停止状态。

6.5 忘记 std::thread 的生命周期

std::thread 析构时如果仍然 joinable,会调用 std::terminate。使用 std::thread 时必须明确 join() 或 detach();通常不要随意 detach。C++20 项目可以优先考虑 std::jthread。

7. 如何选择

场景 推荐方案
C++11/14/17 项目 condition_variable + 停止标志 + join
C++20 及以上 jthread + stop_token
同时等待任务和停止 condition_variable_any::wait(lock, token, pred)
多个消费者只需唤醒一个 notify_one
所有等待者都要重新判断状态 notify_all

8. 一页速查

cpp 复制代码
// 唤醒一个等待线程
cv.notify_one();

// 唤醒所有等待线程
cv.notify_all();

// 带条件等待,避免虚假唤醒
cv.wait(lock, [] { return ready; });

// C++20 发出停止请求
thread.request_stop();

// 在线程内部检查停止状态
if (token.stop_requested()) {
    return;
}

9. 总结

线程控制的核心不是"从外部强行杀死线程",而是让线程能够在正确的时间醒来、看到共享状态,并在安全位置主动退出。

可以记住三句话:

  1. 状态必须有明确的存储位置,并正确同步。
  2. notify 只负责唤醒,真正的条件要由谓词判断。
  3. request_stop 只表达停止意图,线程需要配合退出。

后续将继续整理 call_once、scoped_lock、atomic::wait、future、promise 等平时不一定主动学习,但在工程中非常实用的 C++ 接口。

相关推荐
caoerzhong1 小时前
跨境海外仓怎么管:JeeWMS 开源 Java 仓库管理系统打通头程、海外仓与尾程
java·开发语言·开源
码云数智-大飞1 小时前
新手写 Python 代码,如何规范命名、减少 Bug
开发语言·python·php
福兮说2 小时前
JS 正则的六个坑:带 g 的 test() 一真一假、空匹配死循环、replace 里的 $
开发语言·前端·javascript·正则表达式
码艺-Alimjan2 小时前
Web 网站打包桌面应用的另一种方式,超级简单(C# exe 33Kb)
开发语言·前端·c#
沫璃染墨2 小时前
《从零入门Linux系统篇(五十一):线程篇·四——pthread线程库详解:从线程创建到终止与分离》
linux·运维·服务器·开发语言·c++·系统架构·线程
shada2 小时前
CMake指定 MSVC 工具集与 Windows SDK
c++·windows
垆边人似月.2 小时前
华为机考题:矩阵中的最长递增路径
c语言·c++
yexianglunbai2 小时前
中间件(Middleware)详解:概念、作用与实战
java·开发语言·中间件
叶总没有会2 小时前
OpenFeign
java·开发语言·springcloud·openfeign