异步调用对于程序员而言是一个十分重要的概念,通过它可以实现方法的调用者和被调用者的并发执行(也即是处于不同的线程中执行)。但是它的实现并不容易,需要进行比较复杂的构造。但是.Net引入了await-async关键字,将异步调用的构造变得十分简单。为了更清楚的表述,本文以C#作为编程语言,写了一个例子
1.async和await
await是一个运算符,是.Net为简化异步调用的程序编写引入的。它的作用是发起一个任务(Task/Task<TResult>),换句话说await的操作对象是Task/Task<TResult>对象。如以下的例子:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net;
using System.Net.Http;
namespace Test_Await
{
public class Program
{
public string str;
public async Task TestAwait()
{
try
{
using (HttpClient client = new HttpClient())
{
string str = await client.GetStringAsync("http://www.baidu.com");
System.Console.WriteLine(str);
}
}
catch (Exception e)
{
System.Console.WriteLine(e.Message);
}
}
public async void Test()
{
await TestAwait();
System.Console.WriteLine(str);
}
static void Main(string[] args)
{
Program obj = new Program();
obj.Test();
Thread.Sleep(1000);
}
}
}
这个例子很简单,就是下载百度主页的html文档。通过以上的例子,可以大概看出的await和async的编程规则。使用async和await关键字需要注意几点:
- 由async关键字修饰的方法,返回值有三种,void/Task/Task<TResult>(任务模板类),如上例中的Test()和TestAwait()方法;
- 由async修饰的方法,内部至少要有一个await表达式,否则编译器会报警告,并最终将方法按照同步的方式执行;
- 由async修饰的方法,如果返回值为void,则不能使用await运算符,因为前面已经说过了await的操作对象为Task/Task<TResult>实例
- await表达式只能出现在由async修饰的方法中,因此主函数不能出现await表达式。
2. 原理
async和await关键字,不会创建新的线程;创建新的线程,可以通过Task.Run实现;由async修饰的方法中,如果有await表达式,执行该表达式发起一个新任务时,程序直接将CPU控制权返还给方法的调用者,任务结束后,会继续执行await表示式后面的内容;如果没有await表达式,则当做同步调用处理了。也就是说在含有await表达式的方法中,await表达式会让方法的执行“挂起”,只有等任务完成后才会继续往下执行。async和await内部实现机制:编译器将await表达式后面的语句从方法中拆分出来,附加到任务结尾。任务结束后,就可以继续调用拆分出来的部分了。这样,一个比较有意思的现象是,如果任务是在另一个线程中执行的,则await表达式的前后部分代码并不是处在一个线程中,后部分的代码是在任务所在线程中执行的,这也就是为什么主线程中需要添加Thread.Sleep()语句了。如果不这样的话,主线程执行了
<pre name="code" class="csharp">string str = await client.GetStringAsync("http://www.baidu.com");
语句之后立马退出了,这样子线程也会随之撤销,从而导致任务没法继续进行了。
以上例进行说明:
主线程在执行await TestAwait()语句时,发起TestAwait()任务,下面的一句
System.Console.WriteLine(str);
已经被编译器附加到了TestAwait()的末尾。主线程进入TestAwait(),当执行:
string str = await client.GetStringAsync("http://www.baidu.com");
时,发起新任务GetStringAsync()。(编译器已经将await之后的代码都附加到了该方法的末尾,包括由await TestAwait()附加而来的System.Console.WriteLine(str)。)主线程跳出调用,执行:
Thread.Sleep(1000);
这样新任务在执行完成下载之后,连续执行两句:
System.Console.WriteLine(str);
3.疑惑
有一个比较让人疑惑的地方,如果
string str = await client.GetStringAsync("http://www.baidu.com");
在控制台中执行时,则会创建一个新的线程,await之后的语句,都会在这个线程中执行。如果是在UI中执行的,await语句并没有创建新的线程,await之后的语句也都会在主线程中执行。