You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#10中Task集合并发/顺序执行的安全规范实现方案

问题背景

现有Task集合需要支持并发执行、顺序执行两种运行模式,运行环境为C# 10 + ASP.NET Core,执行频率约每秒1次,单批次处理约10个任务,性能非首要优先级但需避免无意义开销。
现有并发执行参考实现如下:

public Task Foo(CancellationToken token)
{
  // ...
  IEnumerable<Task> tasks = GetTasks(token);
  // ...
  return Task.WhenAll(tasks);
}

目前待确认顺序执行的正确实现,现有3种候选写法如下:

public Task Bar(CancellationToken token)      // 设计上有意不添加`async`关键字
{
  // ...
  IEnumerable<Task> tasks = GetTasks(token);
  // ...

  // (1)
  return Task.Run(async () => {
    foreach (var task in tasks)
      await task.WaitAsync(token);
  }, token);

  // (2)
  return Task.Run(async () => {
    foreach (var task in tasks)
      await Task.Run(() => task, token);
  }, token);

  // (3)
  foreach (var task in tasks)
    yield return Task.Run(() => task, token);

  // (?)
}
候选方案缺陷分析

3种候选写法均存在明显问题,不符合安全、符合现代C#范式的要求:

  • 方案(1):外层Task.Run属于完全不必要的线程池调度——ASP.NET Core本身就使用线程池线程处理请求,额外把逻辑丢到线程池只会平白增加线程切换开销,没有任何收益。更核心的问题是:如果GetTasks()返回的是提前创建好的“热任务”,在遍历到对应任务之前这些任务就已经启动并发运行了,根本达不到顺序执行的效果。
  • 方案(2):嵌套的Task.Run(() => task)是无意义的包装,既不会改变原有任务的启动时机,还额外增加了线程池工作项排队开销,同样存在热任务提前并发运行的核心问题。
  • 方案(3):首先写法本身存在编译错误——方法签名返回值为Task,yield return要求方法返回IEnumerable/IAsyncEnumerable类型,无法通过编译。即使修正返回值类型,依然没有解决热任务提前启动的问题,无法保证真正的顺序执行。

另外补充:刻意不给Bar方法添加async关键字属于无意义的微优化,C# 10中async/await的异步状态机生成开销极低,在你当前的业务量级下完全不会产生可感知的性能影响,刻意避开async反而会大幅提升代码复杂度、提高出错概率。

正确实现方案

要实现真正的严格顺序执行,核心前提是不能让任务在被调度前就提前启动,这也是GetTasks()实现的核心要求。

GetTasks()的正确实现

GetTasks()不应该直接返回已经实例化的Task集合——Task创建后默认就是已启动的热任务,只要提前创建就会自动并发运行,后续再按顺序await只是按顺序等待结果,无法控制执行顺序。
推荐将GetTasks()改为返回任务工厂委托集合,只有调用委托时才会创建并启动对应任务,从根源上避免任务提前启动:

// 推荐签名:返回延迟执行的任务工厂,而非已启动的Task
private IEnumerable<Func<CancellationToken, Task>> GetTaskFactories()
{
    // 每个元素是未执行的异步委托,调用时才会启动对应任务
    yield return async (ct) => await DoStep1WorkAsync(ct);
    yield return async (ct) => await DoStep2WorkAsync(ct);
    yield return async (ct) => await DoStep3WorkAsync(ct);
    // 其余任务按顺序返回
}

如果因为历史兼容原因无法修改返回类型,必须返回IEnumerable<Task>,则GetTasks()必须使用yield return做延迟生成,绝对不能提前用ToList()/ToArray()等方法把所有任务实例化:

// 兼容旧逻辑的GetTasks实现
private IEnumerable<Task> GetTasks(CancellationToken token)
{
    // 遍历到对应元素时才会创建启动任务,禁止提前集中实例化
    yield return DoStep1WorkAsync(token);
    yield return DoStep2WorkAsync(token);
    yield return DoStep3WorkAsync(token);
}

顺序执行逻辑实现

基于任务工厂的实现最为简洁安全,不需要任何多余的Task.Run包装:

public async Task Bar(CancellationToken token)
{
    var taskFactories = GetTaskFactories();
    foreach (var factory in taskFactories)
    {
        // 上一个任务await完成后,才会调用下一个工厂启动新任务,实现严格顺序执行
        await factory(token).WaitAsync(token);
    }
}

如果使用兼容版IEnumerable<Task>返回值的GetTasks,顺序实现如下:

public async Task Bar(CancellationToken token)
{
    var tasks = GetTasks(token);
    foreach (var task in tasks)
    {
        await task.WaitAsync(token);
    }
}

对应调整后的并发执行实现也很简单,只需要先把所有工厂调用一遍启动所有任务,再统一等待即可:

public async Task Foo(CancellationToken token)
{
    var taskFactories = GetTaskFactories();
    var runningTasks = taskFactories.Select(f => f(token)).ToList();
    await Task.WhenAll(runningTasks);
}

注意:ASP.NET Core没有传统ASP.NET的同步上下文,直接await不会产生死锁问题,完全不需要用Task.Run包装执行逻辑。

内容的提问来源于stack exchange,提问作者lonix

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 06:27:26