C#异步编程async与await
概念
async 和 await 并非直接创建新线程 ,而是编译器层面的状态机转换。当一个方法被标记为 async 时,编译器会将其重构为一个状态机。当执行流遇到 await 关键字时,如果等待的任务尚未完成,当前方法会被挂起,控制权立即交还给调用者。一旦后台任务完成,状态机会恢复上下文并继续执行后续代码。它不创建新线程,而是利用操作系统的异步 I/O 机制(如 IOCP)实现非阻塞等待
核心原则
async与await是为了简化异步代码编写的语法糖,而不是强制约束- 只要方法返回
Task或Task<T>,它就是"可等待的"(Awaitable)
例如
File.ReadAllBytesAsync相关方法就是直接返回的Task,但是它调用的底层方法是标记了async与await
使用场景
IO密集型
网络请求、数据库查询、文件读写,直接使用
async和await,无需手动分配新线程。底层操作系统会通过 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