找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2038

积分

0

好友

268

主题
发表于 2 小时前 | 查看: 2| 回复: 0

在C#的异步编程实践中,我们常常会遇到这样两类需求:

  • 我有多个任务在同时执行,但不想等待所有任务都完成,而是谁先完成就用谁的结果,该怎么办?
  • 任务执行太久怎么办?如何给它设定一个时间限制?

这正是任务竞速与取消控制要解决的问题。本篇将带你深入理解三个紧密关联的核心知识点:Task.WhenAny、超时控制与 CancellationToken

1. Task.WhenAny:等待任意任务完成

Task.WhenAny 方法的核心作用是:在一组并发的任务中,等待任意一个任务率先完成。

来看一个简单的例子:

var t1 = Task.Delay(3000);
var t2 = Task.Delay(1000);
var t3 = Task.Delay(2000);

Task finished = await Task.WhenAny(t1, t2, t3);

Console.WriteLine(“一个任务完成”);

这段代码的执行模型是这样的:

t1 ┐
t2 ├ 同时执行
t3 ┘

1秒 → t2完成
WhenAny返回

因此,代码大约在 1秒 后就会打印“一个任务完成”,而不需要等待3秒。

WhenAny 到底返回了什么?

一个关键的细节是:WhenAny 返回的是 最先完成的那个 Task 对象本身,而不是该任务的结果。

例如:

Task<int> t1 = GetData(1);
Task<int> t2 = GetData(2);

Task<int> first = await Task.WhenAny(t1, t2);

此时,first 变量中存储的是 t1t2 中先完成的那一个 Task<int> 对象。为了拿到这个任务的计算结果,我们还需要再对它进行一次 await

int result = await first; // 获取最先完成任务的结果

为什么需要这一步?因为 WhenAny 的返回类型实际上是 Task<Task<T>>,它只负责告诉你“哪个任务完成了”,但不会自动替你取出那个任务的结果。

完整执行流程与注意事项

假设有三个任务,执行时间分别为3秒、1秒和2秒:

0s   s1开始
0s   s2开始
0s   s3开始

1s   s2完成
WhenAny返回 s2对应的Task对象

await first // 这里获取s2的结果
返回结果

这里有一个非常重要的点:WhenAny 返回时,只有最先完成的任务结束了,其他任务(s1 和 s3)仍然在后台继续执行,除非你主动取消它们。

工程实战:服务器竞速(Racing Requests)

一个经典的应用场景是从多个数据源(如不同的CDN服务器)获取同一份数据,谁先返回就用谁,以提升响应速度。

var s1 = GetFromServer1();
var s2 = GetFromServer2();
var s3 = GetFromServer3();

var first = await Task.WhenAny(s1, s2, s3);
var result = await first; // 使用最快服务器的数据
// 这里可以视情况取消其他仍在进行的请求

这种模式在构建高并发、低延迟的应用时非常有用,特别是在 ASP.NET Core 这类需要快速响应的Web框架中。

2. 超时控制

“给一个异步操作设定执行时间上限”是工程中极其常见的需求。利用 Task.WhenAny 可以很优雅地实现它。

核心思想是:将你的工作任务和一个作为“计时器”的 Task.Delay 任务进行竞速。

var workTask = DoWork();
var timeoutTask = Task.Delay(2000); // 设定2秒超时

var finished = await Task.WhenAny(workTask, timeoutTask);

if (finished == timeoutTask)
{
    Console.WriteLine(“操作超时”);
    // 处理超时逻辑
}
else
{
    Console.WriteLine(“操作完成”);
    var result = await workTask; // 获取工作结果
}

执行模型如下:

DoWork (实际工作时间未知)
Delay(2000) (固定2秒)

两者竞速:
- 若DoWork在2秒内完成,则workTask先完成,进入else分支。
- 若2秒已到但DoWork未完成,则timeoutTask先完成,进入超时分支。

3. CancellationToken:主动取消任务

有时仅仅检测到超时还不够,我们可能希望主动取消那个仍在执行的任务,以释放资源。这就需要用到 .NET 提供的 CancellationToken 机制。

基本用法如下:

var cts = new CancellationTokenSource(); // 创建取消令牌源
var task = DoWork(cts.Token); // 将令牌传递给任务

// ... 在某个时刻(如超时后)决定取消
cts.Cancel(); // 发出取消信号

任务 DoWork 的内部需要协作,定期检查取消令牌:

async Task DoWork(CancellationToken token)
{
    for(int i=0; i<10; i++)
    {
        // 每次循环前检查,如果已取消则抛出异常
        token.ThrowIfCancellationRequested();
        await Task.Delay(1000); // 模拟工作单元
    }
}

当外部调用 cts.Cancel() 时,token.ThrowIfCancellationRequested() 会抛出 OperationCanceledException,从而使任务进入取消状态。

整合超时与取消的完整模式

结合 WhenAnyDelayCancellationToken,我们可以构建一个健壮的、带超时且能主动取消的异步调用模式:

async Task<string> CallApiWithTimeout()
{
    var cts = new CancellationTokenSource();

    var workTask = CallApi(cts.Token); // 传入取消令牌
    var timeoutTask = Task.Delay(2000);

    var finished = await Task.WhenAny(workTask, timeoutTask);

    if (finished == timeoutTask)
    {
        cts.Cancel(); // 超时,主动取消API调用
        return “timeout”;
    }

    // workTask 先完成,返回结果
    return await workTask;
}

知识梳理与实践

现在,你掌握了 async/await 模型中的两组核心“等待”工具:

  • Task.WhenAll:等待所有任务完成。
  • Task.WhenAny:等待任意一个任务完成。

再加上 Task.Delay(用于超时)和 CancellationToken(用于取消),你已经能够组合出应对各种复杂异步场景的工程模式。

思考与实践

练习1:分析执行过程

var work = Task.Delay(3000);
var timeout = Task.Delay(1000);

await Task.WhenAny(work, timeout);
Console.WriteLine(“done”);

请问:

  1. “done” 何时被打印?(答案:大约1秒后)
  2. work 任务还会继续执行吗?(答案:WhenAny 返回并不取消其他任务。)

练习2:实现超时控制
如何用 async 方法调用一个API,并设定最多等待2秒,超时则返回 ”timeout”?要求不阻塞线程。
(答案参考上文整合的 CallApiWithTimeout 方法)

通过本章的学习,你不仅理解了 Task.WhenAnyCancellationToken 的机制,更重要的是掌握了将它们组合应用来解决实际开发问题的能力。这正是构建高效、可靠的后端服务与分布式系统所必需的核心技能之一。如果想深入探讨更多异步编程或 C# 的高级话题,欢迎来到 云栈社区C#/.Net板块 交流分享。




上一篇:C#异步编程核心:async方法的返回类型详解与最佳实践
下一篇:谷歌Gemini深度集成Workspace:表格处理效率提升9倍,办公模式迎来变革
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-15 05:15 , Processed in 0.505597 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表