本篇翻译的英文链接:https://docs.microsoft.com/en-us/dotnet/articles/standard/async-in-depth

使用.NET的基于任务的异步编程模型,可以直观地编写处理I/O密集型或是CPU计算密集型问题的异步代码。这个模型暴露了Task和Task<T>类型,以及async和await两个语言关键字给外界使用。这篇文章解释了如何使用.NET async模型,以及探究表面之下的async框架的工作原理。

Task and Task<T>

Tasks这种构造,是用来实现众所周知的"Promise Model of Concurrency"模型的。简单来说,它们"承诺"工作会在后续的某个时间点完成,允许你使用更干净的API编写代码以达成目标。

* Task表示一个不返回任何值的操作。

* Task<T>表示返回值为类型T的操作。

把Tasks看作一段异步进行的操作的抽象,而不是基于线程的抽象,是非常重要的。默认情况下,Tasks在当前线程上执行并且视情况将工作委托给OS。也可以显式调用Task.Run接口以要求Tasks在一个单独的线程上运行。

对一个task(任务),Tasks提供了API用于监控,等待和访问返回值(使用Task<T>时)。而关键字await则在语言层面上为tasks的使用提供了更高层别的抽象。

使用await允许你的应用程序或服务在运行一个task时,转让控制权给此task的调度者以更有效地工作,直至此task执行完毕。而你的代码也不需要再以回调或是事件的方式来处理task结束后的工作,Task API与语言层面的结合已经帮你做了这些。如果你使用Task<T>的话,那么await在task结束时会抽离出返回结果Task<T>中的T值。工作细节会在下面进一步解释。

你可以通过"Task-based Asynchronous Pattern(TAP) Article"来学习更多关于Tasks的知识,以及与其交互的不同途径。

Deeper Dive into Tasks for an I/O-Bound Operation

下面的小节描述了一个经典的async I/O场景。先看几个例子。

第一个例子调用一个async方法并且返回一个可能尚未完成的活动任务。

 public Task<string> GetHtmlAsync()
{
// Execution is synchronous here
var client = new HttpClient(); return client.GetStringAsync("http://www.dotnetfoundation.org");
}

第二个例子在task的控制上添加了async和await关键字的使用。

 public async Task<string> GetFirstCharactersCountAsync(string url, int count)
{
// Execution is synchronous here
var client = new HttpClient(); // Execution of GetFirstCharactersCountAsync() is yielded to the caller here
// GetStringAsync returns a Task<string>, which is *awaited*
var page = await client.GetStringAsync("http://www.dotnetfoundation.org"); // Execution resumes when the client.GetStringAsync task completes,
// becoming synchronous again. if (count > page.Length)
{
return page;
}
else
{
return page.Substring(, count);
}
}

方法GetStringAsync()会调用一系列的底层.net库(可能会调用其它的async方法)直到它通过P/Invoke方式调用到一个本地网络库。这个本地库可能会后续调用系统API(比如Linux上调用socket的write)。在本地/托管的交互边界,会创建一个task对象,并被层层向上传递,中途可能会被操作或是直接返回,最后返回给初始的caller。

在第二个例子中,GetStringAsync方法会返回一个Task<T>对象。使用await时,此方法会返回一个新创建的task对象。在这个点,控制权会移交给GetFirstCharactersCountAsync方法的caller。Task<T>的方法和属性使得callers可以监控这个task的进度,这个task会在GetFirstCharactersCountAsync方法的剩余代码执行后才真正结束。

系统API调用之后,请求正处在内核态通往OS的网络子系统的途中(比如linux内核的/net)。在这里,OS依然会异步处理这个网络请求。具体细节可能会依赖于具体平台而有不同,但最后运行时都会收到网络请求正在进行中的通知。此时,设备驱动可能正在调度处理,也有可能已经处理结束了(请求已经结束了传输--原文是the request is already out "over the wire")--但是因为这些都是异步发生的,设备驱动因而有能力立即处理其它的事情!

举个例子,在Windows中一个OS线程调用网络设备驱动并请求它通过IRP(Interrupt Request Packet)的方式处理网络操作。设备驱动收到IRP后,将请求转发给网络层,并将IRP打上"pending"(等待)的标记,然后返回到OS。因为OS线程知道IRP正在"pending",所以不需要再做什么工作并返回,以便处理其它的工作。

当请求被处理,数据通过设备驱动返回后,它通过中断通知CPU新数据的到达。至于这个中断是如何处理的,依赖于具体的OS,但是最后这份数据会通过OS传递直到它到达一个系统交互调用层。注意这些依然是异步发生的!结果被入队直至被下一个有空闲的线程调用相应的异步方法,并且将结果从已完成的任务剥离出来。

整个的处理过程,一个关键点是:在处理任务的过程中,没有线程在等待,没有浪费。虽然工作是在一定的上下文中处理的(比如,OS必须把数据传递给设备驱动并且应答中断),但没有任何线程浪费在等待数据从请求至返回的过程中。这可以大大提升系统的吞吐量,而不是把时间浪费在等待I/O调用完成上。

上述看来,似乎有很多的工作要做,但是用时钟周期来衡量时,与实际的I/O工作所花费的时间相比,其花费是相当小的。虽然不能很精确地描述,但是像这种调用的时间轴看起来可以是下面这个样子的:

0-1--------2-3

*从时间点0到时间点1,所有的事情都在被处理,直到一个异步方法将控制权移交给它的caller。

*从时间点1到时间点2是花费在I/O上的时间,没有任何CPU的开销。

