Understanding the behavior of TaskScheduler.Current

.net, async-await, c#, task-parallel-library

Solution

If there is no actual task being executed, then `TaskScheduler.Current` is the same as `TaskScheduler.Default`. In other words, `ThreadPoolTaskScheduler` actually acts both as the thread pool task scheduler and the value meaning "no current task scheduler".

The first part of the `async` delegate is scheduled explicitly using the `SynchronizationContextTaskScheduler`, and runs on the UI thread with both a task scheduler and synchronization context. The task scheduler forwards the delegate to the synchronization context.

When the `await` captures its context, it captures the synchronization context (not the task scheduler), and uses that syncctx to resume. So, the method continuation is posted to that syncctx, which executes it on the UI thread.

When the continuation runs on the UI thread, it behaves very similarly to an event handler; the delegate is executed directly, not wrapped in a task. If you check `TaskScheduler.Current` at the beginning of `button1_Click`, you'll find it is also `ThreadPoolTaskScheduler`.

BTW, I recommend you treat this behavior (executing delegates directly, not wrapped in tasks) as an implementation detail.

Problem

Here's a simple WinForms app: ``` using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApplication { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private async void button1_Click(object sender, EventArgs e) { var ts = TaskScheduler.FromCurrentSynchronizationContext(); await Task.Factory.StartNew(async () => { Debug.WriteLine(new { where = "1) before await", currentTs = TaskScheduler.Current, thread = Thread.CurrentThread.ManagedThreadId, context = SynchronizationContext.Current }); await Task.Yield(); // or await Task.Delay(1) Debug.WriteLine(new { where = "2) after await", currentTs = TaskScheduler.Current, thread = Thread.CurrentThread.ManagedThreadId, context = SynchronizationContext.Current }); }, CancellationToken.None, TaskCreationOptions.None, scheduler: ts).Unwrap(); } } } ``` The debug ouput (when the button is clicked): ``` { where = 1) before await, currentTs = System.Threading.Tasks.SynchronizationContextTaskScheduler, thread = 9, context = System.Windows.Forms.WindowsFormsSynchronizationContext } { where = 2) after await, currentTs = System.Threading.Tasks.ThreadPoolTaskScheduler, thread = 9, context = System.Windows.Forms.WindowsFormsSynchronizationContext } ``` The question: Why is `TaskScheduler.Current` changing from `SynchronizationContextTaskScheduler` to `ThreadPoolTaskScheduler` after `await` here? This essentially exhibits the behavior `TaskCreationOptions.HideScheduler` for `await` continuation, which is unexpected and undesirable, in my opinion. This question has been triggered by another question of mine: AspNetSynchronizationContext and await continuations in ASP.NET.

Original source

Related problems