C++ 异步任务取消机制:手动实现与协作式取消

各位观众,大家好!今天咱们来聊聊C++异步任务取消这档子事儿。这玩意儿听起来高大上,其实就是告诉你的程序:“喂,别忙活了,停下!咱换个活儿干!”

取消异步任务,是个非常现实的需求。想象一下:你打开一个网页,页面开始加载,结果网速慢得像蜗牛爬。你一怒之下点了刷新,或者干脆关了页面。如果程序不知道你已经取消了操作,还在吭哧吭哧地加载,那得多浪费资源啊!

C++标准本身并没有提供直接的、一劳永逸的异步任务取消机制。这就意味着,我们需要自己动手,丰衣足食。咱们今天就来看看两种常见的实现方式:手动实现和协作式取消。

第一种:手动实现,简单粗暴但有效

手动实现,顾名思义,就是你自己用一些标志位、条件变量等工具,来控制异步任务的生命周期。这方法简单直接,但也需要你对代码有足够的掌控力。

#include <iostream>
#include <thread>
#include <chrono>
#include <atomic>

// 模拟一个耗时任务
void long_running_task(std::atomic<bool>& stop_flag) {
  for (int i = 0; i < 100; ++i) {
    // 检查是否需要停止
    if (stop_flag.load()) {
      std::cout << "任务被取消!在迭代 " << i << " 处停止。n";
      return; // 提前结束函数
    }

    std::cout << "正在迭代 " << i << "...n";
    std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 模拟耗时操作
  }

  std::cout << "任务完成!n";
}

int main() {
  std::atomic<bool> stop_flag(false); // 初始状态:不停止

  // 创建一个线程来执行任务
  std::thread task_thread(long_running_task, std::ref(stop_flag));

  // 等待一段时间,然后取消任务
  std::this_thread::sleep_for(std::chrono::seconds(2));
  std::cout << "准备取消任务!n";
  stop_flag.store(true); // 设置停止标志

  // 等待线程结束
  task_thread.join();

  std::cout << "程序结束。n";
  return 0;
}

这段代码里,我们用了一个 std::atomic<bool> stop_flag 作为停止标志。异步任务 long_running_task 在每次迭代时都会检查这个标志,如果发现被设置为 true,就立即停止执行。

这种方法的优点是:

  • 简单易懂:代码逻辑清晰,容易理解和维护。
  • 控制力强:你可以精确地控制任务在何时何地停止。

缺点也很明显:

  • 侵入性强:需要在任务代码中插入检查停止标志的逻辑,这会增加代码的复杂性。
  • 容易出错:如果忘记在关键位置检查停止标志,任务可能无法被正确取消。
  • 忙等待:如果检查频率过高,会浪费CPU资源;如果检查频率过低,取消的响应速度会变慢。

第二种:协作式取消,优雅且灵活

协作式取消,顾名思义,就是让任务自己“意识到”需要停止,并主动退出。这种方法需要使用一些更高级的技巧,比如 std::futurestd::promise

#include <iostream>
#include <thread>
#include <future>
#include <chrono>

// 模拟一个耗时任务,使用 future 来检查是否取消
int long_running_task_with_future(std::future<void> future) {
  for (int i = 0; i < 100; ++i) {
    // 检查 future 是否准备好(是否被取消)
    if (future.wait_for(std::chrono::milliseconds(1)) == std::future_status::ready) {
      std::cout << "任务被取消!在迭代 " << i << " 处停止。n";
      return -1; // 返回一个错误代码
    }

    std::cout << "正在迭代 " << i << "...n";
    std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 模拟耗时操作
  }

  std::cout << "任务完成!n";
  return 0; // 返回一个成功代码
}

int main() {
  // 创建一个 promise 和 future
  std::promise<void> promise;
  std::future<void> future = promise.get_future();

  // 创建一个线程来执行任务,并将 future 传递给它
  std::thread task_thread(long_running_task_with_future, std::move(future));

  // 等待一段时间,然后取消任务
  std::this_thread::sleep_for(std::chrono::seconds(2));
  std::cout << "准备取消任务!n";
  promise.set_value(); // 设置 promise 的值,这将使 future 准备好

  // 等待线程结束
  task_thread.join();

  std::cout << "程序结束。n";
  return 0;
}

这段代码里,我们使用 std::promisestd::future 来进行任务取消。std::promise 就像一个承诺,它承诺在未来的某个时刻会提供一个值。std::future 则代表了对这个值的期望。

