温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

C#中异步与多线程的作用有哪些

发布时间:2021-01-18 15:18:32 来源:亿速云 阅读:154 作者:Leah 栏目:开发技术

C#中异步与多线程的作用有哪些?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。

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#中异步与多线程的作用有哪些问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注亿速云行业资讯频道了解更多相关知识。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI