C#5.0新增功能01 异步编程
如果需要 I/O 绑定(例如从网络请求数据或访问数据库),则需要利用异步编程。 还可以使用 CPU 绑定代码(例如执行成本高昂的计算),对编写异步代码而言,这是一个不错的方案。C# 拥有语言级别的异步编程模型,它使你能轻松编写异步代码,而无需应付回叫或符合支持异步的库。 它遵循基于任务的异步模式 (TAP)。
异步编程的核心是 Task
和 Task<T>
对象,这两个对象对异步操作建模。 它们受关键字 async
和 await
的支持。 在大多数情况下模型十分简单:
对于 I/O 绑定代码,当你 await
一个操作,它将返回 async
方法中的一个 Task
或 Task<T>
。
对于 CPU 绑定代码,当你 await
一个操作,它将在后台线程通过 Task.Run
方法启动。
await
关键字有这奇妙的作用。 它控制执行 await
的方法的调用方,且它最终允许 UI 具有响应性或服务具有灵活性。
除上方链接的 TAP 文章中介绍的 async
和 await
之外,还有其他处理异步代码的方法,但本文档将在下文中重点介绍语言级别的构造。
I/O 绑定示例:从 Web 服务下载数据
你可能需要在按下按钮时从 Web 服务下载某些数据,但不希望阻止 UI 线程。 只需执行如下操作即可轻松实现:
private readonly HttpClient _httpClient = new HttpClient(); downloadButton.Clicked += async (o, e) =>
{
// 当来自Web服务的请求发生时,此行将向UI提供控制权。
// UI线程现在可以自由执行其他工作
var stringData = await _httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
就是这么简单! 代码表示目的(异步下载某些数据),而不会在与任务对象的交互中停滞。
CPU 绑定示例:为游戏执行计算
假设你正在编写一个移动游戏,在该游戏中,按下某个按钮将会对屏幕中的许多敌人造成伤害。执行伤害计算的开销可能极大,而且在 UI 线程中执行计算有可能使游戏在计算执行过程中暂停!
此问题的最佳解决方法是启动一个后台线程,它使用 Task.Run
执行工作,并 await
其结果。 这可确保在执行工作时 UI 能流畅运行。
private DamageResult CalculateDamageDone()
{
// ··· 省略的业务逻辑代码
//
//执行昂贵的计算并返回该计算的结果。
} calculateButton.Clicked += async (o, e) =>
{
// 此行将在计算 damagedone()执行其工作时向UI提供控制权。
// UI线程现在可以自由执行其他工作
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
就是这么简单! 此代码清楚地表达了按钮的单击事件的目的,它无需手动管理后台线程,而是通过非阻止性的方式来实现。
内部原理
异步操作涉及许多移动部分。 若要了解 Task
和 Task<T>
的内部原理,请参阅深入了解异步,以获取详细信息。
在 C# 方面,编译器将代码转换为状态机,它将跟踪类似以下内容:到达 await
时暂停执行以及后台作业完成时继续执行。
从理论上讲,这是异步的承诺模型的实现。
- 异步代码可用于 I/O 绑定和 CPU 绑定代码,但在每个方案中有所不同。
- 异步代码使用
Task<T>
和Task
,它们是对后台所完成的工作进行建模的构造。 async
关键字将方法转换为异步方法,这使你能在其正文中使用await
关键字。- 应用
await
关键字后,它将挂起调用方法,并将控制权返还给调用方,直到等待的任务完成。 - 仅允许在异步方法中使用
await
。
前两个示例演示如何将 async
和 await
用于 I/O 绑定和 CPU 绑定工作。 确定所需执行的操作是 I/O 绑定或 CPU 绑定是关键,因为这会极大影响代码性能,并可能导致某些构造的误用。
以下是编写代码前应考虑的两个问题:
你的代码是否会“等待”某些内容,例如数据库中的数据?
如果答案为“是”,则你的工作是 I/O 绑定。
你的代码是否要执行开销巨大的计算?
如果答案为“是”,则你的工作是 CPU 绑定。
如果你的工作为 I/O 绑定,请使用 async
和 await
(而不使用 Task.Run
)。 不应使用任务并行库 。 相关原因在深入了解异步的文章中说明。
如果你的工作为 CPU 绑定,并且你重视响应能力,请使用 async
和 await
,并在另一个线程上使用 Task.Run
生成工作。 如果该工作同时适用于并发和并行,则应考虑使用任务并行库。
此外,应始终对代码的执行进行测量。 例如,你可能会遇到这样的情况:多线程处理时,上下文切换的开销高于 CPU 绑定工作的开销。 每种选择都有折衷,应根据自身情况选择正确的折衷方案。
如果打算在生产代码中进行 HTML 分析,则不要使用正则表达式。 改为使用分析库。
private readonly HttpClient _httpClient = new HttpClient(); [HttpGet]
[Route("DotNetCount")]
public async Task<int> GetDotNetCountAsync()
{
// 挂起 GetDotNetCountAsync()方法,以允许调用方(Web服务器)接受另一个请求,而不是阻止此请求。
var html = await _httpClient.GetStringAsync("https://dotnetfoundation.org"); return Regex.Matches(html, @"\.NET").Count;
}
以下是为通用 Windows 应用编写的相同方案,当按下按钮时,它将执行相同的任务:
private readonly HttpClient _httpClient = new HttpClient(); private async void SeeTheDotNets_Click(object sender, RoutedEventArgs e)
{
// 在这里捕获任务句柄,以便稍后等待后台任务
var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://www.dotnetfoundation.org"); // 用户界面线程上的任何其他工作都可以在这里完成,例如启用进度条。
// 在“等待”调用之前,这一点很重要,这样用户就可以在生成此方法的执行之前看到进度条。
NetworkProgressBar.IsEnabled = true;
NetworkProgressBar.Visibility = Visibility.Visible; // await 操作符挂起 SeeTheDotNets_Click 事件,将控制权返回给调用方。
// 这使得应用程序能够响应而不阻塞UI线程。
var html = await getDotNetFoundationHtmlTask;
int count = Regex.Matches(html, @"\.NET").Count; DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}"; NetworkProgressBar.IsEnabled = false;
NetworkProgressBar.Visibility = Visibility.Collapsed;
}
等待多个任务完成
你可能发现自己处于需要并行检索多个数据部分的情况。 Task
API 包含两种方法(即 Task.WhenAll
和 Task.WhenAny
),这些方法允许你编写在多个后台作业中执行非阻止等待的异步代码。
此示例演示如何为一组 User
捕捉 userId
数据。
public async Task<User> GetUserAsync(int userId)
{
// ··· 省略的业务逻辑代码
// 给定用户Id {userId},检索与数据库中条目对应的用户对象,其中 {userId}作为其ID
} public static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>(); foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
} return await Task.WhenAll(getUserTasks);
}
以下是使用 LINQ 进行更简洁编写的另一种方法:
public async Task<User> GetUserAsync(int userId)
{
// ··· 省略的业务逻辑代码
// 给定用户Id {userId},检索与数据库中条目对应的用户对象,其中 {userId}作为其ID
} public static async Task<User[]> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id));
return await Task.WhenAll(getUserTasks);
}
尽管它的代码较少,但在混合 LINQ 和异步代码时需要谨慎操作。 因为 LINQ 使用延迟的执行,因此异步调用将不会像在 foreach()
循环中那样立刻发生,除非强制所生成的序列通过对 .ToList()
或 .ToArray()
的调用循环访问。
尽管异步编程相对简单,但应记住一些可避免意外行为的要点。
async
方法需在其主体中具有await
关键字,否则它们将永不暂停!
这一点需牢记在心。 如果 await
未用在 async
方法的主体中,C# 编译器将生成一个警告,但此代码将会以类似普通方法的方式进行编译和运行。 请注意这会导致效率低下,因为由 C# 编译器为异步方法生成的状态机将不会完成任何任务。
- 应将“Async”作为后缀添加到所编写的每个异步方法名称中。
这是 .NET 中的惯例,以便更轻松区分同步和异步方法。 请注意,未由代码显式调用的某些方法(如事件处理程序或 Web 控制器方法)并不一定适用。 由于它们未由代码显式调用,因此对其显式命名并不重要。
async void
应仅用于事件处理程序。
async void
是允许异步事件处理程序工作的唯一方法,因为事件不具有返回类型(因此无法利用 Task
和 Task<T>
)。 其他任何对 async void
的使用都不遵循 TAP 模型,且可能存在一定使用难度,例如:
async void
方法中引发的异常无法在该方法外部被捕获。十分难以测试
async void
方法。如果调用方不希望
async void
方法是异步方法,则这些方法可能会产生不好的副作用。在 LINQ 表达式中使用异步 lambda 时请谨慎
LINQ 中的 Lambda 表达式使用延迟执行,这意味着代码可能在你并不希望结束的时候停止执行。如果编写不正确,将阻塞任务引入其中时可能很容易导致死锁。 此外,此类异步代码嵌套可能会对推断代码的执行带来更多困难。 Async 和 LINQ 的功能都十分强大,但在结合使用两者时应尽可能小心。
- 采用非阻止方式编写等待任务的代码
将阻止当前线程作为等待任务完成的方法可能导致死锁和已阻止的上下文线程,且可能需要更复杂的错误处理。 下表提供了关于如何以非阻止方式处理等待任务的指南:
使用以下方式... | 而不是… | 若要执行此操作 |
---|---|---|
await |
Task.Wait 或 Task.Result |
检索后台任务的结果 |
await Task.WhenAny |
Task.WaitAny |
等待任何任务完成 |
await Task.WhenAll |
Task.WaitAll |
等待所有任务完成 |
await Task.Delay |
Thread.Sleep |
等待一段时间 |
- 编写状态欠缺的代码
请勿依赖全局对象的状态或某些方法的执行。 请仅依赖方法的返回值。 为什么?
- 这样更容易推断代码。
- 这样更容易测试代码。
- 混合异步和同步代码更简单。
- 通常可完全避免争用条件。
- 通过依赖返回值,协调异步代码可变得简单。
- (好处)它非常适用于依赖关系注入。
建议的目标是实现代码中完整或接近完整的引用透明度。 这么做能获得高度可预测、可测试和可维护的基本代码。
其他资源
- 深入了解异步提供了关于任务如何工作的详细信息。
- 使用 Async 和 Await 的异步编程 (C#)
- 由 Lucian Wischik 所著的 Six Essential Tips for Async(关于异步的六个要点)是有关异步编程的绝佳资源
C#5.0新增功能01 异步编程的更多相关文章
- C#2.0新增功能01 分布类与分部方法
连载目录 [已更新最新开发文章,点击查看详细] 分部类型 拆分一个类.一个结构.一个接口或一个方法的定义到两个或更多的文件中, 每个源文件包含类型或方法定义的一部分,编译应用程序时将把所有部分组 ...
- C#4.0新增功能01 动态绑定 (dynamic 类型)
连载目录 [已更新最新开发文章,点击查看详细] C# 4 引入了一个新类型 dynamic. 该类型是一种静态类型,但类型为 dynamic 的对象会跳过静态类型检查. 大多数情况下,该对象就像 ...
- C#3.0新增功能01 自动实现的属性
连载目录 [已更新最新开发文章,点击查看详细] 在 C# 3.0 及更高版本,当属性访问器中不需要任何其他逻辑时,自动实现的属性会使属性声明更加简洁. 它们还允许客户端代码创建对象. 当你声明以 ...
- C#基础拾遗系列之二:使用ILSpy探索C#7.0新增功能点
C#基础拾遗系列之二:使用ILSpy探索C#7.0新增功能点 第一部分: C#是一种通用的,类型安全的,面向对象的编程语言.有如下特点: (1)面向对象:c# 是面向对象的范例的一个丰富实现, 它 ...
- C#2.0新增功能06 协变和逆变
连载目录 [已更新最新开发文章,点击查看详细] 在 C# 中,协变和逆变能够实现数组类型.委托类型和泛型类型参数的隐式引用转换. 协变保留分配兼容性,逆变则与之相反. 以下代码演示分配兼容性.协 ...
- C#8.0 新增功能
连载目录 [已更新最新开发文章,点击查看详细] C#8.0提供了许多增强功能 01 Readonly 成员 可将 readonly 修饰符应用于结构的任何成员. 它指示该成员不会修改状态. 这比 ...
- C#7.0 新增功能
连载目录 [已更新最新开发文章,点击查看详细] C# 7.0 向 C# 语言添加了许多新功能 01 out 变量 支持 out 参数的现有语法已在此版本中得到改进. 现在可以在方法调用的参数列表 ...
- C#基础拾遗系列之二:C#7.0新增功能点
第一部分: C#是一种通用的,类型安全的,面向对象的编程语言.有如下特点: (1)面向对象:c# 是面向对象的范例的一个丰富实现, 它包括封装.继承和多态性.C#面向对象的行为包括: 统一的类型系统 ...
- C#7.0新增功能点
原文地址: https://www.cnblogs.com/runningsmallguo/p/8972678.html 第二部分:C#7.0新增的功能 (1)数字字面量的提升: C#7中的数字文字 ...
随机推荐
- 如何打造VUCA时代的敏捷型组织?
王明兰 --原华为.微软创新与转型教练.华为云SaaS产品总监,著名精益&敏捷转型专家 VUCA最早来源于冷战时期,在现代世界意指商业世界越来越不确定性,越来越易变,越来越不可预测,我们已经进 ...
- DNS查询命令
dig(domain information groper)是一个在类Unix命令行模式下查询DNS,包括NS记录,A记录,MX记录等相关信息的工具 一.简单介绍使用dig命令查询DNS的方法 dig ...
- 深入理解计算机系统 BombLab 实验报告
又快有一个月没写博客了,最近在看<深入理解计算机系统>这本书,目前看完了第三章,看完这章,对程序的机器级表示算是有了一个入门,也对 C 语言里函数栈帧有了一个初步的理解. 为了加深对书本内 ...
- python-基本数据类型(int,bool,str)
一.python基本数据类型 1. int ==> 整数. 主要⽤用来进⾏行行数学运算 2. str ==> 字符串串, 可以保存少量量数据并进⾏行行相应的操作 3. bool==> ...
- 深入V8引擎-AST(2)
先声明一下,这种长系列的大块头博客只能保证尽可能的深入到每一行源码,有些代码我不乐意深究就写个注释说明一下作用.另外,由于本地整理的比较好,博客就随心写了. 整个Compile过程目前只看到asmjs ...
- iOS App开发的那些事儿1:如何建立合适的规范
<iOS App开发的那些事儿>系列文章从更宏观的角度出发,不仅仅局限于具体某个功能.界面的实现,而是结合网易云信iOS端研发负责人多年的经验,从如何优化现有代码的角度出发,深度分析如何创 ...
- Programming In Lua 第四章
1, 2, 3, 4, 5, 6, 7,
- Dubbo源码学习之-SPI介绍
前言 学习之路还是要戒骄戒躁,一以贯之的积累前行.之前的公司部门技术达人少,自己总向往那些技术牛人多的团队,想象自己进去之后能跟别人学到多少东西.如今进到一个这样的团队之后,却发现之前自己的想法过于幼 ...
- ES5_03_Object扩展
ES5给Object扩展了一些静态方法, 常用的2个: 1. Object.create(prototype, [descriptors]) * 作用: 以指定对象为原型创建新的对象 * 为新的对象指 ...
- Js笛卡尔乘积
self.getDescartesSku = function (selSaleProp, i, nowLst, allALst) { if (selSaleProp.length = ...