C#的async异步编程是基于任务异步模型(TAP)实现的,核心是通过async和await关键字让异步代码的编写逻辑更接近同步代码,同时避免主线程被长时间操作阻塞,提升程序的响应性和资源利用率。

async异步编程的核心基础
async关键字的作用
async是用来修饰方法的修饰符,被async修饰的方法称为异步方法。异步方法的返回类型通常是Task或者Task<T>,如果方法没有返回值则使用Task,有返回值则使用Task<T>,只有一种特殊情况可以返回void,就是事件处理程序。
需要注意,async本身并不会让方法异步执行,它只是启用了方法内的await关键字,并且会把方法的返回值包装成Task类型。如果一个async方法内部没有await表达式,那么这个方法会同步执行,编译器还会给出警告。
await关键字的作用
await只能用在async修饰的方法内部,它的作用是挂起当前方法的执行,等待后面的异步操作完成,同时把控制权交还给调用方,不会阻塞调用方的线程。等异步操作完成后,会从await的位置继续执行后续代码。
await后面通常跟的是一个返回Task或者Task<T>的表达式,比如调用另一个异步方法返回的Task,或者手动创建的Task。
实现非阻塞操作的基本步骤
1. 定义异步方法
首先定义带有async修饰的方法,返回类型根据是否有返回值选择Task或者Task<T>,方法内部使用await等待耗时操作。
下面是一个模拟文件读取的异步方法示例,文件读取属于IO操作,使用异步方式可以避免阻塞线程:
using System;
using System.IO;
using System.Threading.Tasks;
public class FileHelper
{
// 无返回值的异步方法,返回Task
public async Task ReadFileAsync(string filePath)
{
// 模拟异步读取文件,实际开发中可以使用File.ReadAllTextAsync等内置异步方法
await Task.Delay(1000); // 模拟1秒的IO耗时
string content = await File.ReadAllTextAsync(filePath);
Console.WriteLine($"文件内容读取完成,长度:{content.Length}");
}
// 有返回值的异步方法,返回Task<string>
public async Task<string> GetFileContentAsync(string filePath)
{
await Task.Delay(500); // 模拟耗时操作
return await File.ReadAllTextAsync(filePath);
}
}
2. 调用异步方法实现非阻塞
调用异步方法时,如果是UI程序(比如WinForm、WPF),在事件处理方法中可以直接await调用,不会阻塞UI线程;如果是控制台程序,需要注意控制台程序的入口方法Main默认不支持async,需要改成异步入口。
控制台程序调用异步方法的示例:
using System;
using System.Threading.Tasks;
class Program
{
// 控制台程序异步入口,C# 7.1及以上支持
static async Task Main(string[] args)
{
FileHelper helper = new FileHelper();
Console.WriteLine("开始执行异步操作,不会阻塞主线程");
// 调用异步方法,使用await等待,不会阻塞后续代码执行
await helper.ReadFileAsync("test.txt");
Console.WriteLine("异步操作执行完成");
// 调用有返回值的异步方法
string content = await helper.GetFileContentAsync("test.txt");
Console.WriteLine($"获取到的内容前100字符:{content.Substring(0, Math.Min(100, content.Length))}");
}
}
3. 处理多个异步操作
如果有多个独立的异步操作需要执行,可以使用Task.WhenAll或者Task.WhenAny来组合,避免逐个await导致的顺序等待,提升执行效率。
多个异步操作并行执行的示例:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
List<Task<string>> taskList = new List<Task<string>>();
// 添加三个独立的异步任务
taskList.Add(GetDataFromApiAsync("接口1"));
taskList.Add(GetDataFromApiAsync("接口2"));
taskList.Add(GetDataFromApiAsync("接口3"));
// 等待所有任务完成,并行执行,总耗时接近最长的单个任务耗时
string[] results = await Task.WhenAll(taskList);
Console.WriteLine($"所有接口数据获取完成,共{results.Length}条结果");
}
static async Task<string> GetDataFromApiAsync(string apiName)
{
// 模拟接口调用耗时
await Task.Delay(1000);
return $"{apiName}的返回数据";
}
}
异步编程的常见注意事项
- 不要使用
Task.Result或者Task.Wait()来获取异步结果,这两个方法会阻塞当前线程,失去异步非阻塞的意义,还可能导致死锁,尤其是在UI线程中。 - 异步方法命名建议以Async结尾,这是C#的约定俗成的规范,方便其他开发者识别方法是异步的。
- 避免使用async void方法,除了事件处理程序之外,其他场景不要使用async void,因为async void方法的异常无法被捕获,也难以组合到其他异步操作中。
- 注意异步方法的执行上下文,在UI程序中,await之后的代码默认会回到UI线程执行,如果需要切换到线程池线程执行耗时计算,可以使用
Task.Run。
常见误区解答
误区:async方法一定是异步执行的?
解答:不是,async只是标记方法可以包含await,如果方法内部没有await,方法会同步执行,所以编写异步方法时一定要确保内部有await等待的异步操作。
误区:使用async await就一定能提升性能?
解答:不是,异步编程主要是提升响应性,避免线程阻塞,对于CPU密集型的操作,使用异步并不会提升性能,反而会因为额外的状态机开销降低性能,CPU密集型操作建议使用Task.Run放到线程池执行。