How do you get a working interaction with ConfigureAwait(true) in a console application?

Viewed 88

In a small project i am working on i have the neccessity for a component to execute a components shutdown code in the same thread that it was initialized in. However unlike in WPF/Winforms/Web the synchronizationcontext which takes care of this does not work.

My guess is that the lack of a synchronization context is the issue that causes the lack of utilization for ConfigureAwait(true).

Does someone know how to properly implement this?

I read this article but could not make any sense of it yet. Perhaps it was too late yesterday.

Minimal Repro:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleSyncContext
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}");
            await SomeBackgroundWorkAsync();
            // if this is the same thread as above the question is solved.
            Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}");
        }

        private static async Task SomeBackgroundWorkAsync()
        {
            await Task.Run(() => { });
        }
    }
}

2 Answers

As you already figured out, console application by default doesn't have synchronization context, so ConfigureAwait has no effect, and continuation after your await SomePageLoad() will run on random thread pool thread. Note that using async main method is essentially equivalent to this:

static async Task AsyncMain() { ... } // your `async Task Main method`

// real Main method generated by compiler
static void RealMain() {
    AsyncMain().GetAwaiter().GetResult(); 
}

In your case you don't need any synchronization context though. What you want is initialize CefSharp on main thread and shutdown CefSharp on main thread. So instead of using async Main - you can do the same as above, but initialize and shutdown Cef outside of async method:

static void Main(string[] args) {
    // starting with thread 1
    Cef.Initialize(new CefSettings());
    try {
        AsyncMain(args).GetAwaiter().GetResult();
    }
    finally {
        // we are on main thread here
        Cef.Shutdown();
    }

}

static async Task AsyncMain(string[] args) {
    await SomePageLoad(); // more stuff here
}

