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

背景

在之前的文章《.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上引发异常。

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, , 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>() { , ,  };
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();
Console.WriteLine($"{ param} finished ------({ DateTime.Now:hh: mm: ss}) ---");
}

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

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

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

var listOfInts = new List<int>() { , ,  };
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>() { , ,  }.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();
}

为什么这段代码会死锁?嗯,这是一个关于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. 3. 上网调查一下目前流行的源程序版本管理软件和项目管理软件都有哪些, 各有什么优缺点? (提示:搜索一下Microsoft TFS、GitHub、Trac、Bugzilla、Rationale,Apple XCode),请用一个实际的源代码管理工具来建立源代码仓库,并签入/签出代码。

    上网调查一下目前流行的源程序版本管理软件和项目管理软件都有哪些, 各有什么优缺点? ---------------答题者:徐潇瑞 (1)Microsoft TFS的优缺点: 优点:是对敏捷,msf,c ...

  2. php模板模式(template design)

    没有写停止条件,所以会一直运行哟. <?php /* The template design pattern defines the program skeleton of an algorit ...

  3. Winform----自定义控件之半透明遮罩(蒙版遮盖指定控件)

    先贴运行效果图,源码点击这里下载 1.新建自定义控件 2.实现功能   namespace UserControlLib   {   [ToolboxBitmap(typeof(ZhLoading)) ...

  4. 在eclipse中安装使用lombok插件

    Eclipse安装lombok插件 1.下载lombok.jar,lombok.jar官方下载地址:https://projectlombok.org/download 2.双击下载好的lombak. ...

  5. nginx 其他配置语法

    1.nginx 缓冲区配置 2.跳转重定向 3.头信息 4.超时 location / { proxy_pass http://127.0.0.1:8080;(代理跳转url) proxy_redir ...

  6. VS 代码过长自动换行

    然后就需要设置自动换行.在VS上面的菜单栏中,选择 工具=>选项,出现选项对话框.   在对话框中,展开“文本编辑器”,然后选中“C#”,勾选右边的“自动换行“.   点击确定按钮.这样就可以看 ...

  7. Spring和SpringMVC总结篇

    作者:肥宅兜链接:https://www.cnblogs.com/doudouxiaoye/p/5693399.html 1.为什么使用Spring ? 方便解耦,简化开发;通过Spring提供的Io ...

  8. js判断客户端是iOS还是Android移动终端

    前段时间,小颖公司需要实现:用户在微信中打开一个html5,在该html5中通过点击下载按钮,Android手机会跳到Android的下载地址,IOS会跳转到IOS下载地址,其它则跳转到另一个指定地址 ...

  9. PATA1082Read Number in Chinese

    有几点需要注意的地方一是将right转化为与left在在同一节 while (left + 4 <= right) { right -= 4;//每次将right移动4位,直到left与righ ...

  10. xsxs

    import subprocess compilePopen = subprocess.Popen('gcc haha',shell=True,stderr=subprocess.PIPE) comp ...