各位观众,大家好!今天咱们来聊聊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::future
和 std::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::promise
和 std::future
来进行任务取消。std::promise
就像一个承诺,它承诺在未来的某个时刻会提供一个值。std::future
则代表了对这个值的期望。
当我们需要取消任务时,只需要调用 promise.set_value()
,这会使 future
准备好。异步任务可以通过 future.wait_for()
来检查 future
是否准备好,如果准备好了,就说明任务需要被取消了。
这种方法的优点是:
- 解耦性好:取消操作和任务代码之间解耦,任务代码不需要直接操作停止标志。
- 灵活性高:可以使用
std::future
的各种方法来检查取消状态,比如wait_for()
、wait_until()
等。 - 异常安全:即使在任务执行过程中抛出异常,也可以保证
future
被正确地设置,从而避免死锁。
缺点是:
- 代码复杂:需要理解
std::promise
和std::future
的概念,代码相对复杂。 - 仍然需要侵入:需要在任务代码中插入检查
future
状态的逻辑。
更进一步:使用 std::stop_token
(C++20)
C++20 引入了 std::stop_token
和 std::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++异步任务取消有了更深入的了解。记住,取消任务不是目的,而是手段。我们的目的是让程序更健壮、更高效、更用户友好。 谢谢大家!