Here is some easy piece of code to show the unexpected behavior:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
_UI = TaskScheduler.FromCurrentSynchronizationContext();
Loaded += new RoutedEventHandler(MainWindow_Loaded);
}
TaskScheduler _UI;
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Task.Factory.StartNew(() =>
{
//Expected: Worker thread
//Found: Worker thread
DoSomething();
})
.ContinueWith(t =>
{
//Expected: Main thread
//Found: Main thread
DoSomething();
Task.Factory.StartNew(() =>
{
//Expected: Worker thread
//Found: Main thread!!!
DoSomething();
});
}, _UI);
}
void DoSomething()
{
Debug.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString());
}
}
Why is the inner task executed in the main thread? How can i prevent this behavior?
Unfortunately, the Current task scheduler, when you're running your continuation, becomes the SynchronizationContextTaskScheduler
setup by your TaskScheduler.FromCurrentSynchronizationContext
.
This is discussed in this Connect Bug - and was written this way by design in .NET 4. However, I agree that the behavior leaves a bit to be desired here.
You can work around this by grabbing a "background" scheduler in your constructor, and using it:
TaskScheduler _UI;
// Store the default scheduler up front
TaskScheduler _backgroundScheduler = TaskScheduler.Default;
public MainWindow()
{
InitializeComponent();
_UI = TaskScheduler.FromCurrentSynchronizationContext();
Loaded += new RoutedEventHandler(MainWindow_Loaded);
}
Once you have that, you can easily schedule your "background" task appropriately:
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Task.Factory.StartNew(() =>
{
//Expected: Worker thread
//Found: Worker thread
DoSomething();
})
.ContinueWith(t =>
{
//Expected: Main thread
//Found: Main thread
DoSomething();
// Use the _backgroundScheduler here
Task.Factory.StartNew(() =>
{
DoSomething();
}, CancellationToken.None, TaskCreationOptions.None, _backgroundScheduler);
}, _UI);
}
Also, in this case, since your operation is at the end, you could just put it in its own continuation and get the behavior you want. This, however, is not a "general purpose" solution, but works in this case:
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Task.Factory.StartNew(() =>
{
//Expected: Worker thread
//Found: Worker thread
DoSomething();
})
.ContinueWith(t =>
{
//Expected: Main thread
//Found: Main thread
DoSomething();
}, _UI)
.ContinueWith(t =>
{
//Expected: Worker thread
//Found: Is now worker thread
DoSomething();
});
}
Apart from @reed-copsey`s great answer I want to add that if you want to force your task to be executed on a threadpool thread you can also use the TaskScheduler.Default property which always refers to the ThreadPoolTaskScheduler:
return Task.Factory.StartNew(() =>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString());
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
This way you dont have to capture the task scheduler in a variable like proposed in @reed-copsey `s answer.
More information on TaskSchedulers can be found here: TaskSchedulers on MSDN
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With