109

await和async更多的理解 - 谷歌’s

 6 years ago
source link: http://www.cnblogs.com/laogu2/p/7744411.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

最近有不少网友提起await和async,呵呵,C# 5引进的语法糖。

这个语法糖还真不好吃,能绕倒一堆初学的朋友,在网上也有很多网友关于这块知识点的争论,有对有错,今天在这里把这个误区好好讲讲。

await(C# 参考)这样写道

“await 运算符应用于异步方法中的任务,在方法的执行中插入挂起点,直到所等待的任务完成。 任务表示正在进行的工作。”

不要小看这两句话,内容里暗指的意思还真不少。

1)await 运算符针对于异步方法

2)await 插入挂起点

3)await 等待任务完成

4)任务表式正在进行的工作

带着上面四点,我们暂时停下,因为提到await不禁要联想到一个好基友aysnc

await(C# 参考)这样写道

"await 仅可用于由 async 关键字修改的异步方法中"

到这里,我们对上面的四个关键点,提出疑问。

await 运算符针对于异步方法,那如果我们在异步方法里添加入同步方法会怎么样?

  private static async Task<TResult> XAsync()
  {
           
     X(); //X()同步方法
        
     return await XXAsync(); //XXAsync()异步方法
  }

然后在mainTest主线程里调用这个XAsync()方法

static void Main(string[] args)
{
   XAsync();

OtherMethod(); }

在main方法里,网上有网友博客说道:

1)XAsync在主线程里不会被调用,直到 awiat XAsync 才会被成功调用。就像linq to sql表达式一像,首先是var results=array.select().where();语句一样,他们只是组装,
并不会执行,要等到foreach(var result in results){ ...}迭代时或者.toList()再真正的查询。

2)XAsync在主线程里会被调用,并不阻止主线程,主线程将继续执行下面的程序!原因是我们写这个方法时候,vs会给我们警告,有图为据

329491-20171027133943789-98410222.png

到底是谁正确呢?

呵呵,其实这里两种说法都不正确。

首先,XAsync()在主线程里,直接调用,肯定执行,VS此时也瞎胡闹,警告我们说,“不等待此方法”,这是有个大大的前提!那就是这个方法体内,必须是异步的!
可能说到此不好理解。

在XAsync()方法里,上面有一段同步方法X(),此时它是运行在主线程上的同步方法,会阻止主线程,在X()运行完全后,在运行至 return await XXAsync()时,才把主线程调用权交还给调用的方法

在此过程里,并不会产生新的线程,全部运行在主线程上。

呵呵,越说越迷糊了。。。

await和async 讲白了点,他们并不会真正意义上的去产生新的线程,我们都知道,产生线程可以用Task类或Thread类。
那么async 标注的方法到底是什么呢?微软给我们的一句单简的话,"await 仅可用于由 async 关键字修改的异步方法中"

这就说明,async是为了await起到一种“配合”作用。而是说async修饰的方法都是异步的,那也太相当然了。

在同步方法里,执行具有async修饰的方法,按同步方法来执行。也就是说X()是运行在主线程上的方法。

至于X()方法体内至于执行同步还是异步决定权由方法体内的方法是否据有异步功能!
就像我们上面一样,XAsync()方法体内的X()就同步,那么X()执行的依然是同步方法一样,而不是运行另外一线程上。

那么问题来了,那我们都直接X()方法多好,await还有何用,或者await 为何不直接调用下X()方法呢?。。。

于是我们继续看下一句XXAsync()方法,我们既然用了await 语法,为什么他可以用?我可以把他去了await吗?

当然是肯定的,就像这样:

private static async Task<TResult> XAsync()
  {
           
     X(); //X()同步方法0
        
     XXAsync();//"异步方法1"

     return await XXXAsync(); //XXAsync()异步方法2
  }
XXAsync() 此时是如何运行的呢?同步还是异步?问题又返回来了,至于同步还是异步,不是这个方法“名词”决定的,而是这个方法体内是如何执行的

如:


 private static async Task XXAsync()
  {  
     X();  
  }
此时像上面调用的方式,XXAsync()就是我们平时的同步方法嘛!

但是改下:

