async /等待不同的线程ID

我最近正在阅读关于异步/等待的事情,我很惊讶我正在阅读的许多文章/post说明在使用异步等待时没有创建新线程( 示例 )。

我创建了一个简单的控制台应用程序来测试它

class Program { static void Main(string[] args) { Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId); MainAsync(args).Wait(); Console.WriteLine("Main End: " + Thread.CurrentThread.ManagedThreadId); Console.ReadKey(); } static async Task MainAsync(string[] args) { Console.WriteLine("Main Async: " + Thread.CurrentThread.ManagedThreadId); await thisIsAsync(); } private static async Task thisIsAsync() { Console.WriteLine("thisIsAsyncStart: " + Thread.CurrentThread.ManagedThreadId); await Task.Delay(1); Console.WriteLine("thisIsAsyncEnd: " + Thread.CurrentThread.ManagedThreadId); } } 

以下代码的输出是:

 Main: 8 Main Async: 8 thisIsAsyncStart: 8 thisIsAsyncEnd: 9 Main End: 8 

我错过了这一点,或者这个AsyncEnd是否具有与其他操作不同的线程ID?

编辑:

我已按照下面的答案中的建议更新代码以await Task.Delay(1) ,但我仍然看到相同的结果。

引用下面的答案:

 Rather, it enables the method to be split into multiple pieces, some of which may run asynchronously 

我想知道asynchronously部分在哪里运行,如果没有创建其他线程? 如果它在同一个线程上运行,不应该由于长I / O请求而阻塞它,或者编译器是否足够聪明,如果它花费太长时间将该操作移动到另一个线程,并且毕竟使用了新线程?

我建议您阅读我的async简介post,以了解asyncawait关键字。 特别是, await (默认情况下)将捕获“上下文”并使用该上下文来恢复其异步方法。 这个“上下文”是当前的SynchronizationContext (如果没有SynchronzationContext ,则为TaskScheduler )。

我想知道异步部分在哪里运行,如果没有创建其他线程? 如果它在同一个线程上运行,不应该由于长I / O请求而阻塞它,或者编译器是否足够聪明,如果它花费太长时间将该操作移动到另一个线程,并且毕竟使用了新线程?

正如我在博客上解释的那样, 真正的异步操作不会在任何地方“运行” 。 在这种特殊情况下( Task.Delay(1) ),异步操作基于一个计时器, 而不是一个在某个地方阻塞做Thread.Sleep的线程。 大多数I / O都以相同的方式完成。 例如, HttpClient.GetAsync基于重叠(异步)I / O, 而不是在某处等待HTTP下载完成的线程。


一旦了解了await如何使用其上下文,就可以更轻松地浏览原始代码:

 static void Main(string[] args) { Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId); MainAsync(args).Wait(); // Note: This is the same as "var task = MainAsync(args); task.Wait();" Console.WriteLine("Main End: " + Thread.CurrentThread.ManagedThreadId); Console.ReadKey(); } static async Task MainAsync(string[] args) { Console.WriteLine("Main Async: " + Thread.CurrentThread.ManagedThreadId); await thisIsAsync(); // Note: This is the same as "var task = thisIsAsync(); await task;" } private static async Task thisIsAsync() { Console.WriteLine("thisIsAsyncStart: " + Thread.CurrentThread.ManagedThreadId); await Task.Delay(1); // Note: This is the same as "var task = Task.Delay(1); await task;" Console.WriteLine("thisIsAsyncEnd: " + Thread.CurrentThread.ManagedThreadId); } 
  1. 主线程开始执行Main并调用MainAsync
  2. 主线程正在执行MainAsync并调用thisIsAsync
  3. 主线程正在执行thisIsAsync并调用Task.Delay
  4. Task.Delay完成它的任务 – 启动一个计时器和诸如此类的东西 – 并返回一个不完整的任务(注意Task.Delay(0)将返回一个完成的任务,这会改变行为)。
  5. 主线程返回thisIsAsync并等待从Task.Delay返回的任务。 由于任务不完整,因此从thisIsAsync返回不完整的任务。
  6. 主线程返回MainAsync并等待MainAsync返回的任务。 由于任务不完整,因此从MainAsync返回不完整的任务。
  7. 主线程返回Main并从MainAsync返回的任务上调用Wait 。 这将阻止主线程,直到MainAsync完成。
  8. Task.Delay设置的计时器关闭时, thisIsAsync将继续执行。 由于await没有捕获SynchronizationContextTaskScheduler ,它将继续在线程池线程上执行。
  9. 线程池线程到达thisIsAsync的末尾,完成其任务。
  10. MainAsync继续执行。 由于await没有捕获上下文,因此它继续在线程池线程上执行(实际上是运行thisIsAsync的同一线程)。
  11. 线程池线程到达MainAsync的末尾,完成其任务。
  12. 主线程从其对Wait调用返回并继续执行Main方法。 用于继续thisIsAsyncMainAsync的线程池线程不再需要并返回到线程池。

