为什么我的异步代码在另一个线程上执行?

14 浏览
0 Comments

为什么我的异步代码在另一个线程上执行?

最近我在阅读关于async/await的内容时,发现许多文章/帖子都提到使用async await时不会创建新线程(参考链接)。我创建了一个简单的控制台应用程序进行测试。

以下是代码的输出结果:

Main: 8

Main Async: 8

thisIsAsyncStart: 8

thisIsAsyncEnd: 9

Main End: 8

我是否理解错了,为什么thisIsAsyncEnd的线程ID与其他操作不同?

编辑:

我按照下面答案中的建议更新了代码,但结果仍然相同。

下面是来自下面答案的引用:

相反,它将方法分成多个部分,其中一些可能会异步运行。

我想知道异步部分在哪里运行,如果没有创建其他线程的话?

如果它在同一个线程上运行,长时间的I/O请求不应该阻塞它吗?或者编译器是否足够聪明,在操作花费太长时间时将其移动到另一个线程,并在所有操作完成后使用新线程?

0
0 Comments

当你在控制台应用程序的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方法来指定线程上下文,以确保异步操作在正确的线程上下文中执行。

0
0 Comments

为什么我的异步代码会在另一个线程上执行?

我也曾对此感到困惑。对我而言,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 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分离开来。

好的例子。但要记住,有两种类型的任务:委托任务和Promise任务。只有委托任务实际上会执行代码(因此有一个它们运行的线程)。这些是StartNew和(在某种程度上)Task.Run。大多数与await一起使用的任务都是Promise任务,它们没有代码(也没有线程)。这些是I/O操作、Task.Delay、async Task等。所以,Task.Run确实使用了线程池,但是它也并不能代表大多数异步代码。

感谢您的澄清。我期待在实际实现中遇到第一个用例。

0
0 Comments

为什么我的异步代码在另一个线程上执行?

异步和等待关键字的理解,请参阅我的异步介绍文章。特别是,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将以同步方式继续执行。如果要看到它切换线程,请增加延迟的时间。

0