Search code examples
c#multithreadingasync-awaitconcurrencytask-parallel-library

Concurrency without multithreading Async/Await


There is a strong emphasis that async/await is unrelated to multi-threading in most tutorials; that a single thread can dispatch multiple I/O operations and then handle the results as they complete without creating new threads. The concept makes sense but I've never seen that actual behavior in practice.

Take the below example:

static void Main(string[] args)
{
    // No Delay
    // var tasks = new List<int> { 3, 2, 1 }.Select(x => DelayedResult(x, 0));

    // Staggered delay
    // var tasks = new List<int> { 3, 2, 1 }.Select(x => DelayedResult(x, x));

    // Simultaneous Delay
    // var tasks = new List<int> { 3, 2, 1 }.Select(x => DelayedResult(x, 1));

    var allTasks = Task.WhenAll(tasks);
    allTasks.Wait();

    Console.ReadLine();
}

static async Task<T> DelayedResult<T>(T result, int seconds = 0)
{
    ThreadPrint("Yield:" + result);
    await Task.Delay(TimeSpan.FromSeconds(seconds));
    ThreadPrint("Continuation:" + result);
    return result;
}

static void ThreadPrint(string message)
{
    int threadId = Thread.CurrentThread.ManagedThreadId;
    Console.WriteLine("Thread:" + threadId + "|" + message);
}

"No Delay" uses only one thread and executes the continuation immediately as though it were synchronous code. Looks good.

Thread:1|Yield:3
Thread:1|Continuation:3
Thread:1|Yield:2
Thread:1|Continuation:2
Thread:1|Yield:1
Thread:1|Continuation:1

"Staggered Delay" uses two threads. We have left the single-threaded world behind and there are absolutely new threads being created in the thread pool. At least the thread used for processing the continuations is reused and processing occurs in the order completed rather than the order invoked.

Thread:1|Yield:3
Thread:1|Yield:2
Thread:1|Yield:1
Thread:4|Continuation:1
Thread:4|Continuation:2
Thread:4|Continuation:3

"Simultaneous Delay" uses...4 threads! This is no better than regular old multi-threading; in fact, its worse since there is an ugly state machine hiding under the covers in the IL.

Thread:1|Yield:3
Thread:1|Yield:2
Thread:1|Yield:1
Thread:4|Continuation:1
Thread:7|Continuation:3
Thread:5|Continuation:2

Please provide a code example for the "Simultaneous Delay" that only uses one thread. I suspect there isn't one...which begs the question of why the async/await pattern is advertised as unrelated to multi-threading when it clearly either a) uses the ThreadPool and dispatches new threads as necessary or b) in a UI or ASP.NET context, simply deadlocks on a single thread unless you await "all the way up" which just means that the magic additional thread is being handled by the framework (not that it does not exist).

IMHO, async/await is an awesome abstraction for using continuations everywhere for high availability without getting mired in callback hell...but let's not pretend we are somehow dodging multi-threading. What am I missing?


Solution

  • You are forcing the multithreading in the code you posted.

    When you await Task.Delay the current thread is freed to acomplish other tasks if the task scheduler decides it must be run asynchronously, in this case after it's released from the three tasks you lock that thread with Task.WhenAll.Wait which is a synchronous function.

    Also, when the task scheduler finds the Task.Delay on the tasks it decides the task is going to be long running so it must be executed asynchronously, not synchronously like the No delay case (yes, you also await Task.Delay on the No delay case, but a delay of 0 seconds, the task scheduler is smart enough to distinguish this case).

    As all the tasks resume simultaneously the task scheduler finds the first thread occupied so it creates a new thread for the first task resumed, then the next task sees both threads occupied and so on.

    Basically you are asking something impossible to the async mechanism, you want the methods to be executed in parallel while being executed in one thread.

    Also, async is not announced as unrelated to multithreading, if someone says that then he doesn't understand what async is, in fact, asynchronous implies multithreading but the async mechanism on .net is smart enough to complete some tasks synchronously to ensure the maximum efficiency.

    It can be announced as thread efficient as if a thread is waiting for an I/O operation per example, it can be used for other tasks without completely locking that thread doing nothing, take a TcpClient for example which uses a Socket, at the OS level the socket uses completion threads so retaining that thread doing nothing is totally inefficient, or if you want to go more low level, take a disk read/write which uses DMA to transfer data without using the processor, in that case no other thread is needed at all and retaining the thread is a waste of resources.

    Just as a fact, take this description from Microsoft when they introduced async:

    Visual Studio 2012 introduces a simplified approach, async programming, that leverages asynchronous support in the .NET Framework 4.5 and the Windows Runtime. The compiler does the difficult work that the developer used to do, and your application retains a logical structure that resembles synchronous code. As a result, you get all the advantages of asynchronous programming with a fraction of the effort.

    Also, using async on an UI thread does not lock the thread, that's the benefit, the UI thread will be freed and keep the UI responsive when it's waiting for long tasks, and instead of programming manually the multithreading and synchronization functions the async mechanism takes care of everything for you.