许多开发人员对异步代码和多线程以及它们的工作原理和应用办法都有谬误的意识。在这里,你将理解这两个概念之间的区别,并应用 c# 实现它们。
我:“服务员,这是我第一次来这家餐厅。通常须要 4 个小时能力拿到食物吗?”
服务员:“哦,是的,学生。这家餐厅的厨房里只有一个厨师。”
我:“……只有一个厨师吗?”
服务员:“是的,学生,咱们有好几个厨师,但每次只有一个在厨房工作。”
我:“所以其余 10 个衣着厨师服站在厨房里的人……什么都不做吗? 厨房太小了吗?”
服务员:“哦,咱们的厨房很大,学生。”
我:“那为什么他们不同时工作呢?”
服务员:“学生,这倒是个好主见,但咱们还没想好怎么做。”
我:“好了, 奇怪。然而…嘿…当初的主厨在哪里? 我当初没看见有人在厨房里。”
服务员:“是的,学生。有一份订单的厨房用品曾经用完了,所以厨师曾经进行烹饪,站在里面等着送货了。”
我:“看起来他能够一边等一边做饭,兴许送货员能够间接通知他们什么时候到了?”
服务员:“又是一个绝妙的主见,学生。咱们在前面有送货门铃,但厨师喜爱等。我去给你再拿点水来。”
多蹩脚的餐厅,对吧? 可怜的是,很多程序都是这样工作的。
有两种不同的办法能够让这家餐厅做得更好。
首先,很显著,每个独自的晚餐订单能够由不同的厨师来解决。每一种都是一个必须按特定程序产生的事件列表(筹备原料,而后混合它们,而后烹饪,等等)。因而,如果每个厨师都致力于解决这一清单上的货色,几份晚餐订单能够同时做出。
这是一个真实世界中的多线程示例。计算机有能力让多个不同的线程同时运行,每个线程负责按特定程序执行一系列流动。
而后还有异步行为。须要明确的是,异步不是多线程的。还记得那个始终在等外卖的厨师吗? 真是浪费时间! 在期待的过程中,他没有做任何有意义的事件,比方做饭。而且,期待也不会让送货更快。一旦他打电话订购供应品,发货就会随时产生,所以为什么要等呢? 相同,送货员只需按门铃,说一句:“嘿,这是你的供应品!”
有很多 I / O 流动是由代码之外的货色解决的。例如,向近程服务器发送一个网络申请。这就像给餐厅点餐一样。你的代码所做的惟一事件就是进行调用并接管后果。如果抉择期待后果,在这两者之间齐全不做任何事件,那么这就是“同步”行为。
然而,如果你更喜爱在后果返回时被打断 / 告诉(就像送货员达到时按门铃),同时能够解决其余事件,那么这就是“异步”行为。
只有工作是由不受以后代码间接管制的对象实现的,就能够应用异步代码。例如,当你向硬盘驱动器写入一堆数据时,你的代码并没有执行理论的写入操作。它只是申请硬件执行该工作。因而,你能够应用异步编码开始编写,而后在编写实现时失去告诉,同时持续解决其余事件。
异步的长处在于不须要额定的线程,因而十分高效。
“等等!”你说。“如果没有额定的线程,那么谁或什么在期待后果? 代码如何晓得返回的后果?”
还记得那个门铃吗? 你的电脑里有一个零碎叫做“中断”零碎,它的工作原理有点像那个门铃。当你的代码开始一个异步流动时,它基本上会装置一个虚构的门铃。当其余工作 (写入硬盘驱动器,期待网络响应等) 实现时,中断零碎“中断”以后运行的代码并按下门铃,让你的应用程序晓得有一个工作在期待! 不须要线程坐在那里期待!
让咱们疾速回顾一下咱们的两种工具:
多线程: 应用一个额定的线程来执行一系列流动 / 工作。
异步: 应用同一个线程和中断零碎,让线程外的其余组件实现一些流动,并在流动完结时失去告诉。
UI 线程
还有一件重要的事件须要晓得的是为什么应用这些工具是好的。在.net 中,有一个主线程叫做 UI 线程,它负责更新屏幕的所有可视局部。默认状况下,这是所有运行的中央。当你点击一个按钮,你想看到按钮被短暂地按下,而后返回,这是 UI 线程的责任。你的利用中只有一个 UI 线程,这意味着如果你的 UI 线程忙着做沉重的计算或期待网络申请之类的事件,那么它不能更新你在屏幕上看到的货色,直到它实现。后果是,你的应用程序看起来像“解冻”——你能够点击一个按钮,但仿佛什么都不会产生,因为 UI 线程正在忙着做其余事件。
现实状况下,你心愿 UI 线程尽可能地闲暇,这样你的应用程序仿佛总是在响应用户的操作。这就是异步和多线程的由来。通过应用这些工具,能够确保在其余中央实现沉重的工作,UI 线程保持良好和响应性。
当初让咱们看看如何在 c# 中应用这些工具。
C# 的异步操作
执行异步操作的代码非常简单。你应该晓得两个次要的关键字:“async”和“await”,所以人们通常将其称为 async/await。假如你当初有这样的代码:
public void Loopy()
{var hugeFiles = new string[] {
"Gr8Gonzos_Home_Movie_In_8k_Res.mkv", // 1 GB
"War_And_Peace_In_150_Languages.rtf", // 1.2 GB
"Cats_On_Catnip.mpg" // 0.9 GB
};
foreach (var hugeFile in hugeFiles)
{ReadAHugeFile(hugeFile);
}
MessageBox.Show("All done!");
}
public byte[] ReadAHugeFile(string bigFile)
{var fileSize = new FileInfo(bigFile).Length; // Get the file size
var allData = new byte[fileSize]; // Allocate a byte array as large as our file
using (var fs = new System.IO.FileStream(bigFile, FileMode.Open))
{fs.Read(allData, 0, (int)fileSize); // Read the entire file...
}
return allData; // ...and return those bytes!
}
在以后的模式中,这些都是同步运行的。如果你点击一个按钮从 UI 线程运行 Loopy(), 那么应用程序将仿佛解冻, 直到所有三大文件浏览, 因为每个“ReadAHugeFile”是要花很长时间在 UI 线程上运行, 并将同步浏览。这可不好! 让咱们看看是否将 ReadAHugeFile 变为异步的这样 UI 线程就能持续解决其余货色。
无论何时,只有有反对异步的命令,微软通常会给咱们同步和异步版本的这些命令。在下面的代码中,System.IO.FileStream 对象同时具备 ”Read” 和 ”ReadAsync” 办法。所以第一步就是将“fs.Read”批改成“fs.ReadAsync”。
public byte[] ReadAHugeFile(string bigFile)
{var fileSize = new FileInfo(bigFile).Length; // Get the file size
var allData = new byte[fileSize]; // Allocate a byte array as large as our file
using (var fs = new System.IO.FileStream(bigFile, FileMode.Open))
{fs.ReadAsync(allData, 0, (int)fileSize); // Read the entire file asynchronously...
}
return allData; // ...and return those bytes!
}
如果当初运行它,它会立刻返回,并且“allData”字节数组中不会有任何数据。为什么?
这是因为 ReadAsync 是开始读取并返回一个工作对象,这有点像一个书签。这是.net 的一个“Promise”,一旦异步流动实现(例如从硬盘读取数据),它将返回后果,工作对象能够用来拜访后果。但如果咱们对这个工作不做任何事件,那么零碎就会立刻持续到下一行代码,也就是咱们的 ”return allData” 行,它会返回一个尚未填满数据的数组。
因而,通知代码期待后果是很有用的(但这样一来,原始线程能够在此期间持续做其余事件)。为了做到这一点,咱们应用了一个 ”awaiter”,它就像在 async 调用之前增加单词 ”await” 一样简略:
public byte[] ReadAHugeFile(string bigFile)
{var fileSize = new FileInfo(bigFile).Length; // Get the file size
var allData = new byte[fileSize]; // Allocate a byte array as large as our file
using (var fs = new System.IO.FileStream(bigFile, FileMode.Open))
{await fs.ReadAsync(allData, 0, (int)fileSize); // Read the entire file asynchronously...
}
return allData; // ...and return those bytes!
}
哦。如果你试过,你会发现有一个谬误。这是因为.net 须要晓得这个办法是异步的,它最终会返回一个字节数组。因而,咱们做的第一件事是在返回类型之前增加单词“async”,而后用 Task<…>, 是这样的:
public async Task<byte[]> ReadAHugeFile(string bigFile)
{var fileSize = new FileInfo(bigFile).Length; // Get the file size
var allData = new byte[fileSize]; // Allocate a byte array as large as our file
using (var fs = new System.IO.FileStream(bigFile, FileMode.Open))
{await fs.ReadAsync(allData, 0, (int)fileSize); // Read the entire file asynchronously...
}
return allData; // ...and return those bytes!
}
好吧! 当初咱们烹饪! 如果咱们当初运行咱们的代码,它将持续在 UI 线程上运行,直到咱们达到 ReadAsync 办法的 await。此时,. net 晓得这是一个将由硬盘执行的流动,因而“await”将一个小书签放在以后地位,而后 UI 线程返回到它的失常解决(所有的视觉更新等)。
随后,一旦硬盘驱动器读取了所有数据,ReadAsync 办法将其全副复制到 allData 字节数组中,工作当初就实现了,因而零碎按门铃,让原始线程晓得后果曾经筹备好了。原始线程说:“太棒了! 让我回到来到的中央!”一有机会,它就会回到“await fs.ReadSync”,而后持续下一步,返回 allData 数组,这个数组当初曾经填充了咱们的数据。
如果你在一个接一个地看一个例子,并且应用的是最近的 Visual Studio 版本,你会留神到这一行:
ReadAHugeFile(hugeFile);
…当初,它用绿色下划线示意,如果将鼠标悬停在它下面,它会说,“因为这个调用没有被期待,所以在调用实现之前,以后办法的执行将持续。”思考对调用的后果利用 ’await’ 操作符。”
这是 Visual Studio 让你晓得它抵赖 ReadAHugeFile()是一个异步的办法, 而不是返回一个后果, 这也是返回工作, 所以如果你想期待后果, 而后你就能够增加一个“await”:
await ReadAHugeFile(hugeFile);
…但如果咱们这样做了,那么你还必须更新办法签名:
public async void Loopy()
留神,如果咱们在一个不返回任何货色的办法上(void 返回类型),那么咱们不须要将返回类型包装在 Task<…> 中。
然而,咱们不要这样做。相同,让咱们来理解一下咱们能够用异步做些什么。
如果你不想期待 ReadAHugeFile(hugeFile)的后果,因为你可能不关怀最终的后果,但你不喜爱绿色下划线 / 正告,你能够应用一个非凡的技巧来通知.net。只需将后果赋给_字符,就像这样:
_ = ReadAHugeFile(hugeFile);
这就是.net 的语法,示意“我不在乎后果,但我不心愿用它的正告来打搅我。”
好吧,咱们试试别的。如果咱们在这一行上应用了 await,那么它将期待第一个文件被异步读取,而后期待第二个文件被异步读取,最初期待第三个文件被异步读取。然而…如果咱们想要同时异步地读取所有 3 个文件,而后在所有 3 个文件都实现之后,咱们容许代码持续到下一行,该怎么办?
有一个叫做 Task.WhenAll()的办法,它自身是一个你能够 await 的异步办法。传入其余工作对象的列表,而后期待它,一旦所有工作都实现,它就会实现。所以最简略的办法就是创立一个 List<Task> 对象:
List<Task> readingTasks = new List<Task>();
…而后,当咱们将每个 ReadAHugeFile()调用中的 Task 增加到列表中时:
foreach (var hugeFile in hugeFiles) {readingTasks.Add(ReadAHugeFile(hugeFile));
}
…最初咱们 await Task.WhenAll():
await Task.WhenAll(readingTasks);
最终的办法是这样的:
public async void Loopy()
{var hugeFiles = new string[] {
"Gr8Gonzos_Home_Movie_In_8k_Res.mkv", // 1 GB
"War_And_Peace_In_150_Languages.rtf", // 1.2 GB
"Cats_On_Catnip.mpg" // 0.9 GB
};
List<Task> readingTasks = new List<Task>();
foreach (var hugeFile in hugeFiles)
{readingTasks.Add(ReadAHugeFile(hugeFile));
}
await Task.WhenAll(readingTasks);
MessageBox.Show(sb.ToString());
}
当波及到并行流动时,一些 I / O 机制比其余机制工作得更好(例如,网络申请通常比硬盘读取工作得更好,但这取决于硬件),但原理是雷同的。
当初,“await”操作符还要做的最初一件事是提取最终后果。所以在下面的例子中,ReadAHugeFile 返回一个工作 <byte[]>。await 的神奇性能会在实现后主动抛出 Task<> 包装器,并返回 byte[]数组,所以如果你想拜访 Loopy()中的字节,你能够这样做:
byte[] data = await ReadAHugeFile(hugeFile);
再次强调,await 是一个神奇的小命令,它使异步编程变得非常简单,并为你解决各种各样的小事件。
当初让咱们转向多线程。
C# 中的多线程
微软有时会给你 10 种不同的办法来做同样的事件,这就是它如何应用多线程。你有 BackgroundWorker 类、Thread 和 Task(它们有几个变体)。最终,它们都做着雷同的事件,只是有不同的性能。当初,大多数人都应用 Task,因为它们的设置和应用都很简略,而且如果你想这样做的话(咱们稍后会讲到),它们也能够很好地与异步代码交互。如果你好奇的话,对于这些具体区别有很多文章,然而咱们在这里应用工作。
要让任何办法在独自的线程中运行,只需应用 Task.Run()办法来执行它。例如,假如你有这样一个办法:
public void DoRandomCalculations(int howMany)
{var rng = new Random();
for (int i = 0; i < howMany; i++)
{int a = rng.Next(1, 1000);
int b = rng.Next(1, 1000);
int sum = 0;
sum = a + b;
}
}
咱们能够像这样在以后线程中调用它:
DoRandomCalculations(1000000);
或者咱们能够让另一个线程来做这个工作:
Task.Run(() => DoRandomCalculations(1000000));
当然,有一些不同的版本,但这是总体思路。
Task. run()的一个长处是它返回一个咱们能够期待的工作对象。因而,如果想在一个独自的线程中运行一堆代码,而后在进入下一步之前期待它实现,你能够应用 await,就像你在后面一节看到的那样:
var finalData = await Task.Run(() => {});
请记住,本文探讨的是如何开始,以及这些概念是如何工作的,但它并不是全面的。然而兴许有了这些常识,你将可能了解其他人对于多线程和异步编码更高级品种的更简单的文章。
欢送关注我的公众号,如果你有喜爱的外文技术文章,能够通过公众号留言举荐给我。