Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tasks - how to ensure the ContinueWith action is an STA thread?

I am trying to use tasks in a little .net 4.0 application (written using Visual Studio 2010 if that matters) that needs to work on Windows 2003 and use a WriteableBitmap with the palette parameter.

The code using said class must, therefore, be running as an STA thread to avoid it throwing an invalid cast exception (see here for why I need an STA thread if you are interested, but it is not the thrust of my question).

I, therefore, checked on Stack overflow and came across How to create a task (TPL) running a STA thread? and The current SynchronizationContext may not be used as a TaskScheduler - perfect, so now I know what to do, except...

Here's a little console application:

using System;
using System.Threading;
using System.Threading.Tasks;
namespace TaskPlayingConsoleApplication
{
    class Program
    {
        [STAThread]
        static void Main()
        {
            Console.WriteLine("Before Anything: " 
                + Thread.CurrentThread.GetApartmentState());

            SynchronizationContext.SetSynchronizationContext(
                new SynchronizationContext());
            var cts = new CancellationTokenSource();
            var scheduler = TaskScheduler.FromCurrentSynchronizationContext();

            var task = Task.Factory.StartNew(
                () => Console.WriteLine(
                    "In task: " + Thread.CurrentThread.GetApartmentState()),
                cts.Token,
                TaskCreationOptions.None,
                scheduler);

            task.ContinueWith(t =>
                 Console.WriteLine(
                   "In continue: " + Thread.CurrentThread.GetApartmentState()),
                   scheduler);

            task.Wait();
        }
    }
}

And here is its output:

Before Anything: STA 
In task: STA 
In continue: MTA

What the!?! Yup, it is back to an MTA thread on the Action<Task> passed into the ContinueWith method.

I am passing the same scheduler into the task and the continue but somehow in the continue it seems to be being ignored.

I'm sure it is something stupid, so how would I make sure that my callback passed into the ContinueWith uses an STA thread?

like image 736
kmp Avatar asked Dec 20 '13 07:12

kmp


People also ask

What is Task ContinueWith?

Creates a continuation that executes asynchronously when the target Task completes.

How does ContinueWith work?

The ContinueWith function is a method available on the task that allows executing code after the task has finished execution. In simple words it allows continuation. Things to note here is that ContinueWith also returns one Task. That means you can attach ContinueWith one task returned by this method.

What is a thread task?

Differences Between Task And ThreadThe Thread class is used for creating and manipulating a thread in Windows. A Task represents some asynchronous operation and is part of the Task Parallel Library, a set of APIs for running tasks asynchronously and in parallel. The task can return a result.


1 Answers

EDIT: before you read any of the following, here's an excellent on-topic article: http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx ; You can skip my post and go directly there!

Most important part describing the root cause:

The default implementation of SynchronizationContext.Post just turns around and passes it off to the ThreadPool via QueueUserWorkItem. But (...) can derive their own context from SynchronizationContext and override the Post method to be more appropriate to the scheduler being represented.

In the case of Windows Forms, for example, the WindowsFormsSynchronizationContext implements Post to pass the delegate off to Control.BeginInvoke. For DispatcherSynchronizationContext in WPF, it calls to Dispatcher.BeginInvoke. And so on.

So, you need to use something other than the base SynchronizationContext class. Try using any of the other existing ones, or create your own. Example is included in the article.


And now, my original response:

After thinking a bit, I think the problem is that in your console application there is no thing like "message pump". The default SynchronizationContext is just a piece of lock. It prevents threads from intersecting on a resource, but it does not provide any queueing or thread selection. In general you are meant to subclass the SynchroContext to provide your own way of proper synchronization. Both WPF and WinForms provide their own subtypes.

When you Wait on your task, most probably the MainThread gets blocked and all other are run on some random threads from the default threadpool.

Please try writing Thread IDs to the console along with the STA/MTA flag.

You will probably see:

STA: 1111
STA: 1111
MTA: 1234

If you see this, then most probably your first task is run synchronously on the calling thread and gets instantly finished, then you try to "continue" it's just 'appended' to 'the queue', but it is not started immediatelly (guessing, I dont know why so; the old task is finished, so ContinueWith could also just run it synchronously). Then main thread gets locked on wait, and since there's no message pump - it cannot switch to another job and sleeps. Then threadpool waits and sweps the lingering continuation task. Just guessing though. You could try to check this by

prepare synccontext
write "starting task1"
start task1 ( -> write "task1")
write "continuing task2"         <--- add this one 
continue: task2 ( -> write "task2")
wait

and check the order of messages in the log. Is "continuing" before "hello" from task1 or not?

You may also try seeing what happens if you don't create the Task1 by StartNew, but rather create it as prepared/suspended, then Continue, then start, then wait. If I'm right about the synchronous run, then in such setup main and continuation task will either both be run on the calling '1111' STA thread, or both on threadpool's '2222' thread.

Again, if all of these is right, the providing some message pump and proper SyncContext type will probably solve your issue. As I said, both WPF and WinForms provide their own subtypes. Although I don't remember the names now, you can try using them. If I remember correctly, the WPF starts its dispatcher automatically and you don't need any extra setup. I don't remember how's with WinForms. But, with the WPF's auto-start, if your ConsoleApp is actually some kind of a unit-test that will run many separate cases, you will need to shutdown the WPF's dispatcher before the cases.. but that's far from the topic now.

like image 185
quetzalcoatl Avatar answered Nov 15 '22 17:11

quetzalcoatl