目录

背景

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中的异步编程——常见的错误和最佳实践相关推荐

  1. Java getter和Setter:基础、常见错误和最佳实践

    getter和setter在Java中得到了广泛的应用.它看起来很简单,但并不是每个程序员都能正确地理解和实现这种方法.因此,在本文中,我想深入讨论Java中的getter和setter方法-从基础到 ...

  2. 了解和使用DotNetCore和Blazor中的异步编程

    目录 介绍 您对异步编程了解什么? 那么,什么是异步编程? 我们什么时候应该使用它? 任务.线程.计划.上下文 到底是怎么回事? Asnyc编码模式 命名约定 异步任务模式 任务模式 事件模式 阻塞与 ...

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

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

  4. 【转】.Net中的异步编程总结

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

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

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

  6. C#中的异步编程(Async)

    文章目录 C#中的异步编程(Async) 前言 示例代码 C#中的异步编程(Async) 前言 所谓的异步,就是指代码在运行的过程中,不会发生阻塞,例如我们玩游戏的时候,游戏在下载资源或者在加载本地资 ...

  7. 在ASP.NET Web API中返回错误的最佳实践

    本文翻译自:Best practice to return errors in ASP.NET Web API I have concerns on the way that we returns e ...

  8. dart系列之:dart中的异步编程

    文章目录 简介 为什么要用异步编程 怎么使用 Future 异步异常处理 在同步函数中调用异步函数 总结 简介 熟悉javascript的朋友应该知道,在ES6中引入了await和async的语法,可 ...

  9. 深入理解nodejs中的异步编程

    文章目录 简介 同步异步和阻塞非阻塞 javascript中的回调 回调函数的错误处理 回调地狱 ES6中的Promise 什么是Promise Promise的特点 Promise的优点 Promi ...

最新文章

  1. Sql insert into 后获得自动插入的id
  2. Ubuntu 开机 Firmware Bug , Bios corrupted
  3. 阿里云 x 蒙牛 | 打通数据孤岛,基于MaxCompute实现产销协同的智慧运营
  4. Kubernetes-存活探针(liveness probe)(十六)
  5. 虚拟机和电脑共享文件夹
  6. ICCV-2021 Oral | AdaFocus:利用空间冗余性实现高效视频识别
  7. J2EE框架技术(持续更新)
  8. 呼叫中心系统源码_RC09(支持二次开发)CC呼叫中心系统 源码
  9. php如何让浏览器切换到极速模式,如何识别并切换到浏览器极速模式?
  10. 想变好却不能坚持,我告诉你怎么办
  11. linux中添加程序,学习进阶 uClinux中添加用户应用程序
  12. 2021-12-23 714. 买卖股票的时机含手续费(动态规划)
  13. 密码管理器(PM)安全机制和问题研究
  14. 区块链软件公司:区块链技术三大主要特性的优势
  15. 机器学习: 决策数与随机森林
  16. golang的ping检测主机存活
  17. ubuntu18.04 texstudio下使用自动化学报latex模板的坑
  18. STM32CubeMx笔记--P2. LED亮晶晶
  19. 计算机体系结构在线读,计算机体系结构复习题.ppt
  20. pytorch:如何从头开始训练一个CNN网络?

热门文章

  1. win2008服务器系统功能,详解Win2008初始配置任务功能的应用
  2. java自带数据库_Derby-jdk自带数据库的使用 - Java天堂
  3. 版式之美!左右布局图文排版设计灵感
  4. 平安夜海报PNG免扣素材来了,全都在这|搜图114
  5. UI设计师应该知道的命名规范
  6. linux timerfd系列函数总结:timerfd.h
  7. leetcode题库:3.无重复字符的最长子串
  8. python图书管理系统增删改查_图书管理系统---增删改查
  9. VUE3(setup响应式函数系统API)
  10. easyui的datagrid的使用方法