Where do 'awaited' tasks execute?

2020-03-19 03:57发布

问题:

Consider the following:

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. The first Write in DoItAsync() executes.
  2. SomeLongJobAsync() starts.
  3. The WriteLine in DoItAsync() executes.
  4. DoItAsync() pauses while SomeLongJobAsync() works away until it's done.
  5. SomeLongJobAsync() completes, so DoItAsync() returns.

Meanwhile, the UI is responsive.

On what thread does SomeLongJobAsync() execute?

回答1:

Short Answer

An async method fired by the GUI thread will execute on the same thread, whenever there are CPU operation to execute. Other async methods start running on the calling thread and continue on a ThreadPool thread.

Long Answer

SomeLongJobAsync starts executing on the calling thread (the one that printed "She'll be coming round the mountain") up until it reaches an await. Then a task is returned that represents the asynchronous operation + the continuation after it. When the entire operation is done the task will complete (unless it completes prematurely due to an exception or cancellation).

When Task.Delay(1000) itself is "executing" there is no thread, because none is needed. And when finally Task.Delay(1000) ends, a thread is needed to resume on. Which thread it is depends on the SynchronizationContext (by default there is none so the thread is a ThreadPool thread, but in a GUI application it's the singe GUI thread, more here). That thread executes the rest of the code until it reaches another asynchronous point (i.e. another await) and so forth and so forth.



回答2:

The important thing to realise is that async is not about creating threads, it's about replacing what used to be a blocking call by one that returns a continuation. A thread blocks when it is placed on a queue and it can then do nothing until the thing it blocked on becomes available (or a timeout or exception, etc). Blocking the UI thread is a terrible thing to do.

By contrast a continuation contains enough information captured at a point in the program for the thread to carry on ("continue") from that exact point at a later time. Obviously there is special stuff needed in all the Async calls for that to work, but that's what it does. It freezes the thread state into a continuation, parks that somewhere, returns immediately (so no blocking) and then (somehow) starts again from that state at a later time when the required information is available.

For that reason, you can assume that the work for both the async method and the "long job" will be done on the same thread, just not at the same time, and that the operating system will choose a good time to decide when to make those choices.

In practice there is a difference between threads that have a message pump (UI threads) and others, and there is the possibility of work being moved to a different thread, and there are various features in the Task, the SynchronizationContext and thread pools to support more advanced scenarios.

But I think the key to answering your question and for you to understand is this subtle use of something new called a continuation, and how it can capture the state of a program at one time for use later. Continuations have been used in functional languages for a long time, and are in some ways related to the concepts of futures and promises in other languages. Once you think in these terms you can forget about threads entirely.



回答3:

SomeLongJobAsync starts executing on the thread that called it, and if there is one, the current SynchronizationContext is saved in the state machine generated by the await mechanism.

After the first await completes, the continuation of the method is posted on the current SynchronizationContext. In a GUI app this means that the continuation executes on the UI thread. In a console app, there is no SyncrhronizatonContext so the continuation executes on a thread pool thread.

You can check this by printing out the ManagedThreadId of Thread.CurrentThread as your program executes. Consider this modified version of your code (which I ran from a console app on 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();
}

Output from 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

As you can see the method started runnung on thread 21, but as each of the await completed it continued on a thread pool thread and not always the same one. In this case 11, 12. If I run this in a Windows Forms app the output is this:

Output from 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


回答4:

It is executed on the same thread. The documentation explains:

The async and await keywords don't cause additional threads to be created. Async methods don't require multithreading because an async method doesn't run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is active. You can use Task.Run to move CPU-bound work to a background thread, but a background thread doesn't help with a process that's just waiting for results to become available.