*最后,从时间点2到时间点3这段时间内,是将控制权(也包括可能的返回值)交回给异步方法,并继续执行。

What does this mean for server scenario?(在服务器场合的意义)

这个模型在典型的服务器场景下工作良好。因为没有任何线程会阻塞在未完成的任务上,所以服务器的线程池可以处理更高数量的web请求。

考虑两种服务器:一种是运行async code,另一种不运行。这个例子中我们假设每台server只有5个线程有能力处理服务请求。注意这个数量很小并且工作在很普通的应用场景中。

假设两台server都同时收到6个并发请求。每个请求会导致一个I/O操作。没有async code的server必须缓存第6个请求直到5个线程处理完所有的I/O操作并给予了应答。假设在这个点进来了第20个请求,这台server的处理速度很可能开始变慢,因为请求队列已经变得太长了。

使用async方式编写的server依然会将第6个请求入队,但是因为它使用async和await关键字,每个使用它的线程在I/O工作开始时都会被释放,而不是等I/O完成才释放。当第20个请求到来时,接收进来的请求的队列要小得多,server也不会出现变慢的情况。

虽然这只是个人为的例子,但是真实世界中的工作方式与它是很相像的。事实上,当server以async和await方式处理大量的请求时,你可以认为在处理每个请求时,它是以单线程的方式来工作的。

What does this mean for client scenario?(在客户端场合的意义)

对于客户端应用来说,使用async和await带来的最大好处莫过于在响应上的提升。尽管你可以通过手动开启额外的线程来提升应用的响应能力,但与使用async和await相比,开启新线程的代价是很大的。尤其对于移动游戏这种来说,尽可能减少对UI线程的压力是非常关键的。

更重要的是,因为I/O密集型的工作基本不花费CPU任何时间,占用一个完整的CPU线程而几乎不能做什么有用的工作是对资源的严重浪费。

另外,使用async方法将工作分派给UI线程是非常简单的,而且不需要做任何额外多余的工作(比如调用一个线程安全的委托等)。

Deeper Dive into Task and Task for a CPU-Bound Operation

    使用async编写CPU密集型问题的代码与编写I/O密集型的代码有一点不同。因为工作是在CPU上完成的,在执行计算时是无法将CPU线程释放出来的。async和await提供了一种清晰的与后台线程的交互方式,同时又能使async方法的调用者保持响应。注意其并不为共享数据提供任何保护。如果你正在使用共享数据,你依然需要应用合适的数据同步策略。

下面是CPU密集型的async方法演示:

 public async Task<int> CalculateResult(InputData data)
{
// This queues up the work on the threadpool.
var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data)); // Note that at this point, you can do some other work concurrently,
// as CalculateResult() is still executing! // Execution of CalculateResult is yielded here!
var result = await expensiveResultTask; return result;
}

CalculateResult方法是在调用者的线程上执行的。当其调用Task.Run方法,它将代价昂贵的CPU操作DoExpensiveCalculation向线程池中入队,并返回一个Task<int>句柄。DoExpensiveCalculation()最终会在下一个可用线程上并发执行,可能在另一个单独的CPU核上。当DoExpensiveCalculation()在另一个线程上执行时,是可以并发地做其它工作的,因为调用CalculateResult()的线程还在跑。

一旦执行到await时,CalculateResult()的执行便会移交给它的调用者,允许DoExpensiveCalculation()计算结果时,当前线程依然可以执行其它的工作。一旦计算结束,结果会被入队,等待在主线程上运行。最后,主线程会返回到DoExpensiveCalculation的执行点,取得结果,并继续向下运行。

Why does async help here?

    当你需要处理CPU密集型的工作,但是又需要响应能力时,async和await是最佳实践。使用async编写CPU密集型工作时,有几个模式可供参考。需要注意的是,使用async不是完全没有开销的,不建议在紧凑的循环中使用。至于如何使用这个新能力来编写代码,那就看你自己的了。

最新文章

  1. C# BS消息推送 SignalR介绍(一)
  2. Java 性能分析工具 , 第 2 部分:Java 内置监控工具
  3. 百度地图JavaScript API [一]
  4. 设计模式--代理(Proxy)模式
  5. bugzilla_firefox
  6. c 深度剖析 6
  7. Cocos2d-x 3.1.1 学习日志8--2分钟让你知道cocos2d-x3.1.1 文本类别
  8. iOS_数据库3_sqlite3基本操作
  9. ylb:SQLServer常用系统函数-字符串函数、配置函数、系统统计函数
  10. dapper 扩展插件: Rainbow
  11. 【 js 算法类】数组去重
  12. C语言对齐
  13. 局部加权回归LOWESS
  14. Netty4 学习笔记之二:客户端与服务端心跳 demo
  15. C++函数的重载
  16. Linux 包管理
  17. VMare Workstation 安装Ubuntu 虚拟机教程
  18. 如何在Mac上搭建自己的服务器——Nginx
  19. (1) 天猫精灵接入Home Assistant- 网站论坛
  20. Android四大组件总结

热门文章

  1. Spring Batch 文档(中文)
  2. 初学Android,BroadcastReceiver之发送接收广播
  3. git status检测不到文件变化
  4. NOIPSB评测机+SB题DAY2
  5. slf4j 搭配 log4j2 处理日志
  6. pycharm整体缩进的快捷键
  7. 转: Code Review 程序员的寄望与哀伤
  8. 初探STL之算法
  9. XStream 数组(List)输出结构
  10. ormlite