您好,登录后才能下订单哦!
C#中异步与多线程的作用有哪些?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。
UI线程
还有一件重要的事情需要知道的是为什么使用这些工具是好的。在.net中,有一个主线程叫做UI线程,它负责更新屏幕的所有可视部分。默认情况下,这是一切运行的地方。当你点击一个按钮,你想看到按钮被短暂地按下,然后返回,这是UI线程的责任。你的应用中只有一个UI线程,这意味着如果你的UI线程忙着做繁重的计算或等待网络请求之类的事情,那么它不能更新你在屏幕上看到的东西,直到它完成。结果是,你的应用程序看起来像“冻结”——你可以点击一个按钮,但似乎什么都不会发生,因为UI线程正在忙着做其他事情。
理想情况下,你希望UI线程尽可能地空闲,这样你的应用程序似乎总是在响应用户的操作。这就是异步和多线程的由来。通过使用这些工具,可以确保在其他地方完成繁重的工作,UI线程保持良好和响应性。
现在让我们看看如何在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! }
如果现在运行它,它会立即返回,并且“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是一个神奇的小命令,它使异步编程变得非常简单,并为你处理各种各样的小事情。
现在让我们转向多线程。
微软有时会给你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(() => {});
关于C#中异步与多线程的作用有哪些问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注亿速云行业资讯频道了解更多相关知识。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。