Edit: if you insist on using synchronization context then it can be done, but will add a lot of complications for nothing. Out goal is create synchronization context which will run all actions on the same thread. This case be done with simple actions queue, here is basic implementation (don't use it in production, provided as an example only, no exception handling and so on):

class CustomSyncContext : SynchronizationContext {
    private readonly BlockingCollection<WorkItem> _queue = new BlockingCollection<WorkItem>(new ConcurrentQueue<WorkItem>());
    private readonly Thread _thread;
    public CustomSyncContext() {
        // start new thread which will handle all callbacks
        _thread = new Thread(() => {
            // set outselves as current sync context for this thread
            SynchronizationContext.SetSynchronizationContext(this);
            foreach (var item in _queue.GetConsumingEnumerable()) {
                try {
                    // execute action
                    item.Action();
                }
                finally {
                    // if this action is synchronous, signal the caller
                    item.Signal?.Set();
                }
            }
        });
        _thread.Start();
    }
    public override void Post(SendOrPostCallback d, object state) {
        // Post means acion is asynchronous, just queue and forget
        _queue.Add(new WorkItem(() => d(state), null));
    }

    public override void Send(SendOrPostCallback d, object state) {
        // Send means action is synchronous, wait on a single until our thread executes it
        using (var signal = new ManualResetEvent(false)) {
            _queue.Add(new WorkItem(() => d(state), signal));
            signal.WaitOne();
        }
    }

    public void Shutdown() {
        // signal thread that no more callbacks are expected
        _queue.CompleteAdding();
    }

    public void WaitForShutdown() {
        _thread.Join();
    }

    private class WorkItem {
        public WorkItem(Action action, ManualResetEvent signal) {
            Action = action;
            Signal = signal;
        }
        public Action Action { get; }
        public ManualResetEvent Signal { get; }
    }
}

And your code then becomes:

var ctx = new CustomSyncContext();
ctx.Send(async (_) => {
    try {
        // starting with thread 1
        Cef.Initialize(new CefSettings());

        // this method returns on thread 4
        await SomePageLoad();
    }
    finally {
        Cef.Shutdown();
        // signal the context we are done, so that main thread can unblock
        ctx.Shutdown();
        Console.WriteLine("done");
    }
}, null);

ctx.WaitForShutdown();

Now your code runs on custom synchronization context, and continuation after await SomePageLoad(); will be posted to that synchronization context and executed by our thread (the same thread which inited CefSharp) (no ConfigureAwait(true) is needed, as it's already true by default). Note that we achieved nothing useful - we have one more thread, and our main thread is still blocked waiting for the whole operation to complete (there is no sensible way around that).

Edit 2: here is variation which does not require separate thread, but is not much better:

class CustomSyncContext : SynchronizationContext {
    private readonly BlockingCollection<WorkItem> _queue = new BlockingCollection<WorkItem>(new ConcurrentQueue<WorkItem>());
    public override void Post(SendOrPostCallback d, object state) {
        // Post means acion is asynchronous, just queue and forget
        _queue.Add(new WorkItem(() => d(state), null));
    }

    public override void Send(SendOrPostCallback d, object state) {
        // Send means action is synchronous, wait on a single until our thread executes it
        using (var signal = new ManualResetEvent(false)) {
            _queue.Add(new WorkItem(() => d(state), signal));
            signal.WaitOne();
        }
    }

    public void Shutdown() {
        // signal thread that no more callbacks are expected
        _queue.CompleteAdding();
    }

    public void Start() {
        // now we run the loop on main thread
        foreach (var item in _queue.GetConsumingEnumerable()) {
            try {
                // execute action
                item.Action();
            }
            finally {
                // if this action is synchronous, signal the caller
                item.Signal?.Set();
            }
        }
    }

    private class WorkItem {
        public WorkItem(Action action, ManualResetEvent signal) {
            Action = action;
            Signal = signal;
        }
        public Action Action { get; }
        public ManualResetEvent Signal { get; }
    }
}

static async Task Main(string[] args) {
    var ctx = new CustomSyncContext();
    // set sync context
    SynchronizationContext.SetSynchronizationContext(ctx);
    // now execute our async stuff
    var task = DoStuff().ContinueWith(x => ctx.Shutdown());
    // now run the loop of sync context on the main thread.
    // but, how do we know when to stop? Something from outside should singal that
    // in the case signal is completion of DoStuff task
    // note that most of the time main thread is still blocked while waiting for items in queue
    ctx.Start();
}

private static async Task DoStuff() {
    try {
        // starting with thread 1
        Cef.Initialize(new CefSettings());

        // this method returns on thread 4
        await SomePageLoad();
    }
    finally {
        Cef.Shutdown();
        // signal the context we are done, so that main thread can unblock
        Console.WriteLine("done");
    }
}

Your problem is indeed the lack of a Synchronisation context. You can't use ConfigureAwait(true) as this implies that you need to return to the original scheduler/context which does not exist.

Custom implementation

A very simple implementation that ought to do the trick is the one found here. Basically two steps.

  1. Implement a custom synchronization context
public class CustomSynchronizationContext : SynchronizationContext
{
    public override void Post(SendOrPostCallback action, object state)
    {
        SendOrPostCallback actionWrap = (object state2) =>
        {
            SynchronizationContext.SetSynchronizationContext(new CustomSynchronizationContext());
            action.Invoke(state2);
        };
        var callback = new WaitCallback(actionWrap.Invoke);
        ThreadPool.QueueUserWorkItem(callback, state);
    }
    public override SynchronizationContext CreateCopy()
    {
        return new CustomSynchronizationContext();
    }
    public override void Send(SendOrPostCallback d, object state)
    {
        base.Send(d, state);
    }
    public override void OperationStarted()
    {
        base.OperationStarted();
    }
    public override void OperationCompleted()
    {
        base.OperationCompleted();
    }
}
  1. Initaliaze it and use it
static void Main()
    {
        var context = new CustomSynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(context);

AsyncEx library

You can also use the AsyncEx library

    private static void Main(string[] args)
    {
        AsyncContext.Run(() => AsyncMethod(args));
    }
 
    static async void AsyncMethod(string[] args)
    {
        await something().ConfigureAwait(true);
    }
Related