为什么我的异步代码在另一个线程上执行?
为什么我的异步代码在另一个线程上执行?
最近我在阅读关于async/await的内容时,发现许多文章/帖子都提到使用async await时不会创建新线程(参考链接)。我创建了一个简单的控制台应用程序进行测试。
以下是代码的输出结果:
Main: 8
Main Async: 8
thisIsAsyncStart: 8
thisIsAsyncEnd: 9
Main End: 8
我是否理解错了,为什么thisIsAsyncEnd的线程ID与其他操作不同?
编辑:
我按照下面答案中的建议更新了代码,但结果仍然相同。
下面是来自下面答案的引用:
相反,它将方法分成多个部分,其中一些可能会异步运行。
我想知道异步部分在哪里运行,如果没有创建其他线程的话?
如果它在同一个线程上运行,长时间的I/O请求不应该阻塞它吗?或者编译器是否足够聪明,在操作花费太长时间时将其移动到另一个线程,并在所有操作完成后使用新线程?
当你在控制台应用程序的Main方法中调用异步方法时,由于SynchronizationContext.Current为null,异步方法将不具备线程亲和性,即其中的后续操作可能在任何地方执行。
为了解决这个问题,可以通过使用异步方法的ConfigureAwait方法来指定线程上下文。例如,可以使用ConfigureAwait(true)来指定在调用异步方法时保留线程上下文,或使用ConfigureAwait(false)来指定不需要保留线程上下文。以下是一个示例:
public async Task MyMethodAsync() { await Task.Delay(1000).ConfigureAwait(true); // 在调用异步方法时保留线程上下文 } public async Task MyMethodAsync() { await Task.Delay(1000).ConfigureAwait(false); // 不需要保留线程上下文 }
在控制台应用程序中,可以在Main方法中使用异步方法,并使用ConfigureAwait方法来指定线程上下文,以确保异步操作在正确的线程上下文中执行。
为什么我的异步代码会在另一个线程上执行?
我也曾对此感到困惑。对我而言,MSDN上的解释是矛盾的:
“async”和“await”关键字不会创建额外的线程。异步方法不要求多线程,因为异步方法并不在它自己的线程上运行。
MSDN:使用async和await进行异步编程(C#)
await表达式不会阻塞它所在的线程。[...]当任务完成时,它会调用它的后续操作,异步方法的执行将在离开的地方恢复。
await (C#-参考)
我不明白原始线程如何在不使用其他线程的情况下不被阻塞。此外,“invoke”一词暗示着某种方式会使用多个线程。
但后来我意识到,一切都写得很正确,这些关键字并没有使用其他线程。这是Task类的设计,提供了可能使用不同线程的机制,也可能不使用线程。
虽然Stephen Cleary在Stack Overflow上精彩地解释了Task.Delay()方法的这些机制,我扩展了MSDN的示例,以了解await与Task.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 TaskWaitAsynchronouslyAsync() { 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分离开来。
好的例子。但要记住,有两种类型的任务:委托任务和Promise任务。只有委托任务实际上会执行代码(因此有一个它们运行的线程)。这些是StartNew和(在某种程度上)Task.Run。大多数与await一起使用的任务都是Promise任务,它们没有代码(也没有线程)。这些是I/O操作、Task.Delay、async Task等。所以,Task.Run确实使用了线程池,但是它也并不能代表大多数异步代码。
感谢您的澄清。我期待在实际实现中遇到第一个用例。
为什么我的异步代码在另一个线程上执行?
异步和等待关键字的理解,请参阅我的异步介绍文章。特别是,await(默认情况下)会捕获一个“上下文”,并使用该上下文来恢复其异步方法。这个“上下文”是当前的同步上下文(如果没有同步上下文,则是任务调度器)。
我想知道异步部分在哪里运行,如果没有创建其他线程会发生什么?如果它在同一个线程上运行,由于长时间的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(); // 注意:这与"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(); // 注意:这与"var task = thisIsAsync(); await task;"是相同的 } private static async Task thisIsAsync() { Console.WriteLine("thisIsAsyncStart: " + Thread.CurrentThread.ManagedThreadId); await Task.Delay(1); // 注意:这与"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,并等待从thisIsAsync返回的任务。由于任务未完成,MainAsync将返回一个未完成的任务。
7. 主线程返回到Main,并在从MainAsync返回的任务上调用Wait。这将阻塞主线程,直到MainAsync完成。
8. 当Task.Delay设置的定时器触发时,thisIsAsync将继续执行。由于await没有捕获到任何同步上下文或任务调度器,它将在线程池线程上继续执行。
9. 线程池线程执行完thisIsAsync,完成其任务。
10. MainAsync继续执行。由于await没有捕获到任何上下文,它将在线程池线程上(实际上是与执行thisIsAsync的相同线程)继续执行。
11. 线程池线程执行完MainAsync,完成其任务。
12. 主线程从对Wait的调用返回,并继续执行Main方法。用于继续执行thisIsAsync和MainAsync的线程池线程不再需要,并返回线程池。
重要的一点是,线程池被使用是因为没有上下文。它不是在必要时自动使用。如果在GUI应用程序中运行相同的MainAsync/thisIsAsync代码,你会看到非常不同的线程使用情况:UI线程具有将继续安排在UI线程上的上下文,因此所有的方法都将在同一个UI线程上恢复。
谢谢,我会将这个回答更改为正确的答案,因为它包含了更多的细节,使之前的回答有点不准确。
非常好的答案,非常感谢。
所以当执行到'Main'时,它正在等待(或者说执行暂停了)。当在主线程中执行暂停后,执行会由线程池中的一个线程接管并执行继续吗?另外,执行如何知道要从线程池中的不同线程(或者在UI应用程序中是主线程)开始继续执行?
s:我在我的异步介绍文章中解释了这一点:await会捕获一个“上下文”,并在没有“上下文”的情况下使用线程池线程。UI线程提供了一个将await指示继续在UI线程上恢复的上下文。
即使在WPF应用程序中,当我得到的线程是“1”。我错过了什么吗?
如果你指的是thisIsAsyncEnd,那么最有可能的原因是延迟任务在等待时已经完成。在这种情况下,await将以同步方式继续执行。如果要看到它切换线程,请增加延迟的时间。