当我们需要取消任务时,只需要调用 promise.set_value(),这会使 future 准备好。异步任务可以通过 future.wait_for() 来检查 future 是否准备好,如果准备好了,就说明任务需要被取消了。

这种方法的优点是:

  • 解耦性好:取消操作和任务代码之间解耦,任务代码不需要直接操作停止标志。
  • 灵活性高:可以使用 std::future 的各种方法来检查取消状态,比如 wait_for()wait_until() 等。
  • 异常安全:即使在任务执行过程中抛出异常,也可以保证 future 被正确地设置,从而避免死锁。

缺点是:

  • 代码复杂:需要理解 std::promisestd::future 的概念,代码相对复杂。
  • 仍然需要侵入:需要在任务代码中插入检查 future 状态的逻辑。

更进一步:使用 std::stop_token (C++20)

C++20 引入了 std::stop_tokenstd::stop_source,它们提供了一种更简洁、更标准化的协作式取消机制。

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

// 模拟一个耗时任务,使用 stop_token 来检查是否取消
void long_running_task_with_stop_token(std::stop_token stop_token) {
  for (int i = 0; i < 100; ++i) {
    // 检查 stop_token 是否被请求停止
    if (stop_token.stop_requested()) {
      std::cout << "任务被取消!在迭代 " << i << " 处停止。n";
      return;
    }

    std::cout << "正在迭代 " << i << "...n";
    std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 模拟耗时操作
  }

  std::cout << "任务完成!n";
}

int main() {
  // 创建一个 stop_source
  std::stop_source stop_source;
  std::stop_token stop_token = stop_source.get_token();

  // 创建一个线程来执行任务,并将 stop_token 传递给它
  std::thread task_thread(long_running_task_with_stop_token, stop_token);

  // 等待一段时间,然后取消任务
  std::this_thread::sleep_for(std::chrono::seconds(2));
  std::cout << "准备取消任务!n";
  stop_source.request_stop(); // 请求停止

  // 等待线程结束
  task_thread.join();

  std::cout << "程序结束。n";
  return 0;
}

这段代码里,std::stop_source 用于发起停止请求,std::stop_token 用于检查是否被请求停止。任务通过 stop_token.stop_requested() 来检查是否需要停止。

std::stop_token 的优点是:

  • 标准化:C++标准库提供的,更规范,更容易被其他库和框架集成。
  • 简洁:代码更简洁,易于阅读和维护。
  • 可以注册回调函数:可以在 stop_token 上注册回调函数,当任务被取消时,自动执行一些清理操作。
  std::stop_source stop_source;
  std::stop_token stop_token = stop_source.get_token();

  // 注册一个回调函数,当任务被取消时执行
  std::stop_callback callback(stop_token, []() {
    std::cout << "任务取消回调函数被执行!n";
    // 在这里执行清理操作,比如释放资源
  });

  std::thread task_thread([&stop_token]() { long_running_task_with_stop_token(stop_token); });

总结:选择合适的取消方式

选择哪种取消方式,取决于你的具体需求和场景。

取消方式 优点 缺点 适用场景
手动实现 简单易懂,控制力强 侵入性强,容易出错,忙等待 简单的、对性能要求不高的任务,或者需要精确控制任务停止位置的任务
协作式取消(future 解耦性好,灵活性高,异常安全 代码复杂,仍然需要侵入 比较复杂的、需要解耦的任务,或者需要处理异常情况的任务
协作式取消(stop_token 标准化,简洁,可以注册回调函数 仍然需要侵入,需要C++20支持 需要使用C++20,且希望使用更规范、更简洁的取消机制的任务

总的来说,如果你的项目可以使用C++20,那么推荐使用 std::stop_token。如果不能使用C++20,可以考虑使用 std::future。如果任务非常简单,或者对性能要求不高,也可以使用手动实现。

一些额外的建议

  • 尽可能早地检查取消状态:在任务的关键位置,尽可能早地检查取消状态,以便尽快停止任务,释放资源。
  • 避免死锁:在取消任务时,要特别注意避免死锁。比如,不要在持有锁的情况下等待任务结束。
  • 清理资源:在任务被取消后,一定要清理资源,避免内存泄漏等问题。可以使用 RAII (Resource Acquisition Is Initialization) 技术来自动管理资源。
  • 考虑异常:在任务执行过程中,可能会抛出异常。要确保即使在抛出异常的情况下,也能正确地取消任务,并清理资源。

好了,今天的分享就到这里。希望大家对C++异步任务取消有了更深入的了解。记住,取消任务不是目的,而是手段。我们的目的是让程序更健壮、更高效、更用户友好。 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注