这里重要的一点是使用了线程池, 因为没有上下文 。 它“不必要时”自动使用。 如果你在GUI应用程序中运行相同的MainAsync / thisIsAsync代码,那么你会看到非常不同的线程使用情况:UI线程有一个SynchronizationContext ,它将连续调度回到UI线程,所以所有方法都将在同一个UI线程上恢复。

使用async创建方法并不意味着它将创建另一个线程。如果RunTime看到你的async方法中使用await调用的方法被延迟,它将退出该方法并等待等待的方法完成然后继续使用另一个线程的方法。尝试将Task.Delay(2000)更改为Task.Delay(0) ,您将看到它不会创建新的线程

RunTime会计算它,如果它需要创建它会创建,如果不是 – 不。我尝试你的例子0毫秒,并得到所有相同的线程:

 Main: 1 Main Async: 1 thisIsAsyncStart: 1 thisIsAsyncEnd: 1 Main End: 1 

摘自Stephen Toub的博客 :

“异步”关键字

应用于方法时,“async”关键字的作用是什么?

当您使用“async”关键字标记方法时,您实际上是在告诉编译器两件事:

  1. 您告诉编译器您希望能够在方法中使用“await”关键字(当且仅当将其所在的方法或lambda标记为async时,才可以使用await关键字)。 在这样做时,您告诉编译器使用状态机编译该方法,这样该方法将能够挂起,然后在等待点异步恢复。
  2. 您告诉编译器“提升”方法的结果或可能出现在返回类型中的任何exception。 对于返回Task或Task的方法,这意味着在方法中未处理的任何返回值或exception都存储在结果任务中。 对于返回void的方法,这意味着任何exception都会通过方法初始调用时当前的“SynchronizationContext”传播到调用者的上下文。

在方法上使用“async”关键字是否强制该方法的所有调用都是异步的?

不。当您调用标记为“async”的方法时,它会在当前线程上开始同步运行。 因此,如果您有一个返回void的同步方法,并且您要做的就是将其标记为“async”,那么该方法的调用仍将同步运行。 无论您将返回类型保留为“void”还是将其更改为“Task”,都是如此。 类似地,如果您有一个返回某些TResult的同步方法,并且您所做的只是将其标记为“async”并将返回类型更改为“Task”,则该方法的调用仍将同步运行。

将方法标记为“异步”不会影响方法是同步还是异步运行。 相反,它使该方法能够分成多个部分,其中一些部分可以异步运行,这样该方法可以异步完成。 这些部分的边界只能在使用“await”关键字显式编码的情况下发生,因此如果在方法的代码中根本不使用“await”,则只有一个部分,因为该部分将开始运行同步,它(和它的整个方法)将同步完成。

我想知道完全一样。 对我来说,MSDN的解释是矛盾的:

async和await关键字不会导致创建其他线程。 异步方法不需要multithreading, 因为异步方法不能在自己的线程上运行

MSDN:使用async和await进行异步编程

await表达式不会阻塞正在执行它的线程 。 [..]当任务完成时,它会调用它的继续,异步方法的执行从它停止的地方恢复。

