Daniel Jant
Daniel Jant

Reputation: 53

Inner task is executed in an unexpected thread

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?

Upvotes: 5

Views: 504

Answers (3)

David Roth
David Roth

Reputation: 697

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

Upvotes: 1

Reed Copsey
Reed Copsey

Reputation: 564641

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();
            });
}

Upvotes: 6

Tejs
Tejs

Reputation: 41256

If you want to ensure that your inner task is not run on the UI thread, simply mark it as LongRunning:

Task.Factory.StartNew(() =>
            {
                //Expected: Worker thread
                //Found: Main thread!!!
                DoSomething();
            }, TaskCreationOptions.LongRunning);

That should ensure it is given its own thread instead of inlining on the current thread.

Upvotes: 0

Related Questions