Bootstrap

优雅处理任务取消: C++20 的 Cooperative Cancellation

在现代软件开发中, 异步和并发操作是不可避免的. 然而, 当任务需要取消时, 如何优雅地终止任务, 释放资源并保持系统的稳定性, 往往是一个挑战. C++20 引入了 stop_token, stop_sourcestop_callback 三大工具, 为我们提供了灵活的协作式任务取消机制.

本文通过一个模拟文件下载的案例, 逐步讲解这三大工具的使用方法, 并探讨它们在任务取消中的实际应用.


任务场景: 文件下载服务

假设我们正在实现一个服务, 用于从远程服务器下载资源. 每个下载任务都可能因为以下原因中止:

  • 网络错误导致的连接中断;
  • 用户主动取消任务;
  • 超时机制触发.

为了满足以下需求, 我们需要一个可靠的任务取消机制:

  1. 实时检测取消信号;
  2. 安全终止任务执行;
  3. 在取消时释放资源, 避免资源泄漏.

接下来, 我们将逐步实现这一目标.


第一幕: 基础任务取消机制

C++20 提供的 stop_sourcestop_token 是任务取消的核心工具:

  • stop_source: 用于发出取消信号;
  • stop_token: 任务中检测取消信号的工具.

以下示例展示了一个简单的文件下载任务如何实现基础取消功能:

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

using namespace std::chrono_literals;

void download_file(std::stop_token stopToken) {
  int progress = 0;
  while (progress < 100) {
    if (stopToken.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. 传递 stop_token:
    • stop_token 是用于任务内部检测取消信号的工具.
  2. 任务中检查 stop_requested:
    • 使用 stop_requested() 方法可以随时响应取消请求, 确保任务安全终止.

第二幕: 取消任务时的资源清理

在某些场景中, 任务被取消时可能需要进行资源清理, 例如关闭文件句柄或通知其他模块.

C++20 提供的 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;
}

关键点解读

  1. 使用 stop_callback:
    • stop_callback 可以绑定一个回调函数, 在取消信号触发时自动执行.
  2. 确保资源释放:
    • 在回调中关闭文件, 避免任务取消后出现资源泄漏.

第三幕: 多任务的协作取消

在实际应用中, 可能存在多个并发任务需要协同取消. 例如, 一个下载服务中同时存在多个下载任务, 共享同一个 stop_source.

以下示例展示了如何通过共享 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;
}

关键点解读

  1. 共享 stop_source:
    • 多个任务可以共享同一个 stop_source, 实现统一的取消管理.
  2. 同时取消多个任务:
    • 调用 request_stop() 后, 所有关联的任务都会响应取消信号.

总结

C++20 提供的 stop_token, stop_sourcestop_callback 是处理任务取消的强大工具. 在异步和并发编程中, 它们可以:

  • 优雅地管理任务生命周期;
  • 避免资源泄漏;
  • 提升系统稳定性和可维护性.

这些工具特别适合高并发服务, 实时系统和复杂任务的资源管理场景. 通过本文的示例, 希望你能更好地理解和应用 C++20 的协作取消机制, 为开发更可靠的应用奠定基础.

源码链接

源码链接

;