在C#5之前,对于一个长耗时的操作,开发人员需要编写繁琐的回调、事件订阅以及应付错误,C#5推出了async/await特性,await语法可以在不阻塞当前线程的情况下等待某个操作完成。
异步函数是由async修饰符修饰的方法或者匿名函数,async修饰符是对函数中某个表达式使用await时的强制要求,在生成IL代码时async修饰符被省略了。只有异步函数才可以对await表达式使用await运算符,但是异步函数中也可以不包含await表达式,虽然这样async修饰符就没有了任何意义。
await的主要作用是避免在等待长耗时操作时线程被阻塞。当执行到await时,代码会检查是否已得到执行结果(通常结果是否的),如果没有,它就会创建一个续延并立即返回,当await表达式操作完成后就继续执行该续延。
这样开发人员可以像使用同步编程一样来编写异步代码:
public partial class AsyncTestForm : Form
{
public string Result { get; set; } = "";
private int index = 0;
public AsyncTestForm()
{
InitializeComponent();
}
private void AsyncTestBtn_Click(object sender, EventArgs e)
{
//调用函数开始执行
Result = string.Format("{0}:开始测试\n", ++index);
QueryMsgByGetAsync(tbxUri.Text);
//从异步方法中返回了
Result += string.Format("{0}:结束测试函数\n", ++index);
}
public async void QueryMsgByGetAsync(string uri)
{
Result += string.Format("{0}:开始执行async函数\n", ++index);
HttpClient client = new HttpClient
{
Timeout = TimeSpan.FromSeconds(30)
};
Result += string.Format("{0}:执行await表达式\n", ++index);
HttpResponseMessage response = await client.GetAsync(uri);
//await表达式完成后返回该处继续执行
Result += string.Format("{0}:await表达式执行完成\n", ++index);
}
private void ShowRstBtn_Click(object sender, EventArgs e)
{
MessageBox.Show(Result);
}
}
调用函数开始执行,程序进入async被调函数的await表达式处后,立即返回到调用函数,调用函数继续执行后续代码。当被调函数的await表达式执行完后,程序返回到该处继续执行。
续延:指示某项操作完成之后执行什么操作。
await搭配的表达式必须是可等待的,可等待模式接下来讲解。除此之外,使用await表达式还有一些限制条件:
除此之外,对于async方法或者匿名函数也有使用要求:参数不能用ref或out修饰词修饰,异步返回时并不能获知ref或out参数的状态详情。
可等待模式用于判断哪些类型可使用await运算符,是异步操作的定义基础。
假设一个返回T类型的表达式使用await关键字,编译器会执行以下检查:
上述成员不必为public,但是这些方法必须能被调用await的async方法访问到。
public class Task
{
public static YieldAwaitable Yield()
{
YieldAwaitable yieldAwaitable = new YieldAwaitable();
//...
return yieldAwaitable;
}
}
public struct YieldAwaitable
{
public YieldAwaiter GetAwaiter()
{
YieldAwaiter yieldAwaiter = new YieldAwaiter();
//...
return yieldAwaiter;
}
public struct YieldAwaiter : INotifyCompletion
{
public bool IsCompleted { get; }
public void OnCompleted(Action action)
{
//...
}
public void GetResult()
{
//...
}
}
}
顺便重提一下,可等待模式是语言层面的,编译器替我们做了这些隐藏起来的工作,我们不必再自己动手编写这些繁琐的代码。
在真实的异步模型中,续延并没有被传给异步操作,而是由异步操作发起并返回一个令牌,该令牌可供续延使用。该令牌代表正在执行的操作,该操作可能在返回调用方之前就已经执行完成了,也可能还在执行中。该令牌用于表达:在该操作完成前不能开始后续的操作。令牌通常是以Task或Task的形式出现的,但并非强制要求。
一个简单的异步:
Task<string> task = client.GetStringAsync(uri);//创建令牌
string result = await task;//可以选择阻塞于此处,也可以将该令牌用于另一个续延
//也可以将两行代码合并:
string result = await client.GetStringAsync(uri); await处理流程:
不同的执行环境会使用不同的上下文。异步模式的上下文比同步上下文要多,要想了解异步执行机制,首先需要了解同步上下文。SynchronizationContext类诞生于.net2.0,它负责在正确的线程中执行委托。对于更新UI,异步能通过实现SynchronizationContext类来保证await表达式能在UI线程中执行。
C#5.0中异步函数可以返回3个类型:
在C#7.0中新增了自定义task类型,将在之后讲解。
返回void被称为“调用并忘记”,它不反回任何值,仅仅是执行异步操作。
//将某文件复制到其他文件夹
string sourcePath = "G:\\Documents\\C# In Depth,4th Edition\\Chapter5 Summary.txt";
string destinationPath = "D:\\C# In Depth,4th Edition\\Chapter5 Summary.txt";
CopyFileAsync(sourcePath, destinationPath);
async void CopyFileAsync(string sourceFile, string destinationFile)
{
FileStream sourceStream = new FileStream(
path: sourceFile,
mode: FileMode.Open,
access: FileAccess.Read,
share: FileShare.Read,
bufferSize: 4096,
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
FileStream destinationStream = new FileStream(
path: destinationFile,
mode: FileMode.CreateNew,
access: FileAccess.Write,
share: FileShare.None,
bufferSize: 4096,
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
await sourceStream.CopyToAsync(destinationStream);
sourceStream.Close();
destinationStream.Close();
}
返回Task和返回void类似,也不需要从异步方法中返回任何值,但是它可以检查异步方法的状态:
//将某文件复制到其他文件夹,复制完成后删除源文件
string sourcePath = "G:\\Documents\\C# In Depth,4th Edition\\Chapter5 Summary.txt";
string destinationPath = "D:\\C# In Depth,4th Edition\\Chapter5 Summary.txt";
Task task = CopyFileAsync(sourcePath, destinationPath);
//异步完成后执行后续代码
task.Wait();
File.Delete(sourcePath);
async Task CopyFileAsync(string sourceFile, string destinationFile)
{
FileStream sourceStream = new FileStream(
path: sourceFile,
mode: FileMode.Open,
access: FileAccess.Read,
share: FileShare.Read,
bufferSize: 4096,
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
FileStream destinationStream = new FileStream(
path: destinationFile,
mode: FileMode.CreateNew,
access: FileAccess.Write,
share: FileShare.None,
bufferSize: 4096,
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
await sourceStream.CopyToAsync(destinationStream);
sourceStream.Close();
destinationStream.Close();
}
调用方法需要从调用中获取类型为TResult的值时,异步方法的返回类型必须是Task,任何返回Task的异步方法,其返回值必须是TResult类型,或者可以隐式转换成TResult的类型。
string filePath = "D:\\C# In Depth,4th Edition\\Chapter5 Summary.txt";
Task<string> task = ReaderFileAsync(filePath);
Console.WriteLine(task.Result);
async Task<string> ReaderFileAsync(string targetFile)
{
StreamReader reader = new StreamReader(targetFile);
return await reader.ReadToEndAsync();
}
开发者可以取消或延迟异步方法,也可以在调用方法中同步地等待异步操作等。
一些异步方法允许用户终止执行,也可以通过CancellationToken和CancellationTokenSource类在自己的异步方法中实现该特性,它们的工作机制:
string filePath = "D:\\C# In Depth,4th Edition\\Chapter5 Summary.txt";
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;
Task<string> task = ReaderFileAsync(filePath, ct);
if (tokenCancell) cts.Cancel();
Console.WriteLine(task.Result);
async Task<string> ReaderFileAsync(string targetFile, CancellationToken token)
{
if (token.IsCancellationRequested) return "token.IsCancellationRequested";
StreamReader reader = new StreamReader(targetFile);
return await reader.ReadToEndAsync();
}
Task.Delay方法和Thread.Sleep方法不同,前者不会阻塞线程,线程可以继续处理其他工作。
string filePath = "D:\\C# In Depth,4th Edition\\Chapter5 Summary.txt";
Task<string> task = ReaderFileAsync(filePath);
Console.WriteLine(task.Result);
async Task CopyFileAsync(string targetFile)
{
await Task.Delay(1000);
}
使用Task.Wait()方法可以在同步方法中同步等待单一异步完成;使用Task.WaitAll()方法可以在同步方法中同步等待所有异步完成;使用Task.WaitAny()方法可以在同步方法中同步等待任一异步完成,其结果都是等待状态完成后执行后续操作。
异步方法中使用await方法等待一个或多个其它异步操作时,可以使用Task.WhenAll或者Task.WhenAny来等待全部或者任意异步完成。
C#设计团队在语言层面提供了异步异常的操作支持。async/await的基础架构尽量让异步异常接近于同步异常相似的处理方式。
以Task和Task为例,当异步操作失败时:
一般不会需要用户去编写自定义类型的task,以后碰到需求会再返回这里将内容补上,这部分内容暂时不表。