"等待"任务在哪里执行?



考虑以下内容:

private async void btnSlowPoke_Click(object sender, EventArgs e)
{
    await DoItAsync();
}
private async Task<int> SomeLongJobAsync()
{
    for (int x = 0; x < 999999; x++)
    {
        //ponder my existence for one second
        await Task.Delay(1000);
    }
    return 42;
}
public async Task<int> DoItAsync()
{
    Console.Write("She'll be coming round the mountain");
    Task<int> t = SomeLongJobAsync();  //<--On what thread does this execute?
    Console.WriteLine(" when she comes.");
    return await t;
}
  1. 执行DoItAsync()的第一个Write。
  2. SomeLongJobAsync()启动
  3. DoItAsync()中的WriteLine执行。
  4. DoItAsync()暂停,SomeLongJobAsync()工作直到完成。
  5. SomeLongJobAsync()完成,所以DoItAsync()返回。

同时,UI是响应的。

SomeLongJobAsync()在哪个线程上执行?

简短回答

GUI线程触发的async方法将在同一个线程上执行,只要有CPU操作要执行。其他async方法在调用线程上开始运行,并在ThreadPool线程上继续运行。

长回答

SomeLongJobAsync开始在调用线程(打印"She'll be coming around the mountain"的线程)上执行,直到到达await。然后返回一个任务,该任务表示异步操作+其后的延续。当整个操作完成时,任务将完成(除非由于异常或取消而提前完成)。

Task.Delay(1000)本身正在"执行"时,没有线程,因为不需要。当最终Task.Delay(1000)结束时,一个线程需要恢复。它是哪个线程取决于SynchronizationContext(默认情况下有none所以线程是ThreadPool线程,但在GUI应用程序中它是单个GUI线程,更多信息在这里)。该线程执行剩下的代码,直到它到达另一个异步点(即另一个await),以此类推。

重要的是要认识到异步不是关于创建线程,而是关于用返回延续的调用替换过去的阻塞调用。当线程被放置在队列中时,它就会阻塞,然后它什么都不做,直到它阻塞的东西变得可用(或超时或异常等)。阻塞UI线程是一件很糟糕的事情。

相比之下,continuation包含了在程序中某个点捕获的足够的信息,以便线程在稍后的时间从该点继续("continue")。显然,所有的Async调用都需要特殊的东西来工作,但这就是它的作用。它将线程状态冻结为一个延续,将其停放在某个地方,立即返回(因此没有阻塞),然后在稍后所需信息可用时(以某种方式)从该状态重新开始。

因此,您可以假设异步方法和"长作业"的工作将在同一个线程上完成,只是不是在同一时间,并且操作系统将选择一个合适的时间来决定何时做出这些选择。

在实践中,具有消息泵(UI线程)的线程与其他线程之间存在差异,并且有可能将工作转移到不同的线程,并且Task, SynchronizationContext和线程池中有各种功能来支持更高级的场景。

但我认为回答你的问题和让你理解的关键是这种被称为延续的新东西的微妙使用,以及它如何在一次捕获程序的状态以供以后使用。延续已经在函数式语言中使用了很长时间,并且在某些方面与其他语言中的未来和承诺概念相关。一旦你用这些术语思考,你就可以完全忘记线程了。

SomeLongJobAsync开始在调用它的线程上执行,如果有,则当前的SynchronizationContext保存在由await机制生成的状态机中。

第一个await完成后,该方法的延续被发布到当前的SynchronizationContext上。在GUI应用程序中,这意味着延续在UI线程上执行。在控制台应用程序中,没有SyncrhronizatonContext,所以continuation在线程池线程上执行。

可以通过在程序执行时打印出Thread.CurrentThreadManagedThreadId来检查这一点。考虑一下修改后的代码(我从Linqpad上的控制台应用程序运行):

private async void btnSlowPoke_Click(object sender, EventArgs e)
{
    await DoItAsync();
}
private async Task<int> SomeLongJobAsync()
{
    Console.WriteLine("Start SomeLongJobAsync, threadId = " + Thread.CurrentThread.ManagedThreadId);
    for (int x = 0; x < 9; x++)
    {
        //ponder my existence for one second
        await Task.Delay(1000);
        Console.WriteLine("Continue SomeLongJobAsync, threadId = " + Thread.CurrentThread.ManagedThreadId);
    }
    return 42;
}
public async Task<int> DoItAsync()
{   
    Console.WriteLine("She'll be coming round the mountain, threadId = " + Thread.CurrentThread.ManagedThreadId);
    Task<int> t = SomeLongJobAsync();  //<--On what thread does this execute?
    Console.WriteLine(" when she comes., threadId = " + Thread.CurrentThread.ManagedThreadId);
    return await t;
}
void Main()
{
    btnSlowPoke_Click(null, null);
    Console.ReadLine();
}

Console App输出:

She'll be coming round the mountain, threadId = 21
Start SomeLongJobAsync, threadId = 21
 when she comes., threadId = 21
Continue SomeLongJobAsync, threadId = 11
Continue SomeLongJobAsync, threadId = 11
Continue SomeLongJobAsync, threadId = 11
Continue SomeLongJobAsync, threadId = 11
Continue SomeLongJobAsync, threadId = 11
Continue SomeLongJobAsync, threadId = 12
Continue SomeLongJobAsync, threadId = 12
Continue SomeLongJobAsync, threadId = 12
Continue SomeLongJobAsync, threadId = 12

正如您所看到的,该方法在线程21上开始运行,但是当每个await完成时,它继续在线程池线程上运行,并且不总是同一个线程。这里是11,12。如果我在Windows窗体应用程序中运行此命令,输出如下:

Windows Forms App输出:

She'll be coming round the mountain, threadId = 8
Start SomeLongJobAsync, threadId = 8
 when she comes., threadId = 8
Continue SomeLongJobAsync, threadId = 8
Continue SomeLongJobAsync, threadId = 8
Continue SomeLongJobAsync, threadId = 8
Continue SomeLongJobAsync, threadId = 8
Continue SomeLongJobAsync, threadId = 8
Continue SomeLongJobAsync, threadId = 8
Continue SomeLongJobAsync, threadId = 8
Continue SomeLongJobAsync, threadId = 8
Continue SomeLongJobAsync, threadId = 8

在同一个线程中执行。文档解释:

async和await关键字不会导致创建额外的线程。异步方法不需要多线程,因为异步方法不在自己的线程上运行。该方法在当前同步上下文上运行,并且仅在该方法处于活动状态时使用线程上的时间。您可以使用Task。运行将cpu密集型的工作转移到后台线程,但是后台线程对等待结果可用的进程没有帮助。

最新更新