用async关键字修饰方法后,就变成了异步方法。需要注意几点:
- 异步方法返回值一般是Task。
- 约定异步方法名字以Async为结尾。
- 如果异步方法没有返回值,最好把返回值声明为非泛型的Task类型。
- 调用泛型异步方法时,一般在前面加上await,这样方法调用的返回值就是T类型。
- 如果一个方法中有await调用,则方法必须声明为async。
await关键字的意思是:调用异步方法,等异步方法执行结束后再继续向下执行。如果不加await,则不等待异步方法执行结束,就向下执行。
1 2 3 4 5 6 7 8 9 10 11
| async Task<int> DownloadAsync(string url, string destFilePath) { using HttpClient httpClient = new HttpClient(); string body = await httpClient.GetStringAsync(url); await File.WriteAllTextAsync(destFilePath, body); return body.Length; }
Console.WriteLine("开始下载人邮社网站"); int i1 = await DownloadAsync("https://www.ptpress.com.cn", "d:/ptpress.html"); Console.WriteLine($"下载完成,长度{i1}");
|
async异步方法的本质是:在对异步方法进行await调用时的等待时间(比如等待下载),会把当前的线程返回到线程池,等异步方法调用结束后,再从线程池中取出一个线程执行后面的代码。
使用async异步方法要用多任务的概念,如果是一个任务,则使用常规的方法更好。例如餐厅客人叫服务员点菜,当来了多个客人时,某个客人叫服务员来点菜,服务员将菜单给到这个客人,然后不需要一直等着,等到这个客人点菜成功,再叫服务员,这个时候服务员拿着点完的菜单交给厨师。这样做的好处是,这个服务员可以在某个客人点菜的时候,去服务其他顾客。
异步方法使用的技术是线程池,不等通于多线程。
对于async方法,编译器会把代码根据await调用分为若干片段,对于不同的片段使用状态机的方式切换执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
static async Task<decimal> CalcAsync(int n) { Console.WriteLine("CalcAsync:" + Thread.CurrentThread.ManagedThreadId); decimal result = 1; Random rand = new Random(); for (int i = 0; i < n * n; i++) { result = result + (decimal)rand.NextDouble(); } return result; }
async Task<decimal> CalcAsync(int n) { Console.WriteLine("CalcAsync:" + Thread.CurrentThread.ManagedThreadId); return await Task.Run(() => { Console.WriteLine("Task.Run:" + Thread.CurrentThread.ManagedThreadId); decimal result = 1; Random rand = new Random(); for (int i = 0; i < n * n; i++) { result = result + (decimal)rand.NextDouble(); } return result; }); }
|
只要返回值是Task类型,就可以使用await关键字对其进行调用,而不用管是否用async修饰
如果一个异步方法只是对别的异步方法进行简单调用,无太多复杂逻辑(如获取异步方法的返回值并做处理),则可以直接去掉async修饰符
对于使用async的异步方法,编译器会负责将返回值转换为Task对象(reurn 3 自动转换为Task<int>
),但是不带async修饰符的异步方法,需要自己自定义Task对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| string s1 = await ReadFileAsync(1); Console.WriteLine(s1);
Task<string> ReadFileAsync(int num) { switch (num) { case 1: return File.ReadAllTextAsync("d:/1.txt"); case 2: return File.ReadAllTextAsync("d:/2.txt"); default: return Task.CompleterTask; } }
|
重要问题:
- 有异步方法,建议使用异步方法
- 由于某些原因,有的方法不能被async修饰,那么在方法内就不能用await来调用异步方法了,也就是不能自动将Task类型的返回值识别为T类型。对于Task返回值类型,可以使用
Result
属性或者GetAwaiter().GetResult
方法来等待异步执行结束后获取返回值。对于Task返回值类型,可以使用Wait
方法来调用异步方法并等待任务结束,但不推荐这样,会造成阻塞调用线程。
1 2 3
| string s1 = File.ReadAllTextAsync("./1.txt").Result; string s2 = File.ReadAllTextAsync("./1.txt").GetAwaiter().GetResult(); File.ReadAllTextAsync("./1.txt").Wait();
|
- 异步方法的暂停
1 2 3 4
| using HttpClient httpClient = new HttpClient(); string s1 = await httpClient.GetStringAsync("https://www.ptpress.com.cn"); await Task.Delay(3000); string s2 = await httpClient.GetStringAsync("https://www.rymooc.com");
|
- 异步方法中如果有
CancellationToken
参数,则可以使用该对象提前终止操作。 - 可以使用
Task.WhenAll
同时等待多个Task的执行结束。另外也有Task.WhenAny
只要有一个任务完成,代码就向下执行
1 2 3 4 5 6 7
| Task<string> t1 = File.ReadAllTextAsync("d:/1.txt"); Task<string> t2 = File.ReadAllTextAsync("d:/2.txt"); Task<string> t3 = File.ReadAllTextAsync("d:/3.txt"); string[] results = await Task.WhenAll(t1, t2, t3); string s1 = results[0]; string s2 = results[1]; string s3 = results[2];
|
- 接口中或者抽象方法不能使用async修饰符,可以把这些方法的返回值设置为Task类型,在具体实现中可以添加async关键字修饰。