在这篇文章中,我们将通过使用异步编程的一些最常见的错误来给你们一些参考。

背景

在之前的文章《.NET中的异步编程——动机和单元测试》中,我们开始分析.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上引发异常。

  1. private async void ThrowExceptionAsync()
  2. {
  3. throw new Exception("Async exception");
  4. }
  5.  
  6. public void AsyncVoidExceptions_CannotBeCaughtByCatch()
  7. {
  8. try
  9. {
  10. ThrowExceptionAsync();
  11. }
  12. catch (Exception)
  13. {
  14. // The exception is never caught here!
  15. throw;
  16. }
  17. }

使用async void时还有两个缺点首先,这些方法不能提供一种简单的方法来通知他们已经完成的调用代码。而且,由于这第一个缺陷,很难对它们进行测试。单元测试框架(例如xUnit或NUnit)仅适用于返回Task或返回Task<T>的类型的async方法综合考虑所有这些因素,async void一般来说是不赞成使用的,而async Task建议使用。唯一的异常可能是异步事件处理程序,必须返回void。

没有线程

关于.NET中异步机制的最大误解可能是在后台运行某种异步线程。虽然在等待某些操作时似乎很合乎逻辑,但是正在进行等待的线程,情况并非如此。为了理解这一点,让我们后退几步。当我们使用我们的计算机时,我们有多个程序同时运行,这是通过在CPU上一次运行来自不同进程的指令来实现的。

由于这些指令是交错的并且CPU快速地从一个切换到另一个(上下文切换),我们得到一个错觉,它们同时运行。此过程称为并发。现在,当我们在CPU中拥有多个内核时,我们可以在每个内核上运行多个这些指令流。这称为并行性。现在,重要的是要了解这两个概念在CPU级别上都可用。在操作系统级别,我们有一个线程概念——一系列可由调度程序独立管理的指令集。

那么,为什么我会给你计算机科学101讲座?好吧,因为等待,我们之前谈论的是在线程概念尚未存在的水平上发生的事情。让我们来看看这部分代码,对设备的通用写操作(网络,文件等):

  1. public async Task WriteMyDeviceAcync
  2. {
  3. byte[] data = ...
  4. myDevice.WriteAsync(data, , data.Length);
  5. }

现在,让我们深入的研究一下吧。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循环内部使用。看看这个例子:

  1. var listOfInts = new List<int>() { , , };
  2. foreach (var integer in listOfInts)
  3. {
  4. await WaitThreeSeconds(integer);
  5. }

现在,即使这个代码是以异步方式编写的,但是每当等待WaitThreeSeconds时,它将阻塞流的执行。这是一个真实的情况,例如,WaitThreeSeconds正在调用某种Web API,假设它执行HTTP GET请求传递查询数据。有时,我们有这样的情况,我们想这样做,但如果我们这样实现,我们将等待每个请求-响应周期完成,然后再开始一个新的。这是低效的。

这是我们的WaitThreeSeconds功能:

  1. private async Task WaitThreeSeconds(int param)
  2. {
  3. Console.WriteLine($"{param} started ------ ({DateTime.Now:hh:mm:ss}) ---");
  4. await Task.Delay();
  5. Console.WriteLine($"{ param} finished ------({ DateTime.Now:hh: mm: ss}) ---");
  6. }

如果我们尝试运行此代码,我们将得到如下内容:

执行这个代码的时间是九秒。如前所述,这是非常低效的。通常,我们希望这些Tasks中的每一个都被触发,并且所有任务都并行完成(时间稍微超过3秒)。

现在我们可以像这样修改上面的代码:

  1. var listOfInts = new List<int>() { , , };
  2. var tasks = new List<Task>();
  3. foreach (var integer in listOfInts)
  4. {
  5. var task = WaitThreeSeconds(integer);
  6. tasks.Add(task);
  7. }
  8. await Task.WhenAll(tasks);

当我们运行它时,我们会得到这样的东西:

这正是我们想要的。如果我们想用更少的代码编写它,我们可以使用LINQ:

  1. var tasks = new List<int>() { , , }.Select(WaitThreeSeconds);
  2. await Task.WhenAll(tasks);

这段代码返回相同的结果,它正在做我们想要的。

是的,我看到了一些例子,其中工程师在属性中间接使用async/ await,因为您不能在属性上直接使用async/ await。这是一个相当奇怪的事情,我试图从这个反模式中尽我所能。

始终异步

异步代码有时被比作僵尸病毒。它将代码从最高级别的抽象扩展到最低级别的抽象。这是因为当从另一段异步代码调用异步代码时,异步代码工作得最好。作为一般准则,您不应混合使用同步和异步代码,这就是“始终异步”所代表的含义。同步/异步代码混合中存在两个常见错误:

  • 在异步代码中阻塞
  • 为同步方法创建异步包装器

第一个肯定是最常见的错误之一,这将导致僵局。除此之外,async方法中的阻塞占用了可以在其他地方更好地使用的线程。例如,在ASP.NET上下文中,这意味着线程无法为其他请求提供服务,而在GUI上下文中,这意味着该线程不能用于呈现。我们来看看这段代码:

  1. public async Task InitiateWaitTask()
  2. {
  3. var delayTask = WaitAsync();
  4. delayTask.Wait();
  5. }
  6.  
  7. private static async Task WaitAsync()
  8. {
  9. await Task.Delay();
  10. }

