本随笔续接:.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. mybatis_个人总结

    在使用mybatis框架开发数据访问层的过程中,我在这段时间遇到很多细节问题困住我,在这里我来分享一下我遇到的坑,希望能帮到大家. 一.mybatis动态代理方式开发的规范: 1.注意在mybatis ...

  2. Java进击C#——前言

    本章简言 记得三年前笔者来到现在的公司的时候,公司人口不出十个人.那个时候笔者刚从日本回来,想在福州.厦门.青岛找一个合适自己发展的机会.最后我的一个福州的朋友打电话希望我能过去帮他,跟他一起创业.这 ...

  3. C++整数转字符串的一种方法

    #include <sstream> //ostringstream, ostringstream::str() ostringstream stream; stream << ...

  4. 2Sum

    用哈希表(unordered_map)使得时间复杂度从O(n*n)降到O(n),空间复杂度从O(1)增到O(n):一边找一边插入哈希表 注意 在C++11以前要使用unordered_map需要 #i ...

  5. [转载]Jquery中$.get(),$.post(),$.ajax(),$.getJSON()的用法总结

    本文对Jquery中$.get(),$.post(),$.ajax(),$.getJSON()的用法进行了详细的总结,需要的朋友可以参考下,希望对大家有所帮助. 详细解读Jquery各Ajax函数: ...

  6. 网站里加入QQ在线客服

    1.开启"QQ在线状态"服务  http://jingyan.baidu.com/article/b24f6c823425a586bfe5da1f.html http://www. ...

  7. WiFi QC 自动测试:ixChariot API初探

    Chariot虽然给我们提供了友好的界面,但是必须使用命令行或者使用它的API才能 实现自动测试.Chariot在安装的时候会让你选择命令行界面组件,在它的安装目录下面有一些工具, 暂时还不知道是干什 ...

  8. js动态加载css和js

    之前写了一个工具类点此链接里面含有这段代码,感觉用处挺多,特意提出来 var loadUtil = { /* * 方法说明:[动态加载js文件css文件] * 使用方法:loadUtil.loadjs ...

  9. CentOS7 + mono +Jexus 环境的搭建

    CentOS7的安装和配置 1,从http://www.centos.org/下载CentOS7的镜像,并在VMWare中创建该镜像的虚拟机,为方便操作,把虚拟机的网络连接设置为桥接模式:在安装过程中 ...

  10. 在多线程编程中lock(string){...}隐藏的机关

    常见误用场景:在订单支付环节中,为了防止用户不小心多次点击支付按钮而导致的订单重复支付问题,我们用 lock(订单号) 来保证对该订单的操作同时只允许一个线程执行. 这样的想法很好,至少比 lock( ...