private static async Task XXAsync()
  {  
     Task.Run(() =>{
             X();   
      });
  }
依据用相同的方法调用XXAsync它,这个时候,真正的运行在另外“任务上”了哦,会运行在其它线程!

写到这里,我们并没有和await有关哦。

那么在XXAsync()方法前加await到底有何不同?

这里首先要澄清的是:加不加 await 与 XXAsync()是异步还是同步的并没有关系!

await 真正的目的只有一个 在调用异步方法 XXAsync() 时挂起此方法,它认为这个方法是比较耗时的方法,主线程或调用此方法的线程不要在此方法等待。
并同时作个标记,当前执行的线程运行结束时,将由此处恢复运行,所以在await 标记的异步方法下面的代码或方法将不能运行,必须等待这个方法完成!


如:
private static async Task XAsync()
  {
           
     await  XXAsync();

     OtherMothod(); 

  }
在运行至 await XXAsync()时,调用方法XAsync()者将不再运行,直接返回,就像我们程序代码中的return语句。这样做的好处是,调用线程,不将等待此耗时线程。直接让调用线程往下运行,

如果调用线程向上一直存在await 链,就像方法套方法里有return一样,会逐层向上返回,一直返回到主线程。

而每个“子线程”去等待耗时的I/O处理,比如 操作数据库和网络流任何,这里特别强调I/O处理,试想下,我们的程序要查询一个数据库,可能要有点时间,此时查询命令可能已运行在了sql数据库里,

如果数据库在远程另外一台机器上呢?我们的"子线程或者任务“只是等待,而此时的主线程可能已完成。

如何理解主线程已完成呢?Asp.net Mvc 的机制就在这里,我们都知道,IIS里的线程池是有限的,每次的Client端请求,都会到线程池里取一个空闲的线程,如果主线程一直在”占用“线程池,

很快线程池就会被利用完啦。此时我们平时说的”吞吐量“的高低就是与此息息相关!当线程池被请求完后,再次有新的Client端请求,要会等待线程池的释放。

而mvc 就引用了控制器里异步方法的机制,原理就是让耗时的线程,直接返回,交给主线程,从而主线程会第一时间释放线程池的占用,而耗时的子线程完成时,将会在await标记从继续运行,

由此可以看出Mvc的异步将大大提高了应用程序的”吞吐量“。

至于具体的mvc异步编程机制与原理,网上一大把,也可以看看mvc的源代码,这里只简单的说下,本文的主旨await标记给异步带来的作用。

话题转回来:

那么我们何时在调用异步方法用await 作”标记“呢?

看看microsoft的经典例子

// Three things to note in the signature:  
//  - The method has an async modifier.   
//  - The return type is Task or Task<T>. (See "Return Types" section.)  
//    Here, it is Task<int> because the return statement returns an integer.  
//  - The method name ends in "Async."  
async Task<int> AccessTheWebAsync()  
{   
    // You need to add a reference to System.Net.Http to declare client.  
    HttpClient client = new HttpClient();  

    // GetStringAsync returns a Task<string>. That means that when you await the  
    // task you'll get a string (urlContents).  
    Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");  1)

    // You can do work here that doesn't rely on the string from GetStringAsync.  
    DoIndependentWork();  2)

    // The await operator suspends AccessTheWebAsync.  
    //  - AccessTheWebAsync can't continue until getStringTask is complete.  
    //  - Meanwhile, control returns to the caller of AccessTheWebAsync.  
    //  - Control resumes here when getStringTask is complete.   
    //  - The await operator then retrieves the string result from getStringTask.  
    string urlContents = await getStringTask;  3)

    // The return statement specifies an integer result.  
    // Any methods that are awaiting AccessTheWebAsync retrieve the length value.  
    return urlContents.Length;  
}  
对上面的例子做了1)、2)、3)红色标记,

在1)处定义了一个网络流操作,认为此方法可能会耗时,如果在此处,添加一个await client.GetStringAsync("http://msdn.microsoft.com")

