深入分析C#中的异步和多线程

许多开发人员对异步代码和多线程以及它们的工作原理和使用方法都有错误的认识。在这里,你将了解这两个概念之间的区别,并使用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!
}

如果现在运行它,它会立即返回,并且“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(() => {});

请记住,本文讨论的是如何开始,以及这些概念是如何工作的,但它并不是全面的。但是也许有了这些知识,你将能够理解其他人关于多线程和异步编码更高级种类的更复杂的文章。

以上就是深入分析C#中的异步和多线程的详细内容,更多关于C#中的异步和多线程的资料请关注我们其它相关文章!

(0)

相关推荐

  • c#中Winform实现多线程异步更新UI(进度及状态信息)

    引言 在进行Winform程序开发需要进行大量的数据的读写操作的时候,往往会需要一定的时间,然在这个时间段里面,界面ui得不到更新,导致在用户看来界面处于假死的状态,造成了不好的用户体验.所以在大量数据操作的应用上,需要使用多线程来处理这种情况.在c#中使用多线程很方便只需要使用System.Threading.Thread的一个实例的Start方法就行了,但是如何实现多线程之间的交互就不是那么简单.本文实现了用子线程去处理数据,并实时更新主线程的ui状态了.下面就开始一步步的去实现异步线程更新

  • 解析C#多线程编程中异步多线程的实现及线程池的使用

    0.线程的本质 线程不是一个计算机硬件的功能,而是操作系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码,所以线程需要操作系统投入CPU资源来运行和调度. 1.多线程: 使用多个处理句柄同时对多个任务进行控制处理的一种技术.据博主的理解,多线程就是该应用的主线程任命其他多个线程去协助它完成需要的功能,并且主线程和协助线程是完全独立进行的.不知道这样说好不好理解,后面慢慢在使用中会有更加详细的讲解. 2.多线程的使用: (1)最简单.最原始的使用方法:Thread oGetArgThre

  • C#多线程与异步的区别详解

    C#多线程与异步的区别详解 随着拥有多个硬线程 CPU(超线程.双核)的普及,多线程和异步操作等并发程序设计方法也受到了更多的关注和讨论.本文主要是想与各位高手一同探讨一下如何使用并发来最大化程序的性能. 多线程和异步操作的异同 多线程和异步操作两者都可以达到避免调用线程阻塞的目的,从而提高软件的可响应性.甚至有些时候我们就认为多线程和异步操作是等同的概念.但是,多线程和异步操作还是有一些区别的.而这些区别造成了使用多线程和异步操作的时机的区别. 异步操作的本质 所有的程序最终都会由计算机硬件来

  • 深入分析C#中的异步和多线程

    许多开发人员对异步代码和多线程以及它们的工作原理和使用方法都有错误的认识.在这里,你将了解这两个概念之间的区别,并使用c#实现它们. 我:"服务员,这是我第一次来这家餐厅.通常需要4个小时才能拿到食物吗?" 服务员:"哦,是的,先生.这家餐厅的厨房里只有一个厨师." 我:"--只有一个厨师吗?" 服务员:"是的,先生,我们有好几个厨师,但每次只有一个在厨房工作." 我:"所以其他10个穿着厨师服站在厨房里的人--什么

  • C#中异步和多线程的区别介绍

    一.区别和联系 异步和多线程有什么区别?其实,异步是目的,而多线程是实现这个目的的方法.异步是说,A发起一个操作后(一般都是比较耗时的操作,如果不耗时的操作就没有必要异步了),可以继续自顾自的处理它自己的事儿,不用干等着这个耗时操作返回..Net中的这种异步编程模型,就简化了多线程编程,我们甚至都不用去关心Thread类,就可以做一个异步操作出来. 异步有的时候用普通的线程,有的时候用系统的异步调用功能.有一些IO操作也是异步的,但是未必需要一个线程来运行.例如:硬件是有DMA功能的,在调用DM

  • 个人对于异步和多线程的关系的理解分享

    个人的理解是这样的: 1.  异步通信的意思是,当A发送完消息之后,不等待B的回应,继续执行之后的程序.在将来的某个时刻,A再来检查是否收到B的回应. 异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作. 2. 多线程是程序设计的逻辑层概念,它是进程中并发运行的一段代码.多线程可以实现线程间的切换执行. 3. 异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待.协调运行.线程就是实现异步的一个方式.异步是让调用方法的主线程不需要同步等待另一线

  • 深入浅析python中的多进程、多线程、协程

    进程与线程的历史 我们都知道计算机是由硬件和软件组成的.硬件中的CPU是计算机的核心,它承担计算机的所有任务. 操作系统是运行在硬件之上的软件,是计算机的管理者,它负责资源的管理和分配.任务的调度. 程序是运行在系统上的具有某种功能的软件,比如说浏览器,音乐播放器等. 每次执行程序的时候,都会完成一定的功能,比如说浏览器帮我们打开网页,为了保证其独立性,就需要一个专门的管理和控制执行程序的数据结构--进程控制块. 进程就是一个程序在一个数据集上的一次动态执行过程. 进程一般由程序.数据集.进程控

  • 在.NET Core中使用异步编程的方法步骤

    近期对于异步和多线程编程有些启发,所以我决定把自己的理解写下来. 思考:为什么要使用异步编程? 我们先看看同步方法和异步方法之前在程序中执行的逻辑: 1. 同步方法 static void Main(string[] args) { Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ms")}:开始"); // 调用同步方法 SyncTestMethod(); Console.WriteL

  • 实例分析javascript中的异步

    js 异步解析 一 .js单线程分析 我们都知道js的一大特点是单线程,也就是同一时间点,只能处理一件事,一句js代码.那为什么js要设计成单线程而不是多线程呢?这主要和js的用途有关,js作为浏览器端的脚本语言,主要的用途为用户与服务端的交互与操作dom.而操作dom就注定了js只能是单线程语言.假如js才取多线程将会出现,多个线程同时对一个dom进行操作的情况,浏览器将无法判断如何渲染.不仅js是单线程,浏览器渲染dom也是单线程的,js的执行和浏览器渲染dom共用的一个线程,这就导致了在h

  • Node8中AsyncHooks异步生命周期

    Async Hooks 是 Node8 新出来的特性,提供了一些 API 用于跟踪 NodeJs 中的异步资源的生命周期,属于 NodeJs 内置模块,可以直接引用. const async_hooks = require('async_hooks'); 这是一个很少使用的模块,为什么会有这个模块呢? 我们都知道,JavaScript在设计之初就是一门单线程语言,这和他的设计初衷有关,最初的JavaScript仅仅是用来进行页面的表单校验,在低网速时代降低用户等待服务器响应的时间成本.随着Web

  • python中asyncio异步编程学习

    1.   想学asyncio,得先了解协程 携程的意义: 计算型的操作,利用协程来回切换执行,没有任何意义,来回切换并保存状态 反倒会降低性能. IO型的操作,利用协程在IO等待时间就去切换执行其他任务,当IO操作结束后再自动回调,那么就会大大节省资源并提供性能,从而实现异步编程(不等待任务结束就可以去执行其他代码 2.协程和多线程之间的共同点和区别: 共同点: 都是并发操作,多线程同一时间点只能有一个线程在执行,协程同一时间点只能有一个任务在执行: 不同点: 多线程,是在I/O阻塞时通过切换线

  • 深入理解.NET中的异步

    一.前言 网上有很多关于 .NET async/await 的介绍,但是很遗憾,很少有正确的,甚至说大多都是"从现象编原理"都不过分. 最典型的比如通过前后线程 ID 来推断其工作方式.在 async 方法中用 Thread.Sleep 来解释 Task 机制而导出多线程模型的结论.在 Task.Run 中包含 IO bound 任务来推出这是开了一个多线程在执行任务的结论等等. 看上去似乎可以解释的通,可是很遗憾,无论是从原理还是结论上看都是错误的. 要了解 .NET 中的 asyn

  • Python异步爬虫多线程与线程池示例详解

    目录 背景 异步爬虫方式 多线程,多进程(不建议) 线程池,进程池(适当使用) 单线程+异步协程(推荐) 多线程 线程池 背景 当对多个url发送请求时,只有请求完第一个url才会接着请求第二个url(requests是一个阻塞的操作),存在等待的时间,这样效率是很低的.那我们能不能在发送请求等待的时候,为其单独开启进程或者线程,继续请求下一个url,执行并行请求 异步爬虫方式 多线程,多进程(不建议) 好处:可以为相关阻塞的操作单独开启线程或者进程,阻塞操作就可以异步会执行 弊端:不能无限制开

随机推荐