.NET中的异步编程——常见的错误和最佳实践
目录
背景
async void
没有线程
Foreach和属性
始终异步
在这篇文章中,我们将通过使用异步编程的一些最常见的错误来给你们一些参考。
背景
在之前的文章中,我们开始分析.NET世界中的异步编程。在那篇文章中,我们担心这个概念有点误解,尽管从.NET4.5开始它已经存在了超过6年时间。使用这种编程风格,更容易编写响应式应用程序,这些应用程序都是异步的、非阻塞I / O操作的。这都是通过使用async
/await
操作符完成的。
async void
在阅读之前的文章时,你可能注意到那些使用async标记的方法可以返回Task, Task<T>或拥有可访问的GetAwaiter方法作为结果的任何类型。嗯,这可能有一点误会,因为这些方法能够,事实上,也返回void类型。然而,这是一种我们想要避免的坏的行为,所以我们把它放到最底层。为什么这是一个滥用的概念?虽然它可以在异步方法中返回void类型,但是这些方法的目的是完全不同的。更精确,这种方法有一个非常特定的任务——使异步处理程序是可能的。
虽然可以让事件处理程序返回一些实际类型,但它并不能很好地与语言一起使用,这个概念没有多大意义。除此之外,async void方法的一些语义不同于async Task或async Task<T>方法。例如,异常处理不一样。如果在async Task方法中抛出异常,它将被捕获并放置在Task对象中。如果在async void方法内引发异常,则会直接在处于活动状态的SynchronizationContext上引发异常。
private async void ThrowExceptionAsync()
{throw new Exception("Async exception");
}public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{try{ThrowExceptionAsync();}catch (Exception){// The exception is never caught here!throw;}
}
使用async void时还有两个缺点。首先,这些方法不能提供一种简单的方法来通知他们已经完成的调用代码。而且,由于这第一个缺陷,很难对它们进行测试。单元测试框架(例如xUnit或NUnit)仅适用于返回Task或返回Task<T>的类型的async方法。综合考虑所有这些因素,async void一般来说是不赞成使用的,而async Task建议使用。唯一的异常可能是异步事件处理程序,必须返回void。
没有线程
关于.NET中异步机制的最大误解可能是在后台运行某种异步线程。虽然在等待某些操作时似乎很合乎逻辑,但是正在进行等待的线程,情况并非如此。为了理解这一点,让我们后退几步。当我们使用我们的计算机时,我们有多个程序同时运行,这是通过在CPU上一次运行来自不同进程的指令来实现的。
由于这些指令是交错的并且CPU快速地从一个切换到另一个(上下文切换),我们得到一个错觉,它们同时运行。此过程称为并发。现在,当我们在CPU中拥有多个内核时,我们可以在每个内核上运行多个这些指令流。这称为并行性。现在,重要的是要了解这两个概念在CPU级别上都可用。在操作系统级别,我们有一个线程概念——一系列可由调度程序独立管理的指令集。
那么,为什么我会给你计算机科学101讲座?好吧,因为等待,我们之前谈论的是在线程概念尚未存在的水平上发生的事情。让我们来看看这部分代码,对设备的通用写操作(网络,文件等):
public async Task WriteMyDeviceAcync
{byte[] data = ...myDevice.WriteAsync(data, 0, data.Length);
}
现在,让我们深入的研究一下吧。WriteAsync将在设备的底层HANDLE上启动重叠的I / O操作。之后,OS将调用设备驱动程序并要求它开始写操作。这是通过两个步骤完成的。首先,创建写请求对象——I / O请求包或IRP。然后,一旦设备驱动程序接收到IRP,它就会向实际设备发出命令以写入数据。这里有一个重要的事实,在处理IRP时不允许设备驱动程序阻塞,甚至不允许同步操作。
这是有道理的,因为这个驱动程序也可以获得其他请求,它不应该成为瓶颈。由于没有太多事情可以做,设备驱动程序将IRP标记为“挂起”,并将其返回到OS。IRP现在处于“挂起”状态,因此OS返回WriteAsync。此方法向WriteMyDeviceAcync返回一个不完整的任务,WriteMyDeviceAcync方法挂起async方法,并且调用线程继续执行。
一段时间后,设备完成写入,它向CPU发送一个通知,然后魔术开始发生。这是通过中断完成的,该中断是CPU级事件,将控制CPU。设备驱动程序必须响应此中断,并且它正在ISR - 中断服务程序中执行此操作。作为回报,ISR正在排队称为延迟过程调用(DCP)的东西,它在完成中断后由CPU处理。
DCP将在操作系统级别将IRP标记为“完成”,并且OS将异步过程调用(APC)调度到拥有HANDLE的线程。然后简单地借用I / O线程池线程来执行APC,通知任务完成。UI上下文将捕获此内容并知道如何恢复。
请注意处理等待的指令——ISR和DCP在CPU上直接执行,“低于”OS和“低于”线程的存在。本质上,没有线程,没有OS级别,也没有设备驱动程序级别,这就是处理异步机制。
Foreach和属性
其中一个常见错误是await在foreach循环内部使用。看看这个例子:
var listOfInts = new List<int>() { 1, 2, 3 };foreach (var integer in listOfInts)
{await WaitThreeSeconds(integer);
}
现在,即使这个代码是以异步方式编写的,但是每当等待WaitThreeSeconds时,它将阻塞流的执行。这是一个真实的情况,例如,WaitThreeSeconds正在调用某种Web API,假设它执行HTTP GET请求传递查询数据。有时,我们有这样的情况,我们想这样做,但如果我们这样实现,我们将等待每个请求-响应周期完成,然后再开始一个新的。这是低效的。
这是我们的WaitThreeSeconds功能:
private async Task WaitThreeSeconds(int param)
{Console.WriteLine($"{param} started ------ ({DateTime.Now:hh:mm:ss}) ---");await Task.Delay(3000);Console.WriteLine($"{ param} finished ------({ DateTime.Now:hh: mm: ss}) ---");
}
如果我们尝试运行此代码,我们将得到如下内容:
执行这个代码的时间是九秒。如前所述,这是非常低效的。通常,我们希望这些Tasks中的每一个都被触发,并且所有任务都并行完成(时间稍微超过3秒)。
现在我们可以像这样修改上面的代码:
var listOfInts = new List<int>() { 1, 2, 3 };
var tasks = new List<Task>();foreach (var integer in listOfInts)
{var task = WaitThreeSeconds(integer);tasks.Add(task);
}await Task.WhenAll(tasks);
当我们运行它时,我们会得到这样的东西:
这正是我们想要的。如果我们想用更少的代码编写它,我们可以使用LINQ:
var tasks = new List<int>() { 1, 2, 3 }.Select(WaitThreeSeconds);
await Task.WhenAll(tasks);
这段代码返回相同的结果,它正在做我们想要的。
是的,我看到了一些例子,其中工程师在属性中间接使用async/ await,因为您不能在属性上直接使用async/ await。这是一个相当奇怪的事情,我试图从这个反模式中尽我所能。
始终异步
异步代码有时被比作僵尸病毒。它将代码从最高级别的抽象扩展到最低级别的抽象。这是因为当从另一段异步代码调用异步代码时,异步代码工作得最好。作为一般准则,您不应混合使用同步和异步代码,这就是“始终异步”所代表的含义。同步/异步代码混合中存在两个常见错误:
- 在异步代码中阻塞
- 为同步方法创建异步包装器
第一个肯定是最常见的错误之一,这将导致僵局。除此之外,async方法中的阻塞占用了可以在其他地方更好地使用的线程。例如,在ASP.NET上下文中,这意味着线程无法为其他请求提供服务,而在GUI上下文中,这意味着该线程不能用于呈现。我们来看看这段代码:
public async Task InitiateWaitTask()
{var delayTask = WaitAsync();delayTask.Wait();
}private static async Task WaitAsync()
{await Task.Delay(1000);
}
为什么这段代码会死锁?嗯,这是一个关于SynchronizationContext的长篇故事,它用于捕获正在运行的线程的上下文。更确切地说,当等待不完整的Task时,线程的当前上下文被存储并在稍后Task完成时使用。此上下文是当前的SynchronizationContext,即应用程序中线程的当前抽象。GUI和ASP.NET应用程序有一个SynchronizationContext,其只允许一次运行一个代码块。然而,ASP.NET Core应用程序没有SynchronizationContext,这样它们就不会发生死锁。总而言之,你不应该阻止异步代码。
如今,很多的API有成对的异步和方法,例如,Start()和StartAsync(),Read()和ReadAsync()。我们可能想在我们自己的纯同步库中创建它们,但事实是我们可能不应该这样做。正如Stephen Toub在他的博客文章中完美描述的那样,如果开发人员想要使用同步API实现响应性或并行性,他们可以简单地用调用Task.Run()包装调用。我们没有必要在我们的API中执行此操作。
结论
总结一下,当您使用异步机制时,尽量避免使用这些async void方法,除非在异步事件处理程序的特殊情况下。请记住,在async/await 期间没有产生额外的线程,并且此机制在较低的级别上完成。除此之外,尽量不要在foreach循环和属性中使用await,这是没有意义的。是的,不要混用同步和异步代码,它会给你带来可怕的麻烦。
谢谢你的阅读!
原文地址:https://www.codeproject.com/Articles/1246010/Asynchronous-Programming-in-NET-Common-Mistakes-an
.NET中的异步编程——常见的错误和最佳实践相关推荐
- Java getter和Setter:基础、常见错误和最佳实践
getter和setter在Java中得到了广泛的应用.它看起来很简单,但并不是每个程序员都能正确地理解和实现这种方法.因此,在本文中,我想深入讨论Java中的getter和setter方法-从基础到 ...
- 了解和使用DotNetCore和Blazor中的异步编程
目录 介绍 您对异步编程了解什么? 那么,什么是异步编程? 我们什么时候应该使用它? 任务.线程.计划.上下文 到底是怎么回事? Asnyc编码模式 命名约定 异步任务模式 任务模式 事件模式 阻塞与 ...
- 一文说通C#中的异步编程
天天写,不一定就明白. 又及,前两天看了一个关于同步方法中调用异步方法的文章,里面有些概念不太正确,所以整理了这个文章. 一.同步和异步. 先说同步. 同步概念大家都很熟悉.在异步概念出来之前,我 ...
- 【转】.Net中的异步编程总结
一直以来很想梳理下我在开发过程中使用异步编程的心得和体会,但是由于我是APM异步编程模式的死忠,当TAP模式和TPL模式出现的时候我并未真正的去接纳这两种模式,所以导致我一直没有花太多心思去整理这两部 ...
- 一文说通C#中的异步编程补遗
前文写了关于C#中的异步编程.后台有无数人在讨论,很多人把异步和多线程混了. 文章在这儿:一文说通C#中的异步编程 所以,本文从体系的角度,再写一下这个异步编程. 一.C#中的异步编程演变 1. ...
- C#中的异步编程(Async)
文章目录 C#中的异步编程(Async) 前言 示例代码 C#中的异步编程(Async) 前言 所谓的异步,就是指代码在运行的过程中,不会发生阻塞,例如我们玩游戏的时候,游戏在下载资源或者在加载本地资 ...
- 在ASP.NET Web API中返回错误的最佳实践
本文翻译自:Best practice to return errors in ASP.NET Web API I have concerns on the way that we returns e ...
- dart系列之:dart中的异步编程
文章目录 简介 为什么要用异步编程 怎么使用 Future 异步异常处理 在同步函数中调用异步函数 总结 简介 熟悉javascript的朋友应该知道,在ES6中引入了await和async的语法,可 ...
- 深入理解nodejs中的异步编程
文章目录 简介 同步异步和阻塞非阻塞 javascript中的回调 回调函数的错误处理 回调地狱 ES6中的Promise 什么是Promise Promise的特点 Promise的优点 Promi ...
最新文章
- Sql insert into 后获得自动插入的id
- Ubuntu 开机 Firmware Bug , Bios corrupted
- 阿里云 x 蒙牛 | 打通数据孤岛,基于MaxCompute实现产销协同的智慧运营
- Kubernetes-存活探针(liveness probe)(十六)
- 虚拟机和电脑共享文件夹
- ICCV-2021 Oral | AdaFocus:利用空间冗余性实现高效视频识别
- J2EE框架技术(持续更新)
- 呼叫中心系统源码_RC09(支持二次开发)CC呼叫中心系统 源码
- php如何让浏览器切换到极速模式,如何识别并切换到浏览器极速模式?
- 想变好却不能坚持,我告诉你怎么办
- linux中添加程序,学习进阶 uClinux中添加用户应用程序
- 2021-12-23 714. 买卖股票的时机含手续费(动态规划)
- 密码管理器(PM)安全机制和问题研究
- 区块链软件公司:区块链技术三大主要特性的优势
- 机器学习: 决策数与随机森林
- golang的ping检测主机存活
- ubuntu18.04 texstudio下使用自动化学报latex模板的坑
- STM32CubeMx笔记--P2. LED亮晶晶
- 计算机体系结构在线读,计算机体系结构复习题.ppt
- pytorch:如何从头开始训练一个CNN网络?
热门文章
- win2008服务器系统功能,详解Win2008初始配置任务功能的应用
- java自带数据库_Derby-jdk自带数据库的使用 - Java天堂
- 版式之美!左右布局图文排版设计灵感
- 平安夜海报PNG免扣素材来了,全都在这|搜图114
- UI设计师应该知道的命名规范
- linux timerfd系列函数总结:timerfd.h
- leetcode题库:3.无重复字符的最长子串
- python图书管理系统增删改查_图书管理系统---增删改查
- VUE3(setup响应式函数系统API)
- easyui的datagrid的使用方法