1. 介绍
想象你的代码像商店的队列。如果你运行同步方法,整个线程(你的队列)会等收银员处理完一个顾客才开始下一个。如果方法是异步的,你就把东西放到收银台然后去做别的事,等收银员处理好会叫你回来。
在 .NET 里,异步方法(带 async 的方法)是普通方法,只是在前面有个特殊的“标记”(async),它允许在方法内部使用 await 来对其他异步操作(比如网络或文件操作)进行非阻塞等待而不阻塞主线程。正是这个“标记”把普通的同步商店变成了现代化的无队列服务。
异步方法的基础签名
异步方法必须在签名上声明 async 修饰符。下面是最基础的例子:
public async Task MyAsyncMethod()
{
// 你的异步代码写这里
}
异步方法的返回类型:
| 返回类型 | 说明 | 示例 |
|---|---|---|
|
方法异步执行,但不返回结果 | |
|
方法异步执行并返回类型为 T 的结果 | |
|
只用于事件处理。其他情况不推荐! | |
|
用于高性能场景,当结果经常同步可用时 | — |
重要: 除了事件处理器别使用 async void!否则你可能无法正常处理异常。
2. 简单的异步方法示例
回到我们的练习程序(假设是个普通控制台程序),添加一个从网络获取数据的异步方法。我们将模拟延迟。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("启动异步方法...");
await MyFakeDownloadAsync();
Console.WriteLine("异步操作完成!");
}
static async Task MyFakeDownloadAsync()
{
Console.WriteLine("开始下载(模拟 2 秒延迟)...");
await Task.Delay(2000); // 模拟耗时操作
Console.WriteLine("下载完成!");
}
}
示例说明:
- 关键字 async 用在 Main 和 MyFakeDownloadAsync 上。
- 操作符 await 只能在标记为 async 的方法内部使用。
- 方法 Task.Delay(2000) 模拟一个耗时操作(比如网络请求)。
- 在遇到 await 后线程不会被阻塞——Main 的执行会“挂起”直到延迟完成。
3. 异步方法在底层是如何工作的?
当编译器看到带有 async 的方法时,会把它转换成一个“状态机”,记录暂停点、局部变量,并在 awaited 操作完成后继续执行。
下面是一个非常简化的示意:
+-----------------------------------------------------------+
| 调用异步方法(例如 await X) |
+-------------------------+---------------------------------+
|
v
操作完成了吗?(不是 Task.Completed)
| |
| 是 | 否
v v
暂停执行, 立即返回
记住上下文 结果
|
异步操作完成...
|
v
状态机“唤醒”方法,
从 await 之后的位置继续执行
不需要记住所有实现细节:上下文(暂停点和局部变量)会被保存,并在操作完成时自动恢复。
在哪些地方可以(或不能)使用 async 和 await
- async 放在方法(或 lambda)声明前面。
- 在 async 方法内部可以(而且常常需要)使用 await。
- 如果在没有 async 的方法中使用 await——会编译错误。
- 如果写了 async 但方法里没有任何 await——会有警告;方法会同步执行并返回已完成的任务。
4. 返回值选项及其特点
返回 Task
如果方法执行异步操作但不返回值,使用 Task:
public async Task SaveToFileAsync(string path)
{
await Task.Delay(1000);
// 保存东西到文件
}
返回 Task<T>
如果需要返回结果,使用 Task<T>:
public async Task<int> CalculateAsync()
{
await Task.Delay(500);
return 42;
}
调用:
int result = await CalculateAsync();
结果为 async void
仅用于事件!例如按钮点击处理器:
private async void Button_Click(object sender, EventArgs e)
{
await Task.Delay(1000);
// 再做点别的
}
为什么不要在普通方法中使用 async void?因为你无法得知该方法何时完成,也无法正确捕获其中的异常。
返回 ValueTask 和 ValueTask<T>
它们为高性能库而生,当结果经常同步可用时可以避免额外的任务分配。刚开始学的时候可以不使用:不那么常见。
5. 示例:返回结果的异步计算器
假设程序“带延迟”计算两个数字的和,好像这是个很复杂的操作:
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("请输入第一个数字:");
int a = int.Parse(Console.ReadLine()!);
Console.WriteLine("请输入第二个数字:");
int b = int.Parse(Console.ReadLine()!);
Console.WriteLine("正在计算...");
int sum = await CalculateSumAsync(a, b);
Console.WriteLine($"和: {sum}");
}
static async Task<int> CalculateSumAsync(int x, int y)
{
await Task.Delay(2000); // “耗时”操作
return x + y;
}
}
这里需要注意的:
- CalculateSumAsync 是异步方法,返回 Task<int>,内部使用了 await。
- 在 Main 中使用了 await 来调用它(提示:从 C# 7.1 起 Main 可以是异步的)。
- 计算和的时候线程不会阻塞——可以显示进度或做其它工作。
6. 嵌套方法和嵌套 await
异步方法可以调用另一个异步方法,而那个方法又可以调用另一个。整个链会自动继续:
public async Task<int> StartWorkAsync()
{
int data = await GetDataAsync();
int result = await ProcessDataAsync(data);
return result;
}
很简单:等待一个操作完成,然后等待下一个。
7. 陷阱和常见错误
1. 别忘了写 async!
如果忘记了 async,编译器会因为在方法内部看到 await 而报错,代码无法编译。
2. 除了事件处理器别用 async void
异步 void 方法(除了事件处理器)是异常的“黑洞”。里面的错误可能会被吃掉,你可能根本不会知道。
3. 异步方法没有 await
可以这样写,但没意义——方法会同步执行并返回一个已完成的任务。
public async Task DoNothingAsync()
{
// 哎,根本没有 await!
}
4. 从非异步方法返回 Task
这样做是为了统一接口:有些操作真实是异步的,有些是瞬时完成的。
public Task<int> Foo()
{
// return 42; // 这样不行!
return Task.FromResult(42); // 可以,但这里没有任何异步性。
}
GO TO FULL VERSION