本随笔续接:.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();
}
// 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 = ; i < ; 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

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

(未完待续...)

.NET 实现并行的几种方式(四)的更多相关文章

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

    本随笔续接:.NET 实现并行的几种方式(一) 四.Task 3)Task.NET 4.5 中的简易方式 在上篇随笔中,两个Demo使用的是 .NET 4.0 中的方式,代码写起来略显麻烦,这不 .N ...

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

    本随笔续接:.NET 实现并行的几种方式(二) 在前两篇随笔中,先后介绍了 Thread .ThreadPool .IAsyncResult (即 APM系列) .Task .TPL (Task Pa ...

  3. Android提交数据到服务器的两种方式四种方法

    本帖最后由 yanghe123 于 2012-6-7 09:58 编辑 Android应用开发中,会经常要提交数据到服务器和从服务器得到数据,本文主要是给出了利用http协议采用HttpClient方 ...

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

    好久没有更新了,今天来一篇,算是<同步与异步>系列的开篇吧,加油,坚持下去(PS:越来越懒了). 一.Thread 利用Thread 可以直接创建和控制线程,在我的认知里它是最古老的技术了 ...

  5. ios 页面传值4种方式(四) 之通过delegate(代理)

    这是ios里最常用的设计模式了,简直贯穿了整个cocoa touch框架.废话不多说,直接上代码: 场景是: A--打开--B; B里输入数值,点击--返回--A; A里显示B输入的值; △在开始写之 ...

  6. Solon 开发,四、Bean 扫描的三种方式

    Solon 开发 一.注入或手动获取配置 二.注入或手动获取Bean 三.构建一个Bean的三种方式 四.Bean 扫描的三种方式 五.切面与环绕拦截 六.提取Bean的函数进行定制开发 七.自定义注 ...

  7. Android请求服务器的两种方式--post, get的区别

    android中用get和post方式向服务器提交请求_疯狂之桥_新浪博客http://blog.sina.com.cn/s/blog_a46817ff01017yxt.html Android提交数 ...

  8. Android发送数据到web服务器4种方式

    1./** 2. * Android中向web服务器提交数据的两种方式四种方法 3. */ 4.public class SubmitDataByHttpClientAndOrdinaryWay { ...

  9. ajax处理跨域有几种方式

    一.什么是跨域 同源策略是由Netscape提出的著名安全策略,是浏览器最核心.基本的安全功能,它限制了一个源(origin)中加载文本或者脚本与来自其他源(origin)中资源的交互方式,所谓的同源 ...

随机推荐

  1. windows环境下sublime的nodejs插件详细安装图解

    前面的话   搜索了好多文档后,才成功地安装了sublime text3的nodejs插件.为了存档,也为了方便有同样需求的朋友,将其安装过程详细记录如下 安装nodejs 虽然nodejs官网提供了 ...

  2. lua 学习笔记(1)

    一.lua函数赋值与函数调用         在lua中函数名也是作为一种变量出现的,即函数和所有其他值一样都是匿名的,当要使用某个函数时,需要将该函数赋值给一个变量,这样在函数块的其他地方就可以通过 ...

  3. 算法与数据结构(十七) 基数排序(Swift 3.0版)

    前面几篇博客我们已经陆陆续续的为大家介绍了7种排序方式,今天博客的主题依然与排序算法相关.今天这篇博客就来聊聊基数排序,基数排序算法是不稳定的排序算法,在排序数字较小的情况下,基数排序算法的效率还是比 ...

  4. [C#] C# 知识回顾 - 表达式树 Expression Trees

    C# 知识回顾 - 表达式树 Expression Trees 目录 简介 Lambda 表达式创建表达式树 API 创建表达式树 解析表达式树 表达式树的永久性 编译表达式树 执行表达式树 修改表达 ...

  5. ZKWeb网页框架1.2正式发布

    发行日志 https://github.com/zkweb-framework/ZKWeb/blob/master/ReleaseNotes/ReleaseNote.1.2.md 主要改动 更新 ZK ...

  6. Jqprint实现页面打印

    好些项目需要实现页面打印,特别是一些后台管理类系统,下面介绍一款轻量级的打印插件: 1.实现页面打印要引入jQuery和Jqprint.点击下载Jqprint插件 <script languag ...

  7. JAVA装饰者模式(从现实生活角度理解代码原理)

    装饰者模式可以动态地给一个对象添加一些额外的职责.就增加功能来说,Decorator模式相比生成子类更为灵活. 该模式的适用环境为: (1)在不影响其他对象的情况下,以动态.透明的方式给单个对象添加职 ...

  8. Android listview和gridview以及view的区别

    GridView 可以指定显示的条目的列数. listview一般显示的条目的列数都是一列 如果是列表(单列多行形式)的使用ListView,如果是多行多列网状形式的优先使用GridView andr ...

  9. mysql开启慢查询日志及查询--windows

    MySQL慢查询配置 1. 慢查询有什么用? 它能记录下所有执行超过long_query_time时间的SQL语句, 帮你找到执行慢的SQL, 方便我们对这些SQL进行优化. 2. 如何开启慢查询? ...

  10. Linux:将rhel yum 切换到centos yum

    Red Hat Enterprise Linux Server(RHEL) yum安装软件时This system is not registered with RHN. RHN support wi ...