0

In Winforms/WPF the following code works:

var id = Thread.CurrentThread.ManagedThreadId;
await DoAsync();
var @equals = id == Thread.CurrentThread.ManagedThreadId; //TRUE

I know that await DoAsync().ConfigureAwait(false) will resume in another thread.

However, how can this WinForms/WPF behavior can be accomplished in the, say, Console App? In the console app, the above condition will return FALSE, regardless if I use ConfigureAwait(true/false). My app is not a console, it's just the same behavior. I have several classes that implements IMyInterface with a method Task<IInterface> MyMethod() and in my starting point I need to start in a STA thread, so I create an STA thread like this

     public static Task<TResult> Start<TResult>(Func<TResult> action, ApartmentState state, CancellationToken cancellation)
     {
        var completion = new TaskCompletionSource<TResult>();
        var thread = new Thread(() =>
        {
            try
            {
                completion.SetResult(action());
            }
            catch (Exception ex)
            {
                completion.SetException(ex);
            }
        });

        thread.IsBackground = true;
        thread.SetApartmentState(state);
        if (cancellation.IsCancellationRequested)
            completion.SetCanceled();
        else
            thread.Start();

        return completion.Task;
    }

So I must ensure that in every class that implements IMyInterface it resumes to the STA thread created in the beginning.

How can one accomplish that?

JobaDiniz
  • 862
  • 1
  • 14
  • 32
  • 3
    You could create your own sync context before you await. – rory.ap Apr 18 '19 at 12:39
  • if you are in an STA context, that indeed sounds like you should be creating your own sync-context that is backed by some kind of work queue and worker loop – Marc Gravell Apr 18 '19 at 12:41
  • I think your initial assumption is incorrect. "I know that await DoAsync().ConfigureAwait(false) will resume in another thread.". Does it? – Neil Apr 18 '19 at 13:12
  • @rory.ap is there some docs/articles about that? – JobaDiniz Apr 18 '19 at 13:14
  • Here, [this](https://devblogs.microsoft.com/pfxteam/await-synchronizationcontext-and-console-apps/) should help. It gets into creating your own setup about 2/3 of the way down. FYI, the author Stephen Toub is one of the leading experts on this stuff (indeed, he's a Software Engineer at Microsoft working on .NET), so you can trust whatever he says on the subject. – rory.ap Apr 18 '19 at 13:24
  • @JobaDiniz STA threads *must* pump. I recommend installing a WinForms or WPF Dispatcher loop on an STA thread. – Stephen Cleary Apr 18 '19 at 18:44

1 Answers1

2

As I mentioned in the comment above, this article is a great resource for answering this question. The author, Stephen Toub, is one of the leading experts on this stuff (indeed, he's a Software Engineer at Microsoft working on .NET), so you can trust whatever he says on the subject.

Here, I've adapted his example code to accomplish this. First, derive your own SynchronizationContext class:

private sealed class SingleThreadSynchronizationContext : SynchronizationContext
{
    private readonly BlockingCollection<KeyValuePair<SendOrPostCallback, object>> _queue =
        new BlockingCollection<KeyValuePair<SendOrPostCallback, object>>();

    public override void Post(SendOrPostCallback d, object state) 
        => _queue.Add(new KeyValuePair<SendOrPostCallback, object>(d, state));

    public void RunOnCurrentThread()
    {
        KeyValuePair<SendOrPostCallback, object> workItem;

        while (_queue.TryTake(out workItem, Timeout.Infinite))
            workItem.Key(workItem.Value);
    }

    public void Complete() => _queue.CompleteAdding();
}

Then create a specialized message pump class:

public class AsyncPump
{
    public static void Run(Func<Task> func)
    {
        var prevCtx = SynchronizationContext.Current;

        try
        {
            var syncCtx = new SingleThreadSynchronizationContext();
            SynchronizationContext.SetSynchronizationContext(syncCtx);
            var t = func();
            t.ContinueWith(delegate { syncCtx.Complete(); }, TaskScheduler.Default);
            syncCtx.RunOnCurrentThread();
            t.GetAwaiter().GetResult();
        }
        finally
        { SynchronizationContext.SetSynchronizationContext(prevCtx); }
    }
}

Then you can use it like this:

[STAThread]
private static void Main(string[] args)
{
    AsyncPump.Run(async () =>
    {
        await Task.Delay(2000);
    });

    // We're still on the Main thread!
}
rory.ap
  • 34,009
  • 10
  • 83
  • 174