常见的异步方式async 和 await

之前研究过c#的async和await关键字,幕后干了什么,但是不知道为什么找不到相关资料了。现在重新研究一遍,顺便记录下来,方便以后查阅。

原文地址: 

基础知识

async
关键字标注一个方法,该方法返回值是一个Task、或者Task<TResult>、void、包含GetAwaiter方法的类型。该方法通常包含一个await表达式。该表达式标注一个点,将被某个异步方法回跳到该点。并且,当前函数执行到该点,将立刻返回控制权给调用方。

以上描述了async方法想干的事情,至于如何实现,这里就不涉猎了。

 

个人见解

由此可以知道,async
和await关键字主要目的是为了控制异步线程的同步,让一个异步过程,表现得好像同步过程一样。

比如async
方法分n个任务去下载网页并进行处理:先await下载,然后立刻返回调用方,之后的处理就由异步线程完成下载后调用。这时候调用方可以继续执行它的任务,不过,如果调用方立刻就需要async的结果,那么应该就只能等待,不过大多数情况:他暂时不需要这个结果,那么就可以并行处理这些代码。

可见,并行性体现在await 上,如果await
点和最终的数据结果距离越远,那么并行度就越高。如果await的点越多,相信也会改善并行性。

资料显示,async 和await
关键字并不会创建线程,这是很关键的一点。
他们只是创建了一个返回点,提供给需要他的线程使用。那么线程究竟是谁创建?注意await
表达式的组成,他需要一个Task,一个Task并不代表一定要创建线程,也可以是另一个async方法,但是层层包裹最里面的方法,很可能就是一个原生的Task,比如await
Task.Run(()=>Thread.Sleep(0));
,这个真正产生线程的语句,就会根据前面那些await点,逐个回调。

从这点来看,async
方法,未必就是一个异步方法,他在语义上更加贴近“非阻塞”,
当遇到阻塞操作,立刻用await定点返回,至于其他更深一层的解决手段,它就不关心了。这是程序员需要关心的,程序员需要用真正的创建线程代码,来完成异步操作(当然这一步可由库程序员完成)。

注意async的几个返回值类型,这代表了不同的使用场景。如果是void,说明客户端不关心数据同步问题,它只需要线程的控制权立刻返回。可以用在ui
等场合,如果是Task,客户端也不关心数据,但是它希望能够控制异步线程,这可能是对任务执行顺序有一定的要求。当然,最常见的是Task<TResult>。

综上,async和await并不是为了多任务而设计的,如果追求高并发,应该在async函数内部用Task好好设计一番。在使用async
和await的时候,只需要按照非阻塞的思路去编写代码就可以了,至于幕后怎么处理就交给真正的多线程代码创建者吧。

同步编程与异步编程

通常情况下,我们写的C#代码就是同步的,运行在同一个线程中,从程序的第一行代码到最后一句代码顺序执行。而异步编程的核心是使用多线程,通过让不同的线程执行不同的任务,实现不同代码的并行运行。

示范代码

        static async Task RunTaskAsync(int step)
        {
            for(int i=0; i < step; i++)
            {
                await Task.Run(()=>Thread.Sleep(tmloop));//点是静态的,依次执行
                Thread.Sleep(tm2);
            }
            Thread.Sleep(tm3);
        }

//客户端
            Task tk= RunTaskAsync(step);
            Thread.Sleep(tm1);//这一段是并行的,取max(函数,代码段)最大时间
            tk.Wait( );//这里代表最终数据

为了达到高度并行,应该用真正的多线程代码:

        static async Task RunTaskByParallelAsync(int step)
        {
            await Task.Run(()=>Parallel.For(0,step,
                s=>{loop(tmloop);
                    loop(tm2);
                    }
            ));
            loop(tm3);
        }

前台线程与后台线程

关于多线程,早在.NET2.0时代,基础类库中就提供了Thread实现。默认情况下,实例化一个Thread创建的是前台线程,只要有前台线程在运行,应用程序的进程就一直处于运行状态,以控制台应用程序为例,在Main方法中实例化一个Thread,这个Main方法就会等待Thread线程执行完毕才退出。而对于后台线程,应用程序将不考虑其是否执行完毕,只要应用程序的主线程和前台线程执行完毕就可以退出,退出后所有的后台线程将被自动终止。来看代码应该更清楚一些:

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
35
36
37
38
39
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("主线程开始");
 
            //实例化Thread,默认创建前台线程
            Thread t1 = new Thread(DoRun1);
            t1.Start();
 
            //可以通过修改Thread的IsBackground,将其变为后台线程
            Thread t2 = new Thread(DoRun2) { IsBackground = true };
            t2.Start();
 
            Console.WriteLine("主线程结束");
        }
 
        static void DoRun1()
        {
            Thread.Sleep(500);
            Console.WriteLine("这是前台线程调用");
        }
 
        static void DoRun2()
        {
            Thread.Sleep(1500);
            Console.WriteLine("这是后台线程调用");
        }
    }
}

运行上面的代码,可以看到DoRun2方法的打印信息“这是后台线程调用”将不会被显示出来,因为应用程序执行完主线程和前台线程后,就自动退出了,所有的后台线程将被自动终止。这里后台线程设置了等待1.5s,假如这个后台线程比前台线程或主线程提前执行完毕,对应的信息“这是后台线程调用”将可以被成功打印出来。

发表评论

电子邮件地址不会被公开。 必填项已用*标注