.net5 core webapi进阶之三:异步编程(上篇)async/await的使用

随着智能手机的普及,现在的互联网用户基数动辄数以千万、亿计,这对软件系统的执行效率和稳定性提出了更高的要求,

代码的执行效率除了在硬件层面解决之外,在软件层面也有很多技术出现,异步编程就是其中之一,

C# 5.0 引入一个新特性来构建异步方法------async/await,接下来我们就来学习如何灵活的使用它。

 

一、预备知识

1. 什么是进程(Process)?

进程(Process)是操作系统进行资源分配和调度的基本单位,这些资源包括虚地址空间、文件句柄、寄存器等。

2. 什么是线程(Thread)?

线程(Thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,

它代表了真正执行的程序,一旦进程(Process)建立,系统会在Main方法的第一行语句处开始线程(Thread)的执行。

3. 进程(Process)和线程(Thread)的关系?

  • 默认情况下,一个进程(Process)只包含一个线程(Thread),从程序的开始一直执行到结束。
  • 一个进程(Process)可能包含不同状态的多个线程(Thread),它们并行执行程序的不同部分。
  • 如果一个进程(Process)拥有多个线程(Thread),它们将共享进程(Process)的资源。

4. 同步(Synchronous)和异步(Asynchronous)

  • 程序从第一条语句开始按顺序执行到最后一条叫同步(Synchronous)。
  • 程序代码不需要按照编写顺序严格执行叫异步(Asynchronous)。

      或者换个说法:

  • 同步(Synchronous)就是调用某个方法时得等待这个调用返回结果才能继续往后执行。
  • 异步(Asynchronous)就是调用某个方法时调用方不必立刻得到结果就可继续执行后续操作,
    被调用者通过状态、通知或回调来通知调用方。

      到目前为止,本博客中 .net5 core webapi 项目实战系列文章都是同步编程。

 

二、如何用 async/await 进行异步编程?

异步编程从创建一个异步方法开始,规则如下:

  • 方法头中包含 async 修饰符。
  • 方法体中包含至少一个 await 表达式,表示可以异步完成的任务。
  • 方法的返回类型必须是一下4种之一,其中泛型T表示实际的返回类型。
          ■ void   ■ Task    ■ Task<T>    ■ ValueTask<T>
  • 方法形参不能有 out 或 ref 修饰符。
  • 方法名应该以 Async 结尾(这是约定不是必须)。
  • 如果方法体中没有 await 表达式而标记为 async 那么该方法会以同步方式执行。

 

三、编码演示。

1. 在项目中新建一个 API 控制器 AsyncDemoController ,在里面写一个简单的异步方法 Demo0(),代码如下 :

 private async void Demo0()
 {
     WebClient web = new WebClient();
     await web.DownloadFileTaskAsync("http://www.baidu.com","abc.png"); 
 }

功能:用.NET的 WebClient 实例下载百度网上的一张图片(这张图片abc.png可能并不存在,只是为了演示用)。

可以看到此方法的方法头有 async 修饰,方法体有 await 表达式,无返回值。

2. 再来看一个带返回值的异步方法 Demo1( ) ,代码如下:

private async Task<string> Demo1()
{
    WebClient web = new WebClient(); 
    string str = await web.DownloadStringTaskAsync(new Uri("http://www.baidu.com"));

    return str;
}

此方法头有 async 修饰,方法体有 await 表达式,返回值是字符串,所以方法头中的返回值类型是 Task<string>,

用泛型的 Task 来包装实际的返回值类型(调用方获取返回值用 Task实例的 Result 属性就可以了)。

在控制器 AsyncDemoController 中增加一个终结点来测试这个异步方法:

[HttpGet]
[Route("async1")]
public string Async1() {
  Task<string> task = Demo1();
  return task.Result;
}

 打开 POSTMAN,用 GET 方式访问网址:http://localhost:61946/api/asyncdemo/async1,得到如下结果:

我们得到了百度首页的HTML代码(爬虫程序就是这样抓取网页内容后提取所需信息的)。

3. 在 Demo1( ) 方法中,await 表达式修饰了方法 "web.DownloadStringTaskAsync(new Uri("http://www.baidu.com"));",如下:

private async Task<string> Demo1()
{
    WebClient web = new WebClient(); 
    string str = await web.DownloadStringTaskAsync(new Uri("http://www.baidu.com"));

    return str;
}

如果随便写一个返回字符串的方法是否也可以用 await 来修饰呢?,试着写如下代码:

系统会提示错误,"string" 未包含 "GetAwaiter" 的定义的错误提示,可见不是每个方法都可以用 await 来修饰的。

4. 如果我们有一个方法需要写成异步的该如何做呢,有两种方式可以实现。

第一种,使用 Task.Run( ) 方法,代码如下:

        private async Task<string> Demo21()
        {
            // 使用 Task.Run() 来执行方法 Demo22()即可使用 await 关键字
            string str = await Task.Run<string>(Demo22);// Task.Run<>()的泛型类型与 Demo22() 的返回值类型与一致。

            return str;
        }

        private string Demo22()
        { ··
            return "Demo22";
        }

增加终结点 Async2( ) 来测试此异步方法,代码如下:

        [HttpGet]
        [Route("async2")]
        public string Async2()
        {
            Task<string> task = Demo21();

            return task.Result;
        }

打开 POSTMAN,用 GET 方法访问网址:http://localhost:61946/api/asyncdemo/async2,得到预期的结果如下:

0

第二种,使用 GetAwaiter( ) 的方式,这种方式比较复杂,需要按约定实现一系列的属性和方法,其代码如下:

namespace webapidemo2
{
    public class MyAsyncFunction
    {
        // 方法 CallFuncAsync()可被 await 修饰,因其返回值的类型 MyTask 中包含 GetAwaiter() 方法。
        public MyTask CallFuncAsync() { return new MyTask(); } 
    }
    public static class MyAsyncVariable
    {
        // 给字符串增加扩展方法 GetAwaiter() 后单个字符串也可以被 await 修饰,字符串也可以换成其他类型
        public static MyAwaiter GetAwaiter(this string str) { return new MyAwaiter(); }
    }
    // 自定义的类 MyTask 中要有 GetAwaiter() 方法
    public class MyTask
    { 
        public MyAwaiter GetAwaiter() { return new MyAwaiter(); } 
    }
    // 自定义的类 MyAwaiter 中要实现 INotifyCompletion 接口并包含如下三个成员 。 
    public class MyAwaiter : INotifyCompletion
    {
        public void GetResult() { return; }         
        public bool IsCompleted {  get { return true; } }         
        public void OnCompleted(Action continuation) { }
    }
}

在 AsyncDemoController 中加入异步方法 Demo3( ) 和终结点 Async3( ) 如下:

        private async Task<string> Demo3()
        {
            MyAsyncFunction func = new MyAsyncFunction();
            await func.CallFuncAsync(); // 方法 CallFuncAsync() 可以被 await 修饰。
            await "abc"; // 扩展方法 GetAwaiter(this string str) 让字符串也可以被 await 修饰。

            return "Demo3";
        }
         
        [HttpGet]
        [Route("async3")]
        public string Async3()
        {
            Task<string> task = Demo3();
            return task.Result; 
        }

打开 POSTMAN,用 GET 方法访问网址:http://localhost:61946/api/asyncdemo/async3 得到期望的结果:

从 MyTask 和 MyNotify 的定义可以看到,如果想要某个变量/方法可以被 await 修饰,需要满足如下几个条件:

1. 如果修饰变量则该变量必须拥有 GetAwaiter() 方法(此方法是变量的扩展方法),即可执行 task.GetAwaiter(); 的操作。

2. 如果修饰方法则该方法的返回值类型必须拥有 GetAwaiter() 方法。

3.  GetAwaiter() 方法的返回类型必须有 " void GetResult(); " 方法 、"bool IsCompleted" 属性、且实现了 INotifyCompletion 接口,三者缺一不可。

( 待续... )

posted @ 2021-02-24 17:10  屏风马  阅读(4394)  评论(1编辑  收藏  举报