locked
How does an async method get executed if you're not starting another thread?

    問題

  • Okay, I'm probably missing something very obvious. I came across this example today and it surprised me:

    private async void button1_Click(object sender, EventArgs e)
    {
        string result = await WaitAsynchronouslyAsync();
        textBox1.Text += result;
    }
    
    public async Task<string> WaitAsynchronouslyAsync()
    {
        await Task.Delay(10000);
        return "Finished";
    }

    I expected to see a Task.Run() in here somewhere. I don't understand who's executing the code in WaitAsynchronouslyAsync.

    I mean, my understanding is that when button1_Click gets to the await call it's going to "sing up" the rest of its code (ie the assignment to textBox1.Text) as a continuation of the Task returned by WaitAsynchronouslyAsync, and then return to the UI thread. And that nothing else will happen related to our Task until it finishes. But how is it going to finish if it's not being executed somewhere? Who's executing it if the UI thread has gone back to it's message loop and is no longer involved with the Task??


    Michael

    2012年7月30日 下午 06:29

解答

  • What await does is that it translates your code to code that is mostly equivalent with:

    private void button1_Click(object sender, EventArgs e)
    {
        var task = WaitAsynchronouslyAsync();
        task.ContinueWith(t => textBox1.Text += t.Result, TaskScheduler.FromCurrentSynchronizationContext());
    }

    The part with synchronization context is important. What it means is that the continuation will be scheduled on the UI thread (assuming it's processing it message loop). So the UI thread will be involved with the Task again in the future, when the Task completes.

    If there is no synchronization context (or if you use ConfigureAwait(false)), the continuation will execute on the ThreadPool, but that's not the case here.

    2012年7月30日 下午 07:30
  • Well, the Task returned from Task.Delay() doesn't really run itself, it has no code that needs to run. Instead, there is a timer that completes the Task at the right time, so that its continuation can execute (and it will execute in the right context, which here means the UI thread).

    If you wanted to implement Task.Delay() yourself, you could do it using a Timer and a TaskCompletionSource:

    public static Task Delay(int milliseconds)
    {
    	var tcs = new TaskCompletionSource<bool>();
    	var timer = new Timer { AutoReset = false, Interval = milliseconds };
    	timer.Elapsed += (sender, args) => tcs.SetResult(true);
    	timer.Start();
    	return tcs.Task;
    }

    This specific implementation is buggy, because the Timer could get garbage collected, but I hope you get the idea. When the timer elapses, it's the ContinueWith() that takes care of executing the continuation (like setting the other Task to "Finished") in the correct context.

    2012年7月30日 下午 09:08

所有回覆

  • What await does is that it translates your code to code that is mostly equivalent with:

    private void button1_Click(object sender, EventArgs e)
    {
        var task = WaitAsynchronouslyAsync();
        task.ContinueWith(t => textBox1.Text += t.Result, TaskScheduler.FromCurrentSynchronizationContext());
    }

    The part with synchronization context is important. What it means is that the continuation will be scheduled on the UI thread (assuming it's processing it message loop). So the UI thread will be involved with the Task again in the future, when the Task completes.

    If there is no synchronization context (or if you use ConfigureAwait(false)), the continuation will execute on the ThreadPool, but that's not the case here.

    2012年7月30日 下午 07:30
  • Thanks for your reply.

    I think the piece I am probably missing is that Task.Delay is taking care of getting itself executed. I.e., it's starting up a "hot task" that's already running (perhaps somehow using the GUI thread, perhaps using some other thread.. as the consumer of this task I don't know or care, right?)

    Michael

    2012年7月30日 下午 08:34
  • Well, the Task returned from Task.Delay() doesn't really run itself, it has no code that needs to run. Instead, there is a timer that completes the Task at the right time, so that its continuation can execute (and it will execute in the right context, which here means the UI thread).

    If you wanted to implement Task.Delay() yourself, you could do it using a Timer and a TaskCompletionSource:

    public static Task Delay(int milliseconds)
    {
    	var tcs = new TaskCompletionSource<bool>();
    	var timer = new Timer { AutoReset = false, Interval = milliseconds };
    	timer.Elapsed += (sender, args) => tcs.SetResult(true);
    	timer.Start();
    	return tcs.Task;
    }

    This specific implementation is buggy, because the Timer could get garbage collected, but I hope you get the idea. When the timer elapses, it's the ContinueWith() that takes care of executing the continuation (like setting the other Task to "Finished") in the correct context.

    2012年7月30日 下午 09:08