How to Create a Task (Tpl) Running a Sta Thread

How to create a task (TPL) running a STA thread?

You can use the TaskScheduler.FromCurrentSynchronizationContext Method to get a TaskScheduler for the current synchronization context (which is the WPF dispatcher when you're running a WPF application).

Then use the ContinueWith overload that accepts a TaskScheduler:

var scheduler = TaskScheduler.FromCurrentSynchronizationContext();

Task.Factory.StartNew(...)
.ContinueWith(r => AddControlsToGrid(r.Result), scheduler);

How to force a task to run on an STA thread?

I have WinForms application, and I want to use the Windows.Forms.OpenFileDialog, which requires an STA thread.

Yes. Every WinForms app has an STA thread which is its main thread. It's easiest to just use that thread.

It works fine with threads, but what about tasks? ... How to create a task (TPL) running a STA thread?

Apartment threading models is a thread concept. There's no such thing as an STA task.

However, a Delegate Task can run on an STA thread if it is scheduled to that thread.

I don't understand how TaskScheduler.FromCurrentSynchronizationContext() is supposed to force a task to run on a STA thread

It doesn't always do that. TaskScheduler.FromCurrentSynchronizationContext will create a TaskScheduler that schedules tasks to SynchronizationContext.Current. Now, on WinForms, if the calling code is on the UI thread, then SynchronizationContext.Current is a WindowsFormsSynchronizationContext instance. That WindowsFormsSynchronizationContext will execute code on the UI thread. So, the TaskScheduler will schedule tasks to the UI thread (which is an STA thread), and the tasks end up running on the existing STA thread. Again, that is if TaskScheduler.FromCurrentSynchronizationContext was called from the UI thread in the first place.

If TaskScheduler.FromCurrentSynchronizationContext is called from a thread pool thread, then SynchronizationContext.Current is null, and the resulting TaskScheduler schedules tasks on a thread pool thread, which is not STA.

So how do I forcibly start a task on STA apartment state, so that I can use components that require it in my application?

The optimum way to do this is to structure your code so that the UI thread calls the background threads, not the other way around. Don't have your background threads call the UI to do things. If the UI is in control, then simpler patterns like await and IProgress<T> can be used to coordinate with the background threads. If background thread(s) must drive the UI, then one solution is to capture the UI SynchronizationContext (or TaskScheduler) and use that from a background thread to execute code on the UI thread.

How do you run on the STA thread when using a Task

An example by using an 'old fashioned' thread:

    var task = new Task<bool>(IsConnectedToNetwork);
task.ContinueWith(res =>
{
bool onNetwork = res.Result;
var thread = new Thread(() =>
{
Form frm = onNetwork ? (Form)new OnNetworkForm() : new OffNetworkForm();
frm.ShowDialog();
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
});
task.Start();

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

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.

How to fix Task.Run from UI thread throwing STA error

The COM components accessed through Interop require the calling thread to be a STA thread but in your case it is not STA. Otherwise the STA component could be accessed through multiple threads. You can read more about why STA is required in Understanding and Using COM Threading Models.

You can make a extension method on Task class as suggested in Set ApartmentState on a Task to call the COM component through Interop using task:

public static Task<T> StartSTATask<T>(Func<T> func)
{
var tcs = new TaskCompletionSource<T>();
Thread thread = new Thread(() =>
{
try
{
tcs.SetResult(func());
}
catch (Exception e)
{
tcs.SetException(e);
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
return tcs.Task;
}

When you use Thread instead of task, you have to set the ApartmentState to STA using something like thread.SetApartmentState(ApartmentState.STA).

Task.StartNew() works differently on STA mode?

but now when I run the dll by clicking the button, I cannot navigate
the UI

Task != Thread. A task may or may not use a thread to do it's work.
When you use:

var scheduler = TaskScheduler.FromCurrentSynchronizationContext();

You're telling the task factory to execute the given delegate on the current captured synchronization context, which is your UI synchronization context. When you do that, it will execute this on the UI message loop.

this worked very well until I had to run this Task on STA mode to open
windows in the dll

Opening a window should be done on the UI thread, hence why it's complaining. What you can do is defer the execution of the CPU bound work the a background thread, and once you need to manipulate the UI, marshal the work back:

// Start CPU bound work on a background thread
await Task.Run(() => strTime = StartSync.DoCpuWork(strPathFile,
licManager.idCmsMediator)));

// We're done awaiting, back onto the UI thread, Update window.

Why is Task.Delay breaking the STA state of the thread?

Hans has nailed it. Technically, your code is breaking because there's no SynchronizationContext captured by the await. But even if you write one, it won't be enough.

The one big problem with this approach is that your STA thread isn't pumping. STA threads must pump a Win32 message queue, or else they're not STA threads. SetApartmentState(ApartmentState.STA) is just telling the runtime that this is an STA thread; it doesn't make it an STA thread. You have to pump messages for it to be an STA thread.

You can write that message pump yourself, though I don't know of anyone brave enough to have done this. Most people install a message pump from WinForms (a la Hans' answer) or WPF. It may also be possible to do this with a UWP message pump.

One nice side effect of using the provided message pumps is that they also provide a SynchronizationContext (e.g., WinFormsSynchronizationContext / DispatcherSynchronizationContext), so await works naturally. Also, since every .NET UI framework defines a "run this delegate" Win32 message, the underlying Win32 message queue can also contain all the work you want to queue to your thread, so the explicit queue and its "runner" code is no longer necessary.



Related Topics



Leave a reply



Submit