许多开发人员对异步代码和多线程以及它们的工做原理和使用方法都有错误的认识。在这里,你将了解这两个概念之间的区别,并使用c#实现它们。html
我:“服务员,这是我第一次来这家餐厅。一般须要4个小时才能拿到食物吗?”编程
服务员:“哦,是的,先生。这家餐厅的厨房里只有一个厨师。”c#
我:“……只有一个厨师吗?”数组
服务员:“是的,先生,咱们有好几个厨师,但每次只有一个在厨房工做。”服务器
我:“因此其余10个穿着厨师服站在厨房里的人……什么都不作吗?厨房过小了吗?”网络
服务员:“哦,咱们的厨房很大,先生。”多线程
我:“那为何他们不一样时工做呢?”app
服务员:“先生,这却是个好主意,但咱们还没想好怎么作。”dom
我:“好了,奇怪。可是…嘿…如今的主厨在哪里?我如今没看见有人在厨房里。”异步
服务员:“是的,先生。有一份订单的厨房用品已经用完了,因此厨师已经中止烹饪,站在外面等着送货了。”
我:“看起来他能够一边等一边作饭,也许送货员能够直接告诉他们何时到了?”
服务员:“又是一个绝妙的主意,先生。咱们在后面有送货门铃,但厨师喜欢等。我去给你再拿点水来。”
多糟糕的餐厅,对吧?不幸的是,不少程序都是这样工做的。
有两种不一样的方法可让这家餐厅作得更好。
首先,很明显,每一个单独的晚餐订单能够由不一样的厨师来处理。每一种都是一个必须按特定顺序发生的事情列表(准备原料,而后混合它们,而后烹饪,等等)。所以,若是每一个厨师都致力于处理这一清单上的东西,几份晚餐订单能够同时作出。
这是一个真实世界中的多线程示例。计算机有能力让多个不一样的线程同时运行,每一个线程负责按特定顺序执行一系列活动。
而后还有异步行为。须要明确的是,异步不是多线程的。还记得那个一直在等外卖的厨师吗?真是浪费时间!在等待的过程当中,他没有作任何有意义的事情,好比作饭。并且,等待也不会让送货更快。一旦他打电话订购供应品,发货就会随时发生,因此为何要等呢?相反,送货员只需按门铃,说一句:“嘿,这是你的供应品!”
有不少I/O活动是由代码以外的东西处理的。例如,向远程服务器发送一个网络请求。这就像给餐厅点餐同样。你的代码所作的惟一事情就是进行调用并接收结果。若是选择等待结果,在这二者之间彻底不作任何事情,那么这就是“同步”行为。
然而,若是你更喜欢在结果返回时被打断/通知(就像送货员到达时按门铃),同时能够处理其余事情,那么这就是“异步”行为。
只要工做是由不受当前代码直接控制的对象完成的,就可使用异步代码。例如,当你向硬盘驱动器写入一堆数据时,你的代码并无执行实际的写入操做。它只是请求硬件执行该任务。所以,你可使用异步编码开始编写,而后在编写完成时获得通知,同时继续处理其余事情。
异步的优势在于不须要额外的线程,所以很是高效。
“等等!”你说。“若是没有额外的线程,那么谁或什么在等待结果?代码如何知道返回的结果?”
还记得那个门铃吗?你的电脑里有一个系统叫作“中断”系统,它的工做原理有点像那个门铃。当你的代码开始一个异步活动时,它基本上会安装一个虚拟的门铃。当其余任务(写入硬盘驱动器,等待网络响应等)完成时,中断系统“中断”当前运行的代码并按下门铃,让你的应用程序知道有一个任务在等待!不须要线程坐在那里等待!
让咱们快速回顾一下咱们的两种工具:
多线程:使用一个额外的线程来执行一系列活动/任务。
异步:使用同一个线程和中断系统,让线程外的其余组件完成一些活动,并在活动结束时获得通知。
UI线程
还有一件重要的事情须要知道的是为何使用这些工具是好的。在.net中,有一个主线程叫作UI线程,它负责更新屏幕的全部可视部分。默认状况下,这是一切运行的地方。当你点击一个按钮,你想看到按钮被短暂地按下,而后返回,这是UI线程的责任。你的应用中只有一个UI线程,这意味着若是你的UI线程忙着作繁重的计算或等待网络请求之类的事情,那么它不能更新你在屏幕上看到的东西,直到它完成。结果是,你的应用程序看起来像“冻结”——你能够点击一个按钮,但彷佛什么都不会发生,由于UI线程正在忙着作其余事情。
理想状况下,你但愿UI线程尽量地空闲,这样你的应用程序彷佛老是在响应用户的操做。这就是异步和多线程的由来。经过使用这些工具,能够确保在其余地方完成繁重的工做,UI线程保持良好和响应性。
如今让咱们看看如何在c#中使用这些工具。
C#的异步操做
执行异步操做的代码很是简单。你应该知道两个主要的关键字:“async”和“await”,因此人们一般将其称为async/await。假设你如今有这样的代码:
在当前的形式中,这些都是同步运行的。若是你点击一个按钮从UI线程运行Loopy(),那么应用程序将彷佛冻结,直到全部三大文件阅读,由于每一个“ReadAHugeFile”是要花很长时间在UI线程上运行,并将同步阅读。这可很差!让咱们看看可否将ReadAHugeFile变为异步的这样UI线程就能继续处理其余东西。
不管什么时候,只要有支持异步的命令,微软一般会给咱们同步和异步版本的这些命令。在上面的代码中,System.IO.FileStream对象同时具备"Read"和"ReadAsync"方法。因此第一步就是将“fs.Read”修改为“fs.ReadAsync”。
若是如今运行它,它会当即返回,而且“allData”字节数组中不会有任何数据。为何?
这是由于ReadAsync是开始读取并返回一个任务对象,这有点像一个书签。这是.net的一个“Promise”,一旦异步活动完成(例如从硬盘读取数据),它将返回结果,任务对象能够用来访问结果。但若是咱们对这个任务不作任何事情,那么系统就会当即继续到下一行代码,也就是咱们的"return allData"行,它会返回一个还没有填满数据的数组。
所以,告诉代码等待结果是颇有用的(但这样一来,原始线程能够在此期间继续作其余事情)。为了作到这一点,咱们使用了一个"awaiter",它就像在async调用以前添加单词"await"同样简单:
哦。若是你试过,你会发现有一个错误。这是由于.net须要知道这个方法是异步的,它最终会返回一个字节数组。所以,咱们作的第一件事是在返回类型以前添加单词“async”,而后用Task<…>,是这样的:
好吧!如今咱们烹饪!若是咱们如今运行咱们的代码,它将继续在UI线程上运行,直到咱们到达ReadAsync方法的await。此时,. net知道这是一个将由硬盘执行的活动,所以“await”将一个小书签放在当前位置,而后UI线程返回到它的正常处理(全部的视觉更新等)。
随后,一旦硬盘驱动器读取了全部数据,ReadAsync方法将其所有复制到allData字节数组中,任务如今就完成了,所以系统按门铃,让原始线程知道结果已经准备好了。原始线程说:“太棒了!让我回到离开的地方!”一有机会,它就会回到“await fs.ReadSync”,而后继续下一步,返回allData数组,这个数组如今已经填充了咱们的数据。
若是你在一个接一个地看一个例子,而且使用的是最近的Visual Studio版本,你会注意到这一行:
…如今,它用绿色下划线表示,若是将鼠标悬停在它上面,它会说,“由于这个调用没有被等待,因此在调用完成以前,当前方法的执行将继续。”考虑对调用的结果应用'await'操做符。"
这是Visual Studio让你知道它认可ReadAHugeFile()是一个异步的方法,而不是返回一个结果,这也是返回任务,因此若是你想等待结果,而后你就能够添加一个“await”:
…但若是咱们这样作了,那么你还必须更新方法签名:
注意,若是咱们在一个不返回任何东西的方法上(void返回类型),那么咱们不须要将返回类型包装在Task<…>中。
可是,咱们不要这样作。相反,让咱们来了解一下咱们能够用异步作些什么。
若是你不想等待ReadAHugeFile(hugeFile)的结果,由于你可能不关心最终的结果,但你不喜欢绿色下划线/警告,你可使用一个特殊的技巧来告诉.net。只需将结果赋给_字符,就像这样:
这就是.net的语法,表示“我不在意结果,但我不但愿用它的警告来打扰我。”
好吧,咱们试试别的。若是咱们在这一行上使用了await,那么它将等待第一个文件被异步读取,而后等待第二个文件被异步读取,最后等待第三个文件被异步读取。可是…若是咱们想要同时异步地读取全部3个文件,而后在全部3个文件都完成以后,咱们容许代码继续到下一行,该怎么办?
有一个叫作Task.WhenAll()的方法,它自己是一个你能够await的异步方法。传入其余任务对象的列表,而后等待它,一旦全部任务都完成,它就会完成。因此最简单的方法就是建立一个List<Task>对象:
…而后,当咱们将每一个ReadAHugeFile()调用中的Task添加到列表中时:
…最后咱们 await Task.WhenAll():
最终的方法是这样的:
当涉及到并行活动时,一些I/O机制比其余机制工做得更好(例如,网络请求一般比硬盘读取工做得更好,但这取决于硬件),但原理是相同的。
如今,“await”操做符还要作的最后一件事是提取最终结果。因此在上面的例子中,ReadAHugeFile返回一个任务<byte[]>。await的神奇功能会在完成后自动抛出Task<>包装器,并返回byte[]数组,因此若是你想访问Loopy()中的字节,你能够这样作:
再次强调,await是一个神奇的小命令,它使异步编程变得很是简单,并为你处理各类各样的小事情。
如今让咱们转向多线程。
C#中的多线程
微软有时会给你10种不一样的方法来作一样的事情,这就是它如何使用多线程。你有BackgroundWorker类、Thread和Task(它们有几个变体)。最终,它们都作着相同的事情,只是有不一样的功能。如今,大多数人都使用Task,由于它们的设置和使用都很简单,并且若是你想这样作的话(咱们稍后会讲到),它们也能够很好地与异步代码交互。若是你好奇的话,关于这些具体区别有不少文章,可是咱们在这里使用任务。
要让任何方法在单独的线程中运行,只需使用Task.Run()方法来执行它。例如,假设你有这样一个方法:
咱们能够像这样在当前线程中调用它:
或者咱们可让另外一个线程来作这个工做:
固然,有一些不一样的版本,但这是整体思路。
Task. run()的一个优势是它返回一个咱们能够等待的任务对象。所以,若是想在一个单独的线程中运行一堆代码,而后在进入下一步以前等待它完成,你可使用await,就像你在前面一节看到的那样:
请记住,本文讨论的是如何开始,以及这些概念是如何工做的,但它并非全面的。可是也许有了这些知识,你将可以理解其余人关于多线程和异步编码更高级种类的更复杂的文章。
欢迎关注个人公众号,若是你有喜欢的外文技术文章,能够经过公众号留言推荐给我。
原文连接:https://www.experts-exchange.com/articles/35473/Async-and-Multi-Threading-in-C-in-Plain-English.html