等待(C#-Referenz)

我不明白如何在不使用额外线程的情况下阻止原始线程。 此外,“调用”措辞表明在某处以某种方式使用了多个线程。

但后来我意识到,一切都写得正确, 这些关键字没有使用任何其他线程。 通过设计Task类来提供可能使用不同线程的机制 – 或者不使用。

虽然stephen-cleary精美地解释了Task.Delay()方法的这些机制,但我扩展了MSDN示例以了解awaitTask.Run()行为方式:

 private async void ds_StartButton_Click(object sender, EventArgs e) { textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Started MSDN Example ..." + Environment.NewLine); // Call the method that runs asynchronously. string result = await WaitAsynchronouslyAsync(); // Call the method that runs synchronously. //string result = await WaitSynchronously (); // Do other Schdaff textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #1 ..." + Environment.NewLine); textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #2 ..." + Environment.NewLine); textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #3 ..." + Environment.NewLine); textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #4 ..." + Environment.NewLine); textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #5 ..." + Environment.NewLine); textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #6 ..." + Environment.NewLine); textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #7 ..." + Environment.NewLine); // Display the result. textBox1.Text += result; } // The following method runs asynchronously. The UI thread is not // blocked during the delay. You can move or resize the Form1 window // while Task.Delay is running. public async Task WaitAsynchronouslyAsync() { Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Entered WaitAsynchronouslyAsync()"); await Task.Delay(10000); Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Task.Delay done, starting random string generation now ..."); await Task.Run(() => LongComputation()); Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Leaving WaitAsynchronouslyAsync() ..."); return DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Finished MSDN Example." + Environment.NewLine; } // The following method runs synchronously, despite the use of async. // You cannot move or resize the Form1 window while Thread.Sleep // is running because the UI thread is blocked. public async Task WaitSynchronously() { // Add a using directive for System.Threading. Thread.Sleep(10000); return DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Finished MSDN Bad Ass Example." + Environment.NewLine; } private void ds_ButtonTest_Click(object sender, EventArgs e) { textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Started Test ..." + Environment.NewLine); Task l_Task = WaitAsynchronouslyAsync(); //WaitAsynchronouslyAsync(); //textBox1.AppendText(l_Result); } private void LongComputation() { Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Generating random string ..."); string l_RandomString = GetRandomString(10000000); Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Random string generated."); } /// Get random string with specified length /// Requested length of random string /// Use case of this is unknown, but assumed to be importantly needed somewhere. Defaults to true therefore. /// But due to huge performance implication, added this parameter to switch this off. /// Random string public static string GetRandomString(int p_Length, bool p_NoDots = true) { StringBuilder l_StringBuilder = new StringBuilder(); string l_RandomString = string.Empty; while (l_StringBuilder.Length <= p_Length) { l_RandomString = (p_NoDots ? System.IO.Path.GetRandomFileName().Replace(".", string.Empty) : System.IO.Path.GetRandomFileName()); l_StringBuilder.Append(l_RandomString); } l_RandomString = l_StringBuilder.ToString(0, p_Length); l_StringBuilder = null; return l_RandomString; } 

从输出中可以看出,有多个线程使用 - 不是通过async/await ,而是通过Task.Run()

 04.11.2016 12:38:06 [10] Entered WaitAsynchronouslyAsync() 04.11.2016 12:38:17 [10] Task.Delay done, starting random string generation now ... 04.11.2016 12:38:17 [12] Generating random string ... 04.11.2016 12:38:21 [12] Random string generated. 04.11.2016 12:38:21 [10] Leaving WaitAsynchronouslyAsync() ... 

这和往常一样,但我个人需要这个明确的例子来了解正在发生的事情,并将async/await所做的事情与Task所做的事情分开。

您的问题的非常好的解释在这里https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/

调用控制台应用程序的Main方法时,SynchronizationContext.Current将返回null。 这意味着如果您在控制台应用程序中调用异步方法,除非您执行一些特殊操作,否则您的异步方法将不具有线程关联性:这些异步方法中的延续可能最终“在任何地方”运行。