C++20 std::jthread 完全指南 - 简化多线程编程与线程管理

你是否在用 C++ 编写多线程程序时因忘记调用 join/detach 而崩溃? C++20 引入了一个新的多线程管理工具 std::jthread, 这是对 C++11 中 std::thread 的重要改进. 相比于 std::thread, std::jthread 提供了更安全和更简洁的接口, 是一个典型的 RAII(资源获取即初始化)对象.

在本篇博客中, 我们将详细解析 std::jthread 的功能, 包括其优点, 特性, 以及如何在实际项目中应用, 帮助你更高效地管理多线程程序.

为什么需要 jthread?

std::thread 的局限

C++11 中引入的 std::thread 虽然让多线程编程变得更加便捷, 但也带来了显著的风险:

以下是一个使用 std::thread 的简单例子:

#include <iostream>
#include <syncstream>
#include <thread>

void task(int i) {
  std::osyncstream(std::cout) << "Task " << i << " done" << std::endl;
}

int main() {
  std::thread t1(task, 1);
  std::thread t2(task, 2);

  return 0;
}

运行之后会看到异常发生:

terminate called without an active exception
已中止 (核心已转储)

这是因为程序中没有调用 joindetach, 将导致未定义行为.

std::jthread 的优势

C++20 引入的 std::jthreadstd::thread 的增强版本, 具有以下显著优势:

  1. 自动调用 join: std::jthread 是一个 RAII 对象, 在作用域结束时会自动调用 join 方法, 极大降低了因疏忽导致的程序崩溃风险.
  2. 线程停止功能: 内置 request_stopstop_token, 提供了一种优雅的线程停止机制.

接下来, 我们通过具体示例深入了解这些特性.

基本用法: 自动管理线程生命周期

我们将上面例子中的std::thread换为std::jthread试试:

#include <iostream>
#include <syncstream>
#include <thread>

void task(int i) {
  std::osyncstream(std::cout) << "Task " << i << " done" << std::endl;
}

int main() {
  std::jthread t1(task, 1);
  std::jthread t2(task, 2);

  return 0;
}

输出示例:

Task 1 done
Task 2 done

只需要把thread改为jthread然后重新编译运行, 问题就解决了, 就是这么简单.

优雅的线程停止机制

std::jthread 引入了 request_stopstop_token, 使得线程停止逻辑更简单.

下面的例子模拟了文件下载的场景, 线程可以在中途被停止.

#include <chrono>
#include <iostream>
#include <stop_token>
#include <thread>

using namespace std::chrono_literals;

void download_file(std::stop_token token) {
  int progress = 0;
  while (progress < 100) {
    if (token.stop_requested()) {
      std::cout << "[下载任务] 检测到取消请求, 停止下载.\n";
      return;
    }

    std::this_thread::sleep_for(200ms);
    progress += 1;
    std::cout << "[下载任务] 当前进度: " << progress << "%\n";
  }
  std::cout << "[下载任务] 下载完成!\n";
}

int main() {
  std::jthread downloader(download_file);

  std::this_thread::sleep_for(1s);

  std::cout << "[主线程] 取消下载任务.\n";
  downloader.request_stop();  // 发出取消信号

  return 0;
}

输出示例:

[下载任务] 当前进度: 1%
[下载任务] 当前进度: 2%
[下载任务] 当前进度: 3%
[下载任务] 当前进度: 4%
[主线程] 取消下载任务.
[下载任务] 当前进度: 5%
[下载任务] 检测到取消请求, 停止下载.

停止回调机制

在某些场景中, 我们希望在线程停止时执行特定的清理操作, 比如关闭文件或者释放资源. std::jthread 提供了 std::stop_callback, 可轻松实现这一功能.

#include <chrono>
#include <fstream>
#include <iostream>
#include <stop_token>
#include <thread>

using namespace std::chrono_literals;

void download_file_with_cleanup(std::stop_token token) {
  std::ofstream file("output.txt", std::ios::out);

  // 定义回调函数: 取消任务时关闭文件
  std::stop_callback callback(token, [&file] {
    std::cout << "[下载任务]-[回调] 检测到取消请求, 关闭文件句柄.\n";
    file.close();
  });

  int progress = 0;
  while (progress < 100) {
    if (token.stop_requested()) {
      std::cout << "[下载任务] 检测到取消请求, 停止下载.\n";
      return;
    }

    // 写入到文件
    file << "下载进度: " << progress << "%\n";

    std::cout << "[下载任务] 当前进度: " << progress << "%\n";
    std::this_thread::sleep_for(200ms);
    progress += 1;
  }

  file.close();
  std::cout << "[下载任务] 下载完成, 文件已保存.\n";
}

int main() {
  std::jthread downloader(download_file_with_cleanup);

  std::this_thread::sleep_for(1s);

  std::cout << "[主线程] 取消下载任务.\n";
  downloader.request_stop();  // 发出取消信号

  return 0;
}

输出示例:

[下载任务] 当前进度: 0%
[下载任务] 当前进度: 1%
[下载任务] 当前进度: 2%
[下载任务] 当前进度: 3%
[下载任务] 当前进度: 4%
[主线程] 取消下载任务.
[下载任务]-[回调] 检测到取消请求, 关闭文件句柄.
[下载任务] 检测到取消请求, 停止下载.

控制多个线程停止: stop_source

std::stop_sourcestd::stop_token 的结合使得可以同时控制多个线程的停止. 以下示例展示了如何使用 stop_source 来管理多个下载任务:

#include <chrono>
#include <iostream>
#include <stop_token>
#include <syncstream>
#include <thread>
#include <vector>

using namespace std::chrono_literals;

void download_task(std::stop_token token, int id) {
  int progress = 0;
  while (progress < 100) {
    if (token.stop_requested()) {
      std::osyncstream(std::cout)
          << "[任务 " << id << "] 检测到取消请求, 停止下载.\n";
      return;
    }

    std::this_thread::sleep_for(200ms);
    progress += 10;
    std::osyncstream(std::cout)
        << "[任务 " << id << "] 当前进度: " << progress << "%\n";
  }

  std::osyncstream(std::cout) << "[任务 " << id << "] 下载完成!\n";
}

int main() {
  // 控制多个线程的退出
  std::stop_source source;

  std::vector<std::jthread> tasks;
  for (int i = 0; i < 3; i++) {
    tasks.emplace_back(download_task, source.get_token(), i);
  }

  std::this_thread::sleep_for(1s);
  std::cout << "[主线程] 取消所有任务.\n";
  source.request_stop();  // 发出取消信号

  return 0;
}

总结

std::jthread 是 C++20 提供的一个强大工具, 它通过 RAII 和内置的线程停止功能, 大大简化了多线程编程的复杂性. 通过使用 std::jthread, 你可以编写更简洁, 更安全的代码, 避免常见的线程管理陷阱.

在未来的项目中, 如果你需要多线程支持, 不妨尝试使用 std::jthread, 体验 C++20 带来的便捷与高效!