对程序来说,这是一个异步操作,开辟了一个新线程,同时程序在此处返回给被调线程或UI,等网络流返回结束时继续在运换醒被调线程或主线程,并由于继续往下运行其它方法。
对于像这样的网站,一级一级的向上await 不会造成任何的吞吐量或响应速度的降低,可是新的问题来了,接下来2)处
DoIndependentWork()方法必须等到1)完成才能继续运行,“跳出来站在更高点看下”,这不相当于”同步“了吗?按顺序一步一步来的,子线程关没有给我们太多的优势。

是的,确实如此。

我们知道,让“子线程或任务”干事情,主线程继续他的活儿才对的,所以在1)处,不应该用await,让”线程或任务再跑一会“。
由于我们没有加await,于是主线程或调用线程与调用网络流的子线程”一块运行“了。

当程序运行至3)时,1)标记处的任务可能已经完成或者快要完成,此时用了await目的只有一个,下面的一句话 urlContents.Length 要用到异步结果,必须待等结束,并同时向调用线程或主线程返回标记,

以使调用者最快的响应,而不是等待以至于阻塞。

回过头来看下:我们即要多线程或多任务执行我们的程序,让耗时的任务得到执行,同时又要给调用者快速响应,不管他有没有完成任务! 这才是真正的目的。

再想想我们前面说的,DoIndependentWork()调用,加不加await,方法肯定是执行的,同时与该方法异步还是同步也没有关系,只是要不要做”标记“而已

至于加不加标记,就是上面我们解释的理由,忘了,回过头来看看吧
再来看看下面的问题:

如果一个方法里存在多个await,会如何执行呢?

我们可以按照上面的猜想下,await某些时候功能很像return,多个await,相必,第一个awiat会返回,并作标记,后面的任何代码都要等待 如:


private static async TaskXAsync()
  {
           
    await  XXAsync();

    await  XXXAsync(); 
  }
事实情况确实如此,XXXAsync()必须等待XXAsync()方法执行结束!此时不会影响调用者的响应速度,但会影响我们代码的执行效率,这点和两个共步方法稍有区别 

private static async TaskXAsync()
  {
           
    XX();

    XXX(); 
  }
像上面的例子XX()和XXX()两同步方法,不仅按顺序执行,而且调用者也无法拿回调用权,也就是无法及时响应,必须待两个方法都结束为止。

”偷偷的想下“,我想在XX(),XXX()方法前加一个await 不就行了吗?

回过头来想想,上面说过:"await 仅可用于由 async 关键字修改的异步方法中"

实际上我们在VS加上那么await会报错,编译不过!   希望破灭。。。此时已经看出被async修饰的目的。因为XX()和XXX()并没被修饰。

那好了,我们就强制在同步方法上用async !

XX()
{
    code here...
}

实际上当我们强制在XX()方法上加上Async时VS已经提示如下:

329491-20171027164131273-1334127086.png

很显然,同步方法,想提高调用者的响应速度是不可能仅仅靠async 就能完成的!根本原因就是调用者与执行方法在同一个线程上。

 再回过头来继续我们上面的例子

private static async TaskXAsync()
  {
           
    await  XXAsync();

    await  XXXAsync(); 
  }
上面已清楚,这两个仅仅按顺序执行,并不能并行执行,势必影响执行效率,那么如何才能让他们并行执行呢?
microsoft有专门的方法 Task.WhenAll(Tasks) 我们可以看看microsoft的例子 如:

await SumPageSizesAsync();
 private async Task SumPageSizesAsync()
        {
            // Make a list of web addresses.
            List<string> urlList = SetUpURLList();


            // Declare an HttpClient object and increase the buffer size. The
            // default buffer size is 65,536.
            HttpClient client = new HttpClient() { MaxResponseContentBufferSize = 1000000 };

            // Create a query.
            IEnumerable<Task<int>> downloadTasksQuery = 
                from url in urlList select ProcessURL(url, client);

            // Use ToArray to execute the query and start the download tasks.
            Task<int>[] downloadTasks = downloadTasksQuery.ToArray();

            // You can do other work here before awaiting.

            // Await the completion of all the running tasks.
            int[] lengths = await Task.WhenAll(downloadTasks);

            //// The previous line is equivalent to the following two statements.
            //Task<int[]> whenAllTask = Task.WhenAll(downloadTasks);
            //int[] lengths = await whenAllTask;

            int total = lengths.Sum();

            //var total = 0;
            //foreach (var url in urlList)
            //{
            //    // GetByteArrayAsync returns a Task<T>. At completion, the task
            //    // produces a byte array.
            //    byte[] urlContent = await client.GetByteArrayAsync(url);

            //    // The previous line abbreviates the following two assignment
            //    // statements.
            //    Task<byte[]> getContentTask = client.GetByteArrayAsync(url);
            //    byte[] urlContent = await getContentTask;

            //    DisplayResults(url, urlContent);

            //    // Update the total.
            //    total += urlContent.Length;
            //}

            // Display the total count for all of the web addresses.
            resultsTextBox.Text +=
                string.Format("\r\n\r\nTotal bytes returned:  {0}\r\n", total);
        }
// The actions from the foreach loop are moved to this async method.
        async Task<int> ProcessURL(string url, HttpClient client)
        {
            byte[] byteArray = await client.GetByteArrayAsync(url);
            DisplayResults(url, byteArray);
            return byteArray.Length;
        }
  private List<string> SetUpURLList()
        {
            List<string> urls = new List<string> 
            { 
                "http://msdn.microsoft.com",
                "http://msdn.microsoft.com/en-us/library/hh290136.aspx",
                "http://msdn.microsoft.com/en-us/library/ee256749.aspx",
                "http://msdn.microsoft.com/en-us/library/hh290138.aspx",
                "http://msdn.microsoft.com/en-us/library/hh290140.aspx",
                "http://msdn.microsoft.com/en-us/library/dd470362.aspx",
                "http://msdn.microsoft.com/en-us/library/aa578028.aspx",
                "http://msdn.microsoft.com/en-us/library/ms404677.aspx",
                "http://msdn.microsoft.com/en-us/library/ff730837.aspx"
            };
            return urls;
        }

上面的例子很简单了,组合任务Tasks,传给 await Task.WhenAll(Tasks) 这样,多个await 就并行得到了执行。

从上面的例子,我们看得出,每个嵌套的方法里,都是层层向上await,这就是await链,不仅要作标记在子线程完成时,在此处”唤醒“同时达到快速响应调用线程,逐层向上返回,结果只有一个,最终让最外层的调用者及时响应,而不用等待,就像MVC原理一样,提高“吞吐量”。

但是中间有一个方法,没向上await,被调用者依然是按照执行的方式决定是同步还是异步。被调者,要是有返回值的,调用者,是没办法获取到返回值的,因为,我们并没办法知道,此方法是否已完成,所以,可能在以后的某段代码中依然要用await 调用。

--------------------------------------------------------------------------------------------------------------------------------------------
通过最近两天的朋友回复,这篇文章确实没有让读者们仔细理解,可能是因为描述的太多,大家没有抓住中心点,下面,为了清楚的让读者理解,用一个示例流程图来说明

大家都知道,点击事件,可能涉及到I/O耗时的方法, 如果,我们直接用同步方法调用,可能点击事件要等许久才能反应,在此之中,我们的事件是“假死”状态

为什么会“假死”这个要好好想一想,一个UI主线程在忙着!其它的事件又是走UI主线程,肯定让你等等!

这个时候,就有一个异步的解决思路,主线程UI遇到耗时的方法,寻找另外一个线程帮忙,主线程得以继续往下运行,主线程往下运行结束了,可能那个代忙的子线程还没有结束,

主线程不能等子线程,因为如果要继续等待,此时如果要给主线程事件,那么,主线程得不到及时响应,因此,主线程要快速的通知系统,我完成了,可以继续干其它事件。

但是问题来了,在某个时候,以前的子线程忙完了,他要把忙完的工作结果交给以前的主线程,就得唤醒以前的“主线程”。

下面通过实例来分析。

329491-20171030111258027-391364558.png
线路1:调用同步方法DoWorkSync,这里DoWorkSync 在不在主线程上执行(是不是异步方法),要进入到DoWorkSync方法体内检查代码才知道,于是进入到方法体内,仅仅有同步方法  
textBox2.Text = "sysnc method",所以这里,主线程要同步完成此代码段,在DoWorkSync同步代码完成后,进入到线路2
线路2:代码运行至 await DoWorkAsync(),系统开始检查DoWorkAsync方法片断,await 标记只有在运行至异步的代码段才会打标记成功,假如
DoWorkAsync里面有同步代码,系统会先把同步代码执行完,巧合的是,里面确实有一个同步的代码片段

private async Task<string> DoWorkAsync()
        {

            DoWorkSync();

            return await Task.Run(() => { return "Done with work!"; });
        }
线路3:此时必须把同步方法DoWorkSync方法执行完为止,这期间都是运行在主线程上的。

线路4:当运行至Task.Run(() => { return "Done with work!"; });时,系统认为,此代码片段开启了子线程,假如说 “Done with work!”是相当耗时的任务,并同时返回结果,

主线程应该把在此处作一个标记,以后这个子线程完成时,将由此继续执行以后的代码。此时是直接return了,并没有下面的代码,读者可以在此后继续写入其它方法。

既然都return了,为啥还要用await呢?此时的await就比较聪明了,他执行的是挂起此行代码,明确的告诉调用主线程不用等待,并马上返回主线程。

这儿要着重得说下,为什么能够不用等待,因为这行代码Task.Run()是开启了子线程,也就是说,把任务交给了子线程,所以主线程才得以自己解脱出来,主线程要管的是子线程完成后,要提醒。当然也可以不提醒。

主要看主线程要不要子线程的结果了。如何提醒?await作了一个标记,以后就从这儿提醒。

线路4:此时await 相当于我们常用的“return”,通过await链路向上返回,就是我们看到的线程4

线路5:当返回到button_click方法体内时,检查 DoWorkAsync()片段已检查过了,里面确实有异步方法,委托到了子线程,于是awiat 要在button处作个标记,以后这里可能会有子线程返回结果给到此处,

如果不要DoworkAsync()子线程的结果,直接不用加await,那么,可以继续执行下面的同步方法DoWorkSync(),而我们这里加了,说明,我们对于这里的子线程结果“很重视”,必须要拿到,才能继续下面的其它方法。

此时有读者会说,在此处等结果,不就阻碍了下面的代码运行了吗?会不会阻碍主线程?首先await 会阻止了下面的同步代码运行,但不会影响主线的响应,因为awiat 对button_click说明了,这个方法是耗时的,

不用等待,于是buttom_click 才会继续向上返回await链,进入线路5,线路5,会对上层系统说,buttom_click 事件“暂时”已完成,可以让系统干其它事了。

线路6:当Task.Run(() => { return "Done with work!"; });子任务完成了,它他要通知“以前的主线程”,系统会分配“以前的主线程”于是进入了线路7
线路7:很简单,直接在button_click 里的await处 得以唤醒,继续执行以下的代码,此时可以拿到子线程的结果。

线路8:继续执行同步方法

线路9:全部方法运行结束后,通知系统,所有任务完成。

希望读者通过这个实例流图有个理性的认识await。


小结:await与async并不能决定方法是同步还是异步,而真正执行异步的还是靠Task、异步委托或其它方式,await的主要作用是,
挂机耗时异步方法,把控制权及时的交给调用者,并在被调用者完成任务时,能够在此唤醒,并继续执行其它方法。
本节的内容,部分例子只起到说明作用,来原于实践的验证,由于时间仓促,并没有提供完整的案例。
同时本节内容主要用简单的语言来加以说明,希望能给读者阐明原理,如果读者希望更清楚的知道await和async,可以查看源代码


如果对于异步的更多了解请参考:

大话异步与并行(一)

大话异步与并行(二)

大话异步与并行(三)

本文部分实例参考 

await(C# 参考) https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/await

使用 Async 和 Await 的异步编程 (C#)  https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/index

Task.WhenAll 方法 (IEnumerable<Task>)  https://msdn.microsoft.com/zh-cn/library/windows/apps/hh160384(v=vs.110)

作者:谷歌's谷歌's博客园
出处:http://www.cnblogs.com/laogu2/ 欢迎转载,但任何转载必须保留完整文章,在显要地方显示署名以及原文链接。如您有任何疑问或者授权方面的协商,请给我留言

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK