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

2030

积分

0

好友

266

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

在C#的后端开发中,一个常见且关键的场景是:如何正确并发执行多个任务,并统一等待它们完成?这个问题直接关系到API的性能和响应速度。本章将深入探讨C#/.Net中实现这一目标的核心模式。

本章内容围绕三个紧密关联的知识点展开:

  1. Task什么时候开始执行
  2. 并发启动任务的正确方式
  3. Task.WhenAll任务组合

这三个知识点构成了一条完整的逻辑链:

任务什么时候启动
      ↓
如何同时启动多个任务
      ↓
如何优雅等待它们完成

任务并发与组合

在真实工程中,我们经常需要同时发起多个独立的异步操作,并等待它们全部完成后再进行后续处理。

典型场景包括

  • 同时加载用户信息、订单列表和账户余额。
  • 并发调用多个下游微服务接口以聚合数据。

如果代码写得不好,很容易将本可以并发执行的操作变成串行执行,导致总耗时急剧增加。

示例:
三个操作各需100毫秒
串行执行总耗时 ≈ 300毫秒
并发执行总耗时 ≈ 100毫秒

Task什么时候开始执行?

许多初学者有一个误解:认为await关键字才是启动异步任务的信号。

实际上,调用异步方法的那一刻,任务就已经开始执行了

看这段代码:

Task t = Task.Delay(2000);

它的执行流程是:

  1. 创建一个Task对象。
  2. 内部的定时器立即开始计时。
  3. 2秒后,该Task自动完成。

关键点在于:await并不会启动任务,它的作用仅仅是等待一个已经在执行中的任务完成

示例分析

async Task Test()
{
    Console.WriteLine(“A”);
    var t = Task.Delay(2000);
    Console.WriteLine(“B”);
    await t;
    Console.WriteLine(“C”);
}

这个方法的执行顺序和输出是:

A
B
(等待约2秒)
C

这清晰地证明了Task.Delay(2000)在创建时(var t = ...这一行)就已经启动了,而不是在await t时才启动。

如何正确并发启动任务?

理解了任务的启动时机,我们来看一个常见的错误写法。

错误示例(串行执行)

async Task Test()
{
    await Task.Delay(1000);
    await Task.Delay(1000);
    await Task.Delay(1000);
}

这段代码的总执行时间大约是3秒。它的执行模型是线性的:

第一个Delay完成
↓
第二个Delay开始并完成
↓
第三个Delay开始并完成

这不是并发,而是串行

正确并发方式

async Task Test()
{
    var t1 = Task.Delay(1000);
    var t2 = Task.Delay(1000);
    var t3 = Task.Delay(1000);

    await t1;
    await t2;
    await t3;
}

这段代码的执行模型是:

t1 ┐
t2 ├ 三者在同一时刻同时开始
t3 ┘

总执行时间大约是1秒。原因很简单:三个任务在创建时都已启动,后续的await只是在分别等待它们各自完成。

Task.WhenAll:优雅的任务组合

上面的写法虽然实现了并发,但代码显得不够优雅,尤其是当任务数量较多时,需要写很多行await

为此,.NET提供了Task.WhenAll方法,它的作用就是等待传入的所有任务完成

基础用法示例

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

    await Task.WhenAll(t1, t2, t3);

    Console.WriteLine(“全部完成”);
}

这段代码的总执行时间大约是3秒,因为Task.WhenAll会等待所有任务中最慢的那个(这里是t2,3秒)完成。

处理带返回值的任务

如果任务有返回值,Task.WhenAll同样适用,并且会以一个数组的形式返回所有任务的结果,顺序与传入的任务顺序一致。

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

int[] results = await Task.WhenAll(t1, t2, t3);
// results[0] 对应 t1 的结果
// results[1] 对应 t2 的结果
// results[2] 对应 t3 的结果

一个真实的工程例子

假设我们需要为用户首页加载三部分数据:用户信息、订单列表和账户余额。

低效的串行写法

var user = await GetUser();      // 假设耗时 100ms
var order = await GetOrders();   // 假设耗时 100ms
var balance = await GetBalance();// 假设耗时 100ms
// 总耗时 ≈ 100ms + 100ms + 100ms = 300ms

优化后的并发写法

var userTask = GetUser();      // 调用即开始,不等待
var orderTask = GetOrders();   // 调用即开始,不等待
var balanceTask = GetBalance();// 调用即开始,不等待

await Task.WhenAll(userTask, orderTask, balanceTask);

var user = userTask.Result;
var order = orderTask.Result;
var balance = balanceTask.Result;
// 总耗时 ≈ max(100ms, 100ms, 100ms) = 100ms

通过并发执行,我们将总响应时间从300毫秒降低到了100毫秒,提升了三倍!这是构建高性能API聚合服务的标准写法

本章核心模式总结

本章建立了一个至关重要且常用的异步编程模式:

启动所有任务
↓
让它们并发执行
↓
统一等待所有任务完成

其代码模板非常清晰:

// 1. 启动所有任务
var task1 = AsyncOperation1();
var task2 = AsyncOperation2();
var task3 = AsyncOperation3();

// 2. 统一等待
await Task.WhenAll(task1, task2, task3);

// 3. 处理结果

这个模式在服务器端开发中极其常见,是优化I/O密集型操作性能的关键手段。通过在云栈社区与其他开发者交流,你可以发现更多关于后端 & 架构中高并发处理的实战技巧和最佳实践。


练习与思考

通过练习来巩固理解:

练习1

async Task Test()
{
    var t1 = Task.Delay(2000);
    await Task.Delay(2000);
    await t1;
}

问: 这个方法的总执行时间大约是多少?为什么?

分析:

  • t=0: 创建t1,一个2秒的延时任务立即开始
  • t=0: 执行await Task.Delay(2000),另一个2秒的延时任务立即开始
  • t=2: 两个延时任务都已完成。
  • await t1会立即返回,因为t1在第2秒时已经完成。
  • 总时间 ≈ 2秒。两个任务是并发执行的。

练习2

async Task Test()
{
    var t1 = Task.Delay(2000);
    var t2 = Task.Delay(4000);

    await t1;
    Console.WriteLine(“A”);
    await t2;
    Console.WriteLine(“B”);
}

问: AB分别会在什么时候打印?

分析:

  • t=0: t1(2秒)和t2(4秒)同时开始。
  • t=2: t1完成,await t1返回,立即打印A。此时t2已经执行了2秒,还剩2秒。
  • t=4: t2完成,await t2返回,打印B
  • 关键点: 在等待和打印A的期间,t2并没有停止,它一直在后台并发执行。

练习3(深入理解并发时机)

问: 下面这行代码能实现真正的并发吗?任务是在什么时候开始执行的?

await Task.WhenAll(
    GetData1(),
    GetData2(),
    GetData3()
);

分析:

  • 执行过程:
    1. 调用GetData1()Task1开始执行
    2. 调用GetData2()Task2开始执行
    3. 调用GetData3()Task3开始执行
    4. Task.WhenAll接收这三个已经启动的任务,并等待它们全部完成。
  • 结论: 并发发生在调用各个方法的时候,而不是在WhenAll内部。WhenAll只是一个“等待器”。



上一篇:WinPE系统完全指南:不止重装,10大高级运维功能详解
下一篇:C#异步编程核心:async方法的返回类型详解与最佳实践
您需要登录后才可以回帖 登录 | 立即注册

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

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

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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