29

I thought one of the points about async/await is that when the task completes, the continuation is run on the same context when the await was called, which would, in my case, be the UI thread.

So for example:

Debug.WriteLine("2: Thread ID: " + Thread.CurrentThread.ManagedThreadId);
await fs.ReadAsync(data, 0, (int)fs.Length);
Debug.WriteLine("3: Thread ID: " + Thread.CurrentThread.ManagedThreadId);

I would NOT expect this:

2: Thread ID: 10
3: Thread ID: 11

What gives? Why is the thread ID for the continuation different than the UI thread?

According to this article[^] I would need to explicitly call ConfigureAwait to change the behavior of the continuation context!

noseratio
  • 59,932
  • 34
  • 208
  • 486
Marc Clifton
  • 756
  • 1
  • 11
  • 19
  • 1
    Can you write a minimal repro and post here or in a Gist? – Stephen Cleary Feb 17 '14 at 20:35
  • Are you using `ConfigureAwait(false)` anywhere *before* this fragment? Or perhaps this code fragment is used inside an async lambda like this: `await Task.Run(async () => { /* no synchronization context here! */ })` ? – noseratio Feb 17 '14 at 21:28
  • 1
    Nope. Didn't even know about ConfigureAwait until I started googling this issue. – Marc Clifton Feb 17 '14 at 21:29

3 Answers3

42

When you await, by default the await operator will capture the current "context" and use that to resume the async method.

This "context" is SynchronizationContext.Current unless it is null, in which case it is TaskScheduler.Current. (If there is no currently-running task, then TaskScheduler.Current is the same as TaskScheduler.Default, the thread pool task scheduler).

It's important to note that a SynchronizationContext or TaskScheduler does not necessarily imply a particular thread. A UI SynchronizationContext will schedule work to the UI thread; but the ASP.NET SynchronizationContext will not schedule work to a particular thread.

I suspect that the cause of your problem is that you are invoking the async code too early. When an application starts, it just has a plain old regular thread. That thread only becomes the UI thread when it does something like Application.Run.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Wow - that's exactly it -- Normally this function gets called when the user clicks on a folder and the UI is up and running, but I'm also calling this function right before Application.Run to initialize with the root directory! I would not have thought of that -- thank you so much! – Marc Clifton Feb 17 '14 at 21:37
  • hello. Https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html it says there that asp.net does by default also returns on the calling thread. it is a particular thread. it just differs from request to request. Why does asp.net do that by default ? does it have any thing to do with the relation to the reuqestcontext? – eran otzap Dec 24 '18 at 07:23
  • @eranotzap: That blog post states that ASP.NET (classic) resumes on a request context (not a particular thread). The request context manages things like `HttpContext.Current` and the current culture. ASP.NET Core has a better design that does not require a `SynchronizationContext`: https://blog.stephencleary.com/2017/03/aspnetcore-synchronization-context.html – Stephen Cleary Dec 24 '18 at 16:13
  • @StephenCleary My question is regarding ASP.net mvc. the await would return on the calling thread if ConfigureAwait(false) is not used, correct ? – eran otzap Dec 24 '18 at 16:44
  • 1
    @eranotzap: It might or might not. It *will* resume on a thread that has the same `HttpContext.Current`, culture, etc. This thread might or might not be the same thread that was running the code before the `await`. – Stephen Cleary Dec 25 '18 at 02:22
  • @StephenCleary that's a great answer, Thanks. I didn't understand that from the blog post. And it's a blog post i send colleges to remind them to use ConfigureAwait(false) – eran otzap Dec 25 '18 at 09:00
6

The await expression will use the value of SynchronizationContext.Current to return control flow back to the thread on which it occurred. In cases where this is null it will default to the TaskScheduler.Current. The implementation relies solely on this value to change the thread context when the Task value completes. It sounds like in this case the await is capturing a context that isn't bound to the UI thread

JaredPar
  • 733,204
  • 149
  • 1,241
  • 1,454
  • 3
    Actually, if `SynchronizationContext.Current` is `null`, then `await` will use `TaskScheduler.Current`. And neither of those schedulers necessarily return back to the *same* thread. But I agree that the op is likely not calling this *from* the UI thread. – Stephen Cleary Feb 17 '14 at 20:34
  • 1
    I am a little late for the party, but how can i *enforce* resuming on the same thread? After a call to `Task.Delay` my function always resumes on a different thread, which i must prevent. – Benni Mar 12 '17 at 18:09
-1

By default, Windows Forms sets up the synchronization context when the first control is created.

So you can do like this:

[STAThread]
static void Main()
{
    Application.SetHighDpiMode(HighDpiMode.SystemAware);
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    var f = new Form1();
    //var s=SynchronizationContext.Current;
    //var s2=TaskScheduler.Current;
    GetAsync();
    Application.Run(f);
}

static async Task<bool> GetAsync()
{
    MessageBox.Show($"thread id {Thread.CurrentThread.ManagedThreadId}");
    bool flag = await Task.Factory.StartNew<bool>(() =>
    {
       Thread.Sleep(1000);
       return true;
    });
    MessageBox.Show($"thread id {Thread.CurrentThread.ManagedThreadId}");
    return flag;
}
Jim
  • 489
  • 5
  • 11