Search code examples
c#async-awaitconcurrencytask

Single-threaded async/await in C# console app


By default in a console application, async Tasks will run on the ThreadPool, which means that multiple tasks can run in parallel on different threads (and will, if you have a multi core processor).

How can I disallow Tasks from running in parallel in my console application (without using a mutex; I don't want to have to deal with fairness issues and remembering to lock the mutex everywhere)? I'd like only one concurrent task to run at one time (i.e. I'd like the tasks to be concurrent but not parallel).

I've tried setting the max threads in the ThreadPool to 1, but the ThreadPool cannot be run with fewer threads than the CPU has cores, so that doesn't work.


Solution

  • You can install a single-thread synchronization context on a Console application, using the static AsyncContext class found in the AsyncEx NuGet package by Stephen Cleary:

    AsyncContext.Run(async () =>
    {
        await Foo();
        await Task.WhenAll(items.Select(async item =>
        {
            var result = await Bar(item);
            DoStuff(result);
        }));
        await FooBar();
    });
    

    The AsyncContext.Run is a blocking call, similar to the Application.Run method. It installs a special AsyncContextSynchronizationContext on the current thread, much like the Application.Run installs a WindowsFormsSynchronizationContext on the UI thread. As long as all awaits inside the AsyncContext.Run delegate have the default configuration, meaning no ConfigureAwait(false) is attached to them, all continuations after each and every await will run on the main thread of the console application. Your code will be single-threaded. Any built-in asynchronous methods might still use ThreadPool threads under the hood to do their work, but your code won't observe these other threads, and won't be affected by them.

    The AsyncContext.Run completes when the asynchronous action delegate completes, along with any async void methods that might have been invoked inside the delegate. On the contrary any pending fire-and-forget tasks don't prevent the AsyncContext.Run from completing, and are left behind in a forever-incomplete state. After the completion of the AsyncContext.Run, the AsyncContextSynchronizationContext is uninstalled. The SynchronizationContext.Current reverts to its original state.

    Be careful not to use .Wait() or .Result inside the delegate, otherwise your program will deadlock. If you block the one and only thread that executes your code, your program will grind to a halt.