第二十章 异步编程
async/await特性的结构
一个程序调用某个方法,等待其执行所有处理后才继续执行,这种方法是同步的。
异步的方法在处理完成之前就返回到调用方法。C#的async/await特性可以创建并使用异步方法。由三部分组成:
- 调用方法(calling method):该方法调用异步方法,然后在异步方法(可能在相同的线程,也可能在不同线程)执行其任务的时候继续执行。
- 异步(async)方法:该方法 异步执行其工作,然后立即返回到调用方法。
- await表达式:用于异步方法内部,指明需要异步执行的任务。一个异步方法可以包含任意多个await表达式,如果一个都不包含编译器会发出警告。
识别 CPU 绑定和 I/O 绑定工作
在使用异步编程前,首先应确定我们要执行的操作时 I/O绑定 还是 CPU绑定 ,因为这会极大影响代码性能,并可能导致某些构造的误用。
以下是编写代码前应考虑的两个问题:
-
你的代码是否会“等待”某些内容,例如数据库中的数据?
如果答案为“是”,则你的工作是 I/O 绑定。 -
你的代码是否要执行开销巨大的计算?
如果答案为“是”,则你的工作是 CPU 绑定。
如果你的工作为 I/O 绑定,请使用 async
和 await
(而不使用 Task.Run
)。 不应使用任务并行库。 相关原因在深入了解异步的文章中说明。
如果你的工作为 CPU 绑定,并且你重视响应能力,请使用 async
和 await
,并在另一个线程上使用 Task.Run
生成工作。 如果该工作同时适用于并发和并行,则应考虑使用任务并行库。
此外,应始终对代码的执行进行测量。 例如,你可能会遇到这样的情况:多线程处理时,上下文切换的开销高于 CPU 绑定工作的开销。 每种选择都有折衷,应根据自身情况选择正确的折衷方案。
class Program
{
static void main()
{
...
Task<int> value = DoAsyncStuff.CalculateSumAsync(5, 6);//调用方法
...
}
}
static class DoAsyncStuff
{
关键字 返回类型
↓ ↓
public static async Task<int> CalculateSumAsync(int i1, int i2)//异步方法
{
int sum = await Task.Run( () => GetSum(i1, i2));//await表达式
return sum;//返回语句
}
...
}
异步方法
- 异步方法包含async方法修饰符
- 包含一个或多个await表达式,表示可以异步完成的任务
- 必须具备以下三种返回类型。第二种(Task)和第三种(Task<T>)的返回对象表示将在未来完成的工作,调用方法和异步方法可以继续执行
void
Task
Task<T> - 异步方法的参数可以为任意类型任意数量,但不能为out和ref参数
- 按照约定,异步方法的名称应该以Async为后缀
- 除方法之外,Lambda表达式和匿名方法也可以作为异步对象
async Task<int> CountCharactersAsync( int id, string site )
{
Console.WriteLine( "Starting CountCharacters" );
WebClient wc = new WebClient();
string result = await wc.DownloadStringTaskAsync( new Uri( site ) );//await表达式
Console.WriteLine( "CountCharacters Completed" );
return result.Length;
}
- 异步方法在方法头中必须包含async关键字,且必须出现在返回类型之前
- 该修饰符只是标识该方法包含一个或多个await表达式,并不能创建任何异步操作
- async关键字是一个上下文关键字,即除了作为修饰符,还可作为标识符
返回类型
- Task<T>:如果调用方法要从调用中获取一个T类型的值,异步方法的返回类型就必须是Task<T>。调用方法将通过读取Task的Result属性来获取这个T类型的值。
Task<int> value = DoStuff.CalculateSumAsync(5, 6);
...
Console.WriteLine( "Value: {0}", value.Result );
class AsyncReturnTest1
{
public void Excute()
{
Task<int> value = DoAsyncStuff.CalculateSumAsync(5, 6);
Console.WriteLine("value: {0}", value.Result);
}
class DoAsyncStuff
{
public static async Task<int> CalculateSumAsync(int i1, int i2)
{
int sum = await Task.Run(() => GetSum(i1, i2));
return sum;
}
private static int GetSum(int i1, int i2)
{
return i1 + i2;
}
}
}
- Task:如果调用方法不需要从异步方法中返回某个值,但需要检查异步方法的状态,则异步方法可以返回一个Task类型的对象。此时,即使异步方法中出现了return语句,也不会返回任何东西。
Task someTask = DoStuff.CalculateSumAsync(5, 6);
...
someTask.Wait();//等待Task完成执行过程
Console.WriteLine("Async stuff is done: {0}", someTask.Status);//可以使用.Status获取Task的执行的返回状态,若完成则为“RanToCompletion”
class AsyncReturnTest2
{
public void Excute()
{
Task someTask = DoAsyncStuff.CalculateSumAsync(5, 6);
someTask.Wait();
Console.WriteLine("Async stuff is done: {0}", someTask.Status);
}
class DoAsyncStuff
{
public static async Task CalculateSumAsync(int i1, int i2)
{
int value = await Task.Run(() => GetSum(i1, i2));
Console.WriteLine("value:{0}", value);
}
private static int GetSum(int i1, int i2)
{
return i1 + i2;
}
}
}
- void:如果调用方法只想执行异步方法,而不需要做任何进一步的交互时[这称为调用并忘记(fire and forget)],异步方法可返回void类型。
class AsyncReturnTest3
{
public void Excute()
{
DoAsyncStuff.CalculateSumAsync(5, 6);
Thread.Sleep(200);
Console.WriteLine("Program Exiting");
}
class DoAsyncStuff
{
public static async void CalculateSumAsync(int i1, int i2)
{
int value = await Task.Run(() => GetSum(i1, i2));
Console.WriteLine("value: {0}", value);
}
private static int GetSum(int i1, int i2)
{
return i1 + i2;
}
}
}
重要信息和建议
尽管异步编程相对简单,但应记住一些可避免意外行为的要点。
-
async
方法需在其主体中具有await
关键字,否则它们将永不暂停!
这一点需牢记在心。 如果 await
未用在 async
方法的主体中,C# 编译器将生成一个警告,但此代码将会以类似普通方法的方式进行编译和运行。 请注意这会导致效率低下,因为由 C# 编译器为异步方法生成的状态机将不会完成任何任务。
- 应将“Async”作为后缀添加到所编写的每个异步方法名称中。
这是 .NET 中的惯例,以便更轻松区分同步和异步方法。 请注意,未由代码显式调用的某些方法(如事件处理程序或 Web 控制器方法)并不一定适用。 由于它们未由代码显式调用,因此对其显式命名并不重要。
-
async void
应仅用于事件处理程序。
async void
是允许异步事件处理程序工作的唯一方法,因为事件不具有返回类型(因此无法利用 Task
和 Task<T>
)。 其他任何对 async void
的使用都不遵循 TAP 模型,且可能存在一定使用难度,例如:
-
async void
方法中引发的异常无法在该方法外部被捕获。 -
十分难以测试
async void
方法。 -
如果调用方不希望
async void
方法是异步方法,则这些方法可能会产生不好的副作用。 -
在 LINQ 表达式中使用异步 lambda 时请谨慎
LINQ 中的 Lambda 表达式使用延迟执行,这意味着代码可能在你并不希望结束的时候停止执行。 如果编写不正确,将阻塞任务引入其中时可能很容易导致死锁。 此外,此类异步代码嵌套可能会对推断代码的执行带来更多困难。 Async 和 LINQ 的功能都十分强大,但在结合使用两者时应尽可能小心。
- 采用非阻止方式编写等待任务的代码
将阻止当前线程作为等待任务完成的方法可能导致死锁和已阻止的上下文线程,且可能需要更复杂的错误处理。下表提供了关于如何以非阻止方式处理等待任务的指南:
使用以下方式... | 而不是… | 若要执行此操作 |
---|---|---|
await |
Task.Wait 或 Task.Result
|
检索后台任务的结果 |
await Task.WhenAny |
Task.WaitAny |
等待任何任务完成 |
await Task.WhenAll |
Task.WaitAll |
等待所有任务完成 |
await Task.Delay |
Thread.Sleep |
等待一段时间 |
- 编写状态欠缺的代码
请勿依赖全局对象的状态或某些方法的执行。 请仅依赖方法的返回值。 为什么?
- 这样更容易推断代码。
- 这样更容易测试代码。
- 混合异步和同步代码更简单。
- 通常可完全避免争用条件。
- 通过依赖返回值,协调异步代码可变得简单。
- (好处)它非常适用于依赖关系注入。
建议的目标是实现代码中完整或接近完整的引用透明度。 这么做能获得高度可预测、可测试和可维护的基本代码。