C#异步编程async与await

C#异步编程async与await

概念

asyncawait 并非直接创建新线程 ,而是编译器层面的状态机转换。当一个方法被标记为 async 时,编译器会将其重构为一个状态机。当执行流遇到 await 关键字时,如果等待的任务尚未完成,当前方法会被挂起,控制权立即交还给调用者。一旦后台任务完成,状态机会恢复上下文并继续执行后续代码。它不创建新线程,而是利用操作系统的异步 I/O 机制(如 IOCP)实现非阻塞等待

核心原则

  • asyncawait 是为了简化异步代码编写的语法糖,而不是强制约束
  • 只要方法返回 TaskTask<T>,它就是"可等待的"(Awaitable)

例如File.ReadAllBytesAsync 相关方法就是直接返回的Task,但是它调用的底层方法是标记了asyncawait

使用场景

IO密集型

网络请求、数据库查询、文件读写,直接使用 asyncawait,无需手动分配新线程。底层操作系统会通过 I/O 完成端口(IOCP)等机制处理等待,从而释放当前线程去处理其他请求。

c# 复制代码
static async Task Main(string[] args)
{
    Console.WriteLine("begin threadId={0}, time={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("O"));
    // 并行执行,总时间 = Max(T1, T2, T3, T4)
    Task<string> task1 = TestIO1Async();
    Task<string> task2 = TestIO2Async();
    Task<string> task3 = TestIO3Async();
    Task<string> task4 = TestIO4Async();

    // 同时等待所以任务完成
    string[] result = await Task.WhenAll(task1, task2, task3, task4).ConfigureAwait(false); ;
    Console.WriteLine("all done threadId={0}, time={1}, result={2}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("O"), result);
}


public async static Task<string> TestIO1Async()
{
    await Task.Delay(1000);
    Console.WriteLine("TestIO1Async done threadId={0}, time={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("O"));
    return "TestIO1Async";

}

public async static Task<string> TestIO2Async()
{
    await Task.Delay(2000);
    Console.WriteLine("TestIO2Async done threadId={0}, time={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("O"));
    return "TestIO2Async";
}

public async static Task<string> TestIO3Async()
{
    await Task.Delay(3000);
    Console.WriteLine("TestIO3Async done threadId={0}, time={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("O"));
    return "TestIO3Async";
}

public async static Task<string> TestIO4Async()
{
    await Task.Delay(4000);
    Console.WriteLine("TestIO4Async done threadId={0}, time={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("O"));
    return "TestIO4Async";
}

CPU密集型

这类操作需要 CPU 持续进行高强度的数学运算,没有任何 I/O 等待可言。强行放一起导致串行执行,消耗时间是累计的

最佳实践:使用 Task.Run 将计算卸载到后台线程。

举个反例

这个例子还不如串行直接调用,纯CPU计算方法加 async,没有 await,并不会让它变成异步执行

c# 复制代码
static async Task Main(string[] args)
{
    long begin = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    Console.WriteLine("begin threadId={0}, time={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("O"));
    // 并行执行,总时间 = T1 + T2 + T3 + T4
    Task<string> task1 = TestCpu1Async();
    Task<string> task2 = TestCpu2Async();
    Task<string> task3 = TestCpu3Async();
    Task<string> task4 = TestCpu4Async();

    // 同时等待所以任务完成
    string[] result = await Task.WhenAll(task1, task2, task3, task4).ConfigureAwait(false); ;
    long end = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    Console.WriteLine("all done threadId={0}, time={1}, result={2}", Thread.CurrentThread.ManagedThreadId, (end - begin) / 1000.0, result);
}


public async static Task<string> TestCpu1Async()
{
    long begin = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    CalculatePi(10000000);
    //await Task.Delay(1);
    long end = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    Console.WriteLine("TestCpu1Async done threadId={0}, time={1}", Thread.CurrentThread.ManagedThreadId, (end - begin) / 1000.0);
    return "TestCpu1Async";

}

public async static Task<string> TestCpu2Async()
{
    long begin = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    CalculatePi(20000000);
    //await Task.Delay(1);
    long end = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    Console.WriteLine("TestCpu2Async done threadId={0}, time={1}", Thread.CurrentThread.ManagedThreadId, (end - begin) / 1000.0);
    return "TestCpu2Async";
}

public async static Task<string> TestCpu3Async()
{
    long begin = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    CalculatePi(40000000);
    //await Task.Delay(1);
    long end = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    Console.WriteLine("TestCpu3Async done threadId={0}, time={1}", Thread.CurrentThread.ManagedThreadId, (end - begin) / 1000.0);
    return "TestCpu3Async";
}

public async static Task<string> TestCpu4Async()
{
    long begin = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    CalculatePi(90000000);
    //await Task.Delay(1);
    long end = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    Console.WriteLine("TestCpu4Async done threadId={0}, time={1}", Thread.CurrentThread.ManagedThreadId, (end - begin) / 1000.0);
    return "TestCpu4Async";
}

/// <summary>
/// 使用蒙特卡洛方法估算 PI
/// 这是一个纯 CPU 密集型操作,会持续占用当前线程
/// </summary>
/// <param name="iterations">采样点数,越大越准,耗时越长</param>
/// <returns>估算的 PI 值</returns>
public static double CalculatePi(int iterations)
{
    int insideCircle = 0;
    var random = new Random();

    // 简单的循环,完全占用 CPU
    for (int i = 0; i < iterations; i++)
    {
        // 生成 -1 到 1 之间的随机坐标
        double x = random.NextDouble() * 2 - 1;
        double y = random.NextDouble() * 2 - 1;

        // 判断是否在单位圆内 (x + y <= 1)
        if (x * x + y * y <= 1)
        {
            insideCircle++;
        }
    }

    // PI ≈ 4 * (圆内点数 / 总点数)
    return 4.0 * insideCircle / iterations;
}

举个正例

c# 复制代码
static async Task Main(string[] args)
{
    long begin = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    Console.WriteLine("begin threadId={0}, time={1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("O"));
    // 并行执行,总时间 = Max(T1, T2, T3, T4)
    Task<string> task1 = TestCpu1Async();
    Task<string> task2 = TestCpu2Async();
    Task<string> task3 = TestCpu3Async();
    Task<string> task4 = TestCpu4Async();

    // 同时等待所以任务完成
    string[] result = await Task.WhenAll(task1, task2, task3, task4).ConfigureAwait(false); ;
    long end = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    Console.WriteLine("all done threadId={0}, time={1}, result={2}", Thread.CurrentThread.ManagedThreadId, (end - begin) / 1000.0, result);
}


public async static Task<string> TestCpu1Async()
{
    return await Task.Run(() =>
    {
        long begin = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
        CalculatePi(10000000);
        long end = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
        Console.WriteLine("TestCpu1Async done threadId={0}, time={1}", Thread.CurrentThread.ManagedThreadId, (end - begin) / 1000.0);
        return "TestCpu1Async";
    });
}

public async static Task<string> TestCpu2Async()
{
    return await Task.Run(() =>
    {
        long begin = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
        CalculatePi(20000000);
        long end = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
        Console.WriteLine("TestCpu2Async done threadId={0}, time={1}", Thread.CurrentThread.ManagedThreadId, (end - begin) / 1000.0);
        return "TestCpu2Async";
    });
}

public async static Task<string> TestCpu3Async()
{
    return await Task.Run(() =>
    {
        long begin = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
        CalculatePi(40000000);
        long end = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
        Console.WriteLine("TestCpu3Async done threadId={0}, time={1}", Thread.CurrentThread.ManagedThreadId, (end - begin) / 1000.0);
        return "TestCpu3Async";
    });
}

public async static Task<string> TestCpu4Async()
{
    return await Task.Run(() =>
    {
        long begin = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
        CalculatePi(90000000);
        long end = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
        Console.WriteLine("TestCpu4Async done threadId={0}, time={1}", Thread.CurrentThread.ManagedThreadId, (end - begin) / 1000.0);
        return "TestCpu4Async";
    });

}

/// <summary>
/// 使用蒙特卡洛方法估算 PI
/// 这是一个纯 CPU 密集型操作,会持续占用当前线程
/// </summary>
/// <param name="iterations">采样点数,越大越准,耗时越长</param>
/// <returns>估算的 PI 值</returns>
public static double CalculatePi(int iterations)
{
    int insideCircle = 0;
    var random = new Random();

    // 简单的循环,完全占用 CPU
    for (int i = 0; i < iterations; i++)
    {
        // 生成 -1 到 1 之间的随机坐标
        double x = random.NextDouble() * 2 - 1;
        double y = random.NextDouble() * 2 - 1;

        // 判断是否在单位圆内 (x + y <= 1)
        if (x * x + y * y <= 1)
        {
            insideCircle++;
        }
    }

    // PI ≈ 4 * (圆内点数 / 总点数)
    return 4.0 * insideCircle / iterations;
}

避坑操作

  • 永远不要在服务端滥用 Task.Run:在 ASP.NET Core 等 Web 应用中,请求处理线程本身就是为了快速流转而设计的。用 Task.Run 处理 Web 请求不仅无法提升吞吐量,反而会增加线程切换的开销。
  • 警惕死锁陷阱:在 UI 线程或旧版 ASP.NET 环境中,绝对不要使用 .Result.Wait() 来同步阻塞异步任务。这极易导致死锁。请始终使用 await。
  • 库代码中的 ConfigureAwait(false):如果你在编写供他人调用的底层类库,且不需要恢复原始上下文,建议在 await 后加上 .ConfigureAwait(false)。这能避免不必要的上下文切换开销,并降低死锁风险。
  • 不要给纯计算方法加 async:如果一个方法内部全是 CPU 计算,没有 await,仅仅加上 async 并不会让它变成异步执行,它依然会同步阻塞当前线程直到返回。
  • 当多个独立的数据源需要同时拉取时,可以使用 Task.WhenAll 来最大化吞吐量,而不是串行 await
相关推荐
周杰伦fans2 小时前
续集:工作空间一切换,我的插件菜单就消失?——MenuBar与Ribbon的自动重载方案
后端·ribbon·c#
ysn111113 小时前
红点框架系统设计
系统架构·c#
步步为营DotNet3 小时前
借助 C# 14 特性强化 .NET 后端数据验证的深度实践
java·c#·.net
影寂ldy4 小时前
C# 泛型委托
java·算法·c#
z落落5 小时前
Timer与DateTimePicker:控件使用全解析
开发语言·c#
2601_961845155 小时前
2026法考资料pdf|电子版|资料已整理
开发语言·前端框架·pdf·c#·xhtml·csrf·view design
ceclar1237 小时前
C#字节流与字符流
算法·c#·.net
z落落18 小时前
C#WinForm 窗体切换与窗体传值(登录跳转案例)+WinForm 窗体传值(从上往下传、从下往上传)
开发语言·windows·c#