如果需要 I/O 绑定(例如从网络请求数据或访问数据库),则需要利用异步编程。
还可以使用 CPU 绑定代码(例如执行成本高昂的计算),对编写异步代码而言,这是一个不错的方案。
C# 拥有语言级别的异步编程模型,它使你能轻松编写异步代码,而无需应付回叫或符合支持异步的库。 它遵循基于任务的异步模式 (TAP)。
异步模型的基本概述
对于 I/O 绑定代码,当你 await
一个操作,它将返回 async
方法中的一个 Task
或 Task<T>
。
对于 CPU 绑定代码,当你 await
一个操作,它将在后台线程通过 Task.Run
方法启动。
await
关键字是点睛之笔,因为它暂停对执行 await
的方法的调用方的控制权。 这正是 UI 具有响应性或服务具有灵活性的原因。
I/O 绑定示例:从 Web 服务下载数据
private readonly HttpClient _httpClient = new HttpClient();
downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await _httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
CPU 绑定示例:为游戏执行计算
最佳解决方法是启动一个后台线程,它使用 Task.Run 执行工作,并 await 其结果。 这可确保在执行工作时 UI 能流畅运行。
private DamageResult CalculateDamageDone()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
}
calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
内部原理
异步操作涉及许多移动部分。 若要了解 Task
和 Task<T>
的内部原理,请参阅深入了解异步。
在 C# 方面,编译器将代码转换为状态机,它将跟踪类似以下内容:到达 await
时暂停执行以及后台作业完成时继续执行。
从理论上讲,这是异步的承诺模型的实现。
需了解的要点
- 异步代码可用于 I/O 绑定和 CPU 绑定代码,但在每个方案中有所不同。
- 异步代码使用
Task<T>
和Task
,它们是对后台所完成的工作进行建模的构造。 -
async
关键字将方法转换为异步方法,这使你能在其正文中使用await
关键字。 - 应用
await
关键字后,它将挂起调用方法,并将控制权返还给调用方,直到等待的任务完成。 - 仅允许在异步方法中使用
await
。
识别 CPU 绑定和 I/O 绑定工作
如果你的工作为 I/O 绑定,请使用 async
和 await
(而不使用 Task.Run
)。 不应使用任务并行库。
如果你的工作为 CPU 绑定,并且你重视响应能力,请使用 async
和 await
,并在另一个线程上使用 Task.Run
生成工作。 如果该工作同时适用于并发和并行,则应考虑使用任务并行库。
此外,应始终对代码的执行进行测量。 例如,你可能会遇到这样的情况:多线程处理时,上下文切换的开销高于 CPU 绑定工作的开销。 每种选择都有折衷,应根据自身情况选择正确的折衷方案。
等待多个任务完成
你可能发现自己处于需要并行检索多个数据部分的情况。 Task
API 包含两种方法(即 Task.WhenAll
和 Task.WhenAny
),这些方法允许你编写在多个后台作业中执行非阻止等待的异步代码。
此示例演示如何为一组 User 捕捉 userId 数据
public async Task<User> GetUser(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
}
public static Task<IEnumerable<User>> GetUsers(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUser(id));
}
return await Task.WhenAll(getUserTasks);
}
以下是使用 LINQ 进行更简洁编写的另一种方法:
public async Task<User> GetUser(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
}
public static async Task<User[]> GetUsers(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUser(id));
return await Task.WhenAll(getUserTasks);
}
尽管它的代码较少,但在混合 LINQ 和异步代码时需要谨慎操作。 因为 LINQ 使用延迟的执行,因此异步调用将不会像在 foreach()
循环中那样立刻发生,除非强制所生成的序列通过对 .ToList()
或 .ToArray()
的调用循环访问。
重要信息和建议
async
方法需在其主体中具有await
关键字,否则它们将永不暂停!- 应将
Async
作为后缀添加到所编写的每个异步方法名称中。 -
async void
应仅用于事件处理程序。
async void
是允许异步事件处理程序工作的唯一方法,因为事件不具有返回类型(因此无法利用Task
和Task<T>
)。 其他任何对async void
的使用都不遵循 TAP 模型,且可能存在一定使用难度,例如:-
async void
方法中引发的异常无法在该方法外部被捕获。 - 十分难以测试
async void
方法。 - 如果调用方不希望
async void
方法是异步方法,则这些方法可能会产生不好的副作用。-
在 LINQ 表达式中使用异步 lambda 时请谨慎
LINQ 中的 Lambda 表达式使用延迟执行,这意味着代码可能在你并不希望结束的时候停止执行。 如果编写不正确,将阻塞任务引入其中时可能很容易导致死锁。 此外,此类异步代码嵌套可能会对推断代码的执行带来更多困难。 Async 和 LINQ 的功能都十分强大,但在结合使用两者时应尽可能小心。
-
在 LINQ 表达式中使用异步 lambda 时请谨慎
-