为什么这段代码会死锁?嗯,这是一个关于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中的异步编程——常见的错误和最佳实践的更多相关文章

  1. .Net中的异步编程总结

    一直以来很想梳理下我在开发过程中使用异步编程的心得和体会,但是由于我是APM异步编程模式的死忠,当TAP模式和TPL模式出现的时候我并未真正的去接纳这两种模式,所以导致我一直没有花太多心思去整理这两部 ...

  2. javaScript中的异步编程模式

    1.事件模型 let button = document.getElementById("my-btn"); button.onclick = function(event) { ...

  3. 一文说通C#中的异步编程

    天天写,不一定就明白. 又及,前两天看了一个关于同步方法中调用异步方法的文章,里面有些概念不太正确,所以整理了这个文章.   一.同步和异步. 先说同步. 同步概念大家都很熟悉.在异步概念出来之前,我 ...

  4. C#中的异步编程Async 和 Await

    谈到C#中的异步编程,离不开Async和Await关键字 谈到异步编程,首先我们就要明白到底什么是异步编程. 平时我们的编程一般都是同步编程,所谓同步编程的意思,和我们平时说的同时做几件事情完全不同. ...

  5. Netty 中的异步编程 Future 和 Promise

    Netty 中大量 I/O 操作都是异步执行,本篇博文来聊聊 Netty 中的异步编程. Java Future 提供的异步模型 JDK 5 引入了 Future 模式.Future 接口是 Java ...

  6. 一文说通C#中的异步编程补遗

    前文写了关于C#中的异步编程.后台有无数人在讨论,很多人把异步和多线程混了. 文章在这儿:一文说通C#中的异步编程 所以,本文从体系的角度,再写一下这个异步编程.   一.C#中的异步编程演变 1. ...

  7. Atitit.500 503 404错误处理最佳实践oak

    Atitit.500 503 404错误处理最佳实践oak 1. 错误处理的流程(捕获>>日志>>db>>email alert) 1 2. 错误的捕获:strut ...

  8. PHP编程中10个最常见的错误

    PHP是一种非常流行的开源服务器端脚本语言,你在万维网看到的大多数网站都是使用php开发的.本篇经将为大家介绍PHP开发中10个最常见的问题,希望能够对朋友有所帮助. 错误1:foreach循环后留下 ...

  9. 全面解析C#中的异步编程

    当我们处理一些长线的调用时,经常会导致界面停止响应或者IIS线程占用过多等问题,这个时候我们需要更多的是用异步编程来修正这些问题,但是通常都是说起来容易做起来难,诚然异步编程相对于同步编程来说,它是一 ...

随机推荐

  1. django-URL路由系统

    配置 URL配置(URLconf)就像Django 所支撑网站的目录.它的本质是URL与要为该URL调用的视图函数之间的映射表.你就是以这种方式告诉Django,对于这个URL调用这段代码,对于那个U ...

  2. 动态加载swiper,默认显示最后一个swiper-slide解决方案???

    问题描述: 用ajax动态加载swiper-slide以后,由于我是自适应屏幕的尺寸来决定一屏显示多少图片,所以加了 slidesPerView:'auto'这条属性,加了这条属性过后,每次刷新页面的 ...

  3. 在windows下安装Superset

    前言 最近想用一下Superset,这个是一个开源项目,可以直接通过写sql来生成图表,有时候对一些图表需求比较多的时候,可以用的上. Superset是由Airbnb(知名在线房屋短租公司)开源BI ...

  4. turtle模块

    turtle(海龟)绘图用法 import turtle -->调出turtle库 setup()-->设置窗体大小和位置 turtle.setup(width,height,startx ...

  5. gnome3 调整标题栏高度

    适用于:gtk 3.20 + 1. 在用户主目录 -/.config/gtk3.0/ 下新建gtk.css文件: 2. 复制如下css值: headerbar.default-decoration { ...

  6. 网络协议 4 - 交换机与 VLAN:拓扑结构

    上一次,我们通过宿舍联网打魔兽的需求,认识了如何通过物理层和链路层组建一个宿舍局域网.今天,让我们切换到稍微复杂点的场景,办公室.     在这个场景里,就不像在宿舍那样,搞几根网线,拉一拉,扯一扯就 ...

  7. [TJOI2009]猜数字(洛谷 3868)

    题目描述 现有两组数字,每组k个,第一组中的数字分别为:a1,a2,...,ak表示,第二组中的数字分别用b1,b2,...,bk表示.其中第二组中的数字是两两互素的.求最小的非负整数n,满足对于任意 ...

  8. suse12.2构建samba

    1:添加用户 useradd wangjunhui -d /home/wangjunhuipasswd wangjunhui 2:配置samba smbpasswd -a wangjunhui vi ...

  9. Spring Cloud @RefreshScope 原理是什么?

    要清楚RefreshScope,先要了解Scope Scope(org.springframework.beans.factory.config.Scope)是Spring 2.0开始就有的核心的概念 ...

  10. Spring Boot 《一》开发一个“HelloWorld”的 web 应用

    一,Spring Boot 介绍 Spring Boot不是一个新的框架,默认配置了多种框架使用方式,使用SpringBoot很容易创建一个独立运行(运行jar,内嵌Servlet).准生产级别的基于 ...