.NET 实现并行的几种方式(四)

本随笔续接:.NET 实现并行的几种方式(三)


八、await、async - 异步方法的秘密武器

1) 使用async修饰符 和 await运算符 轻易实现异步方法

前三篇随笔已经介绍了多种方式、利用多线程、充分利用多核心CPU以提高运行效率。但是以前的方式在WebAPI和GUI系统上、

使用起来还是有些繁琐,尤其是在需要上下文的情况下。而await/async就是在这样的情况下应运而生,并且它可以在理论上让CPU跑到100%。

async修饰符:它用以修饰方法、lambda表达式、匿名方法,以标记方法为异步方法。异步方法必须遵循的规范如下:

1、返回值仅且仅有三种: void、Task、Task<T>.

2、方法参数不可以使用 ref、out类型参数。

await运算符:它用以标记一个系统可在其上恢复执行的挂起点。该运算符会告诉computer不会再往下继续执行该方法、直到等待的异步方法执行完毕为止。同时会将程序的控制权return给其调用者。await表达式不阻止正在执行它的线程。 而是让编译器将异步方法剩余部分注册为等待任务的延续任务。 当等待任务完成时,它会调用其延续任务,如同在挂起点上恢复执行。

2)简单Demo

// 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");  
  
    // You can do work here that doesn't rely on the string from GetStringAsync.  
    DoIndependentWork();  
  
    // 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;  
  
    // The return statement specifies an integer result.  
    // Any methods that are awaiting AccessTheWebAsync retrieve the length value.  
    return urlContents.Length;  
}  
await/async demo

3)直观的顺序图

该图出自: https://msdn.microsoft.com/zh-cn/library/mt674882.aspx 

4) await async编程最佳做法

1、异步方法尽量少用 void类型返回值、替代方案 使用Task类型,特例:异步事件处理函数使用void类型

原因1、async void 无法使用try ... catch进行异常捕获,它的异常会在上下文中引发。捕获该种异常的方式为在GUI或web系统中使用

AppDomain.UnhandledException 进行全局异常捕获。对于需要进队异常进行处理的地方、这将是个灾难。

原因2、async void 方法、不可以“方便”的知道其什么时候完成,这对于超过50%的异步方法而言、将是灭顶之灾。而 async Task

可以配合 await、await Task.WhenAny、await Task.WhenAll、await Task.Delay、await Task.Yield 方便的进行后续的任务处理工作。

特例、因为事件本身是不需要返回值的,并且事件的异常也会在上下文中引发、这是合理的。所以异步的事件处理函数使用void类型。

 

2、推荐一直使用async,而不要混合使用阻塞和异步(async)避免死锁, 特例:Main方法

使用混合编程的死锁demo

public static class DeadlockDemo
{
  private static async Task DelayAsync()
  {
    await Task.Delay(1000);
  }
  // This method causes a deadlock when called in a GUI or ASP.NET context.
public static void Test()
  {
    // Start the delay.
var delayTask = DelayAsync();
    // Wait for the delay to complete.
delayTask.Wait();
  }
}
DeadlockDemo

当在GUI或者web上执行(具有上下文的环境中),会导致死锁。这种死锁的根本原因是 await 处理上下文的方式。 默认情况下,当等待未完成的 Task 时,会捕获当前“上下文”,在 Task 完成时使用该上下文恢复方法的执行。 此“上下文”是当前 SynchronizationContext(除非它是 null,这种情况下则为当前 TaskScheduler)。 GUI 和 ASP.NET 应用程序具有 SynchronizationContext,它每次仅允许一个代码区块运行。 当 await 完成时,它会尝试在捕获的上下文中执行 async 方法的剩余部分。 但是该上下文已含有一个线程,该线程在(同步)等待 async 方法完成。 它们相互等待对方,从而导致死锁。

特例:Main方法是不可用async修饰符进行修饰的(编译不通过)。

执行以下操作… 阻塞式操作… async的替换操作
检索后台任务的结果 Task.Wait 或 Task.Result await
等待任何任务完成 Task.WaitAny await Task.WhenAny
检索多个任务的结果 Task.WaitAll await Task.WhenAll
等待一段时间 Thread.Sleep await Task.Delay

 

 

 

 

 

3、如果可以,请用ConfigureAwait 忽略上下文

上文也说过了,当异步任务完成后、它会尝试在之前的上下文环境中恢复执行。这样带来的问题是时间片会被切分成更多、造成更多的线程调度上的性能损耗。

一旦时间片被切分的过多、尤其是在GUI和Web具有上下文环境中运行,影响会更大。

另外,使用ConfigureAwait忽略上下文后、可避免死锁。 因为当等待完成时,它会尝试在线程池上下文中执行 async 方法的剩余部分,不会存在线程等待。

 

5)疑问:关于 await的使用次数 和 使用的线程数量 之间的关系

使用一个await运算符,就一定会使用一个新的线程吗? 答案:不是的。

前文已经介绍过,await运算符是依赖Task完成异步的、并且将后续代码至于Task的延续任务之中(这一点是编译器搞得怪、生成了大量的模板代码来实现该功能)。

因此,编译器以await为分割点,将前一部分的等待任务和后一部分的延续任务分割到两个线程之中。

前一部分的等待任务:该部分是Task依赖调度器(TaskScheduler)、从线程池中分配的工作线程。

而后一部分的延续任务:该部分所运行的线程取决于两点:第一点,Task等待任务在运行之前捕获的上下文环境,第二点:是否使用ConfigureAwait (false) 

忽略了之前捕获的上下文。如果没有忽略上下文并且之前捕获的上下文环境为:SynchronizationContext(即 GUI UI线程 或 Web中具有HttpContext的线程环境)

则 延续任务继续在 SynchronizationContext 上下文环境中运行,否则 将使用调度器(TaskScheduler)从线程池中获取线程来运行。

 

另外注意:调度器从线程池中获取的线程、并不一定是新的,即使在循环连续使用多次(如果任务很快完成),那么也有可能多次都使用同一个线程。

测试demo:

        /// <summary>
        /// 在循环中使用await, 观察使用的线程数量
        /// </summary>
        /// <returns></returns>
        public async Task ForMethodAsync()
        {
            // 休眠
            // await Task.Delay(TimeSpan.FromMilliseconds(100)).ConfigureAwait(false);
            // await Task.Delay(TimeSpan.FromMilliseconds(100));

            for (int i = 0; i < 5; i++)
            {
                await Task.Run(() =>
                {
                    // 打印线程id
                    PrintThreadInfo("ForMethodAsync", i.ToString());
                });
            }            
        }
在循环中使用await, 观察使用的线程数量

上述demo在运行多次后,可能会得到上述结果:5次循环使用的是同一个线程,线程id为16,UI线程id为10。

 

结论:await的使用次数 大于 使用的线程数量,也有可能、多次使用await 只会 使用一个线程。

 

6)await/async 的缺点

1、由于编译在搞怪、会生成大量的模板代码、使得单个异步方法 比 单个同步方法 运行得要慢,与之相对应的获取到的性能优势是、充分利用了多核心CPU,提高了任务并发量。

2、掩盖了线程调度、使得系统开发人员无意识的忽略了该方面的性能损耗。

3、如果使用不当,容易造成死锁

 

 

附,Demo : https://files.cnblogs.com/files/08shiyan/ParallelDemo.zip

参见更多:随笔导读:同步与异步


(未完待续...)

 

posted @ 2016-12-01 10:26  把爱延续  阅读(2939)  评论(1编辑  收藏  举报