0

I'm working on trying to understand how Task.ContinueWith works. Consider the following code:

private async void HandleButtonClick(object sender, EventArgs e)
{
    Console.WriteLine($"HandleButtonClick: a {GetTrdLabel()}");
    var t1 = Task.Run(() => DoSomethingAsync("First time"));
    Console.WriteLine($"HandleButtonClick: b {GetTrdLabel()}");
    await t1;
    Console.WriteLine($"HandleButtonClick: c {GetTrdLabel()}");

    var t2 = t1.ContinueWith(async (t) =>
    {
        Console.WriteLine($"t3: a {GetTrdLabel()}");
        Thread.Sleep(2000);
        Console.WriteLine($"t3: b {GetTrdLabel()}");
        await DoSomethingAsync("Second time");
        Console.WriteLine($"t3: c {GetTrdLabel()}");
    });

    Console.WriteLine($"HandleButtonClick: d {GetTrdLabel()}");
    await t2;
    Console.WriteLine($"HandleButtonClick: e {GetTrdLabel()}");
}

private async Task DoSomethingAsync(string label)
{
    Console.WriteLine($"DoSomethingElseAsync ({label}): a {GetTrdLabel()}");
    Thread.Sleep(2000);
    Console.WriteLine($"DoSomethingElseAsync ({label}): b {GetTrdLabel()}");
    await Task.Delay(2000);
    Console.WriteLine($"DoSomethingElseAsync ({label}): c {GetTrdLabel()}");
}

private string GetTrdLabel() => $"({Thread.CurrentThread.ManagedThreadId})";

The output is below. My question is about the highlighted lines: why isn't the first one continuing on the captured context after the await -- i.e. managed thread ID 3 -- since I didn't use .ConfigureAwait(false)? The second one is continuing as expected -- i.e. thread ID 4.

I feel like it has something to do with the "...attempt to marshal the continuation back to the original context captured" (emphasis mine) from the documentation, but I don't understand why the attempt fails in the first case.

enter image description here

rory.ap
  • 34,009
  • 10
  • 83
  • 174
  • 6
    You are confusing the thread Id with the context. `await` resumes in the same context, but all background threads share the same context. The first DoSomethingAsync runs on a background thread, so the `await` will resume on another background thread (which may be a different background thread from the one the task started on). – Raymond Chen Apr 04 '19 at 16:54

1 Answers1

3

trying to understand how Task.ContinueWith works

It's better to just ignore ContinueWith and use await instead. But, if you would like to learn the low-level, dangerous way to do things, then I'll oblige. Please do not use in production.

The first thing to note is that ContinueWith always schedules work to a task scheduler. And it does not use the default task scheduler by default; it uses the current task scheduler by default. Assuming that HandleButtonClick is invoked directly by your UI framework (and not, e.g., scheduled using a task scheduler), then there is no current task scheduler, so ContinueWith will use the default task scheduler, which is the thread pool task scheduler. To avoid this kind of convoluted reasoning, code should always pass a TaskScheduler to ContinueWith.

The next thing to note is that ContinueWith does not understand async delegates. As far as it's concerned, an async lambda is just a fancy way to give it a Func<Task> delegate, and ContinueWith only cares about the initial synchronous part of that method.

The final thing to note is that thread pool threads are considered interchangeable. This is true for any async/await code; if it is running in a thread pool context and performs an await, it can resume executing on any thread pool thread. It may or may not be the same thread that was running the code before the await.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810