zoukankan      html  css  js  c++  java
  • .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 : http://files.cnblogs.com/files/08shiyan/ParallelDemo.zip

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


    (未完待续...)

  • 相关阅读:
    当前form窗体随系统屏幕变化
    js类型检测
    js和jquery中的遍历对象和数组(forEach,map,each)
    学了display:flex垂直居中容易多了
    php发送get和post请求
    php输出日志的实现
    php每天自动备份数据库
    windows计划任务
    Css Sprite 图片等比缩放图片大小
    使用js编写一个简单的运动框架
  • 原文地址:https://www.cnblogs.com/08shiyan/p/6093992.html
Copyright © 2011-2022 走看看