Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to execute nested async/await code in parallel while maintaining the same thread on await continuations?

This might be the worst StackOverflow title I've ever written. What I'm actually trying to do is execute an asynchronous method that uses the async/await convention (and itself contains additional await calls) from within a synchronous method multiple times in parallel while maintaining the same thread throughout the execution of each branch of the parallel execution, including for all await continuations. To put it another way, I want to execute some async code synchronously, but I want to do it multiple times in parallel. Now you can see why the title was so bad. Perhaps this is best illustrated with some code...

Assume I have the following:

public class MyAsyncCode
{
    async Task MethodA()
    {
        // Do some stuff...
        await MethodB();
        // Some other stuff
    }

    async Task MethodB()
    {
        // Do some stuff...
        await MethodC();
        // Some other stuff
    }

    async Task MethodC()
    {
        // Do some stuff...
    }
}

The caller is synchronous (from a console application). Let me try illustrating what I'm trying to do with an attempt to use Task.WaitAll(...) and wrapper tasks:

public void MyCallingMethod()
{
    List<Task> tasks = new List<Task>();
    for(int c = 0 ; c < 4 ; c++)
    {
        MyAsyncCode asyncCode = new MyAsyncCode();
        tasks.Add(Task.Run(() => asyncCode.MethodA()));
    }
    Task.WaitAll(tasks.ToArray());
}

The desired behavior is for MethodA, MethodB, and MethodC to all be run on the same thread, both before and after the continuation, and for this to happen 4 times in parallel on 4 different threads. To put it yet another way, I want to remove the asynchronous behavior of my await calls since I'm making the calls parallel from the caller.

Now, before I go any further, I do understand that there's a difference between asynchronous code and parallel/multi-threaded code and that the former doesn't imply or suggest the latter. I'm also aware the easiest way to achieve this behavior is to remove the async/await declarations. Unfortunately, I don't have the option to do this (it's in a library) and there are reasons why I need the continuations to all be on the same thread (having to do with poor design of said library). But even more than that, this has piqued my interest and now I want to know from an academic perspective.

I've attempted to run this using PLINQ and immediate task execution with .AsParallel().Select(x => x.MethodA().Result). I've also attempted to use the AsyncHelper class found here and there, which really just uses .Unwrap().GetAwaiter().GetResult(). I've also tried some other stuff and I can't seem to get the desired behavior. I either end up with all the calls on the same thread (which obviously isn't parallel) or end up with the continuations executing on different threads.

Is what I'm trying to do even possible, or are async/await and the TPL just too different (despite both being based on Tasks)?

like image 632
daveaglick Avatar asked Aug 12 '15 20:08

daveaglick


1 Answers

The methods that you are calling do not use ConfigureAwait(false). This means that we can force the continuations to resume in a context we like. Options:

  1. Install a single-threaded synchronization context. I believe Nito.Async has that.
  2. Use a custom TaskScheduler. await looks at TaskScheduler.Current and resumes at that scheduler if it is non-default.

I'm not sure if there are any pros and cons for either option. Option 2 has easier scoping I think. Option 2 would look like:

Task.Factory.StartNew(
    () => MethodA()
    , new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler).Unwrap();

Call this once for each parallel invocation and use Task.WaitAll to join all those tasks. Probably you should dispose of that scheduler as well.

I'm (ab)using ConcurrentExclusiveSchedulerPair here to get a single-threaded scheduler.

If those methods are not particularly CPU-intensive you can just use the same scheduler/thread for all of them.

like image 51
usr Avatar answered Oct 14 '22 17:10

usr