9
votes

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?

1
I was going to automatically write that you've probably forgot to specify the scheduler, but all of that is in place.. strange.quetzalcoatl
Those results are strange and I get different results. I would expect 'In task' to return MTA (and it does for me) because the default implementation of SynchronizationContext uses the .NET ThreadPool. ThreadPool threads are MTA by default.Mike Zboray
@mikez: yes, that suprised me too. That's why I think that first task was run synchronously right at the moment when StartNew was issued, but I can't think of why it was at all and why later ContinueWith was not treated in the same way..quetzalcoatl

1 Answers

6
votes

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.