4

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?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
RunicMachine
  • 133
  • 5
  • Task.Delay() isn't doing any async I/O. – H H Sep 10 '17 at 04:44
  • 2
    A Console app is not a good test vehicle for GUI or ASP.NET scenarios. – H H Sep 10 '17 at 04:48
  • Look at the [dining philosophers](https://code.msdn.microsoft.com/windowsdesktop/Samples-for-Parallel-b4b76364) sample. – H H Sep 10 '17 at 04:55
  • Yes a single thrread can dispatch multiple io operations. Your code has no io operations. Your code is not really actually doing any async work. You are switching contexts for no apparent reason. Async work is not the same as multithreading. Read the difference between them. – CodingYoshi Sep 10 '17 at 05:31
  • I agree that no I/O is actually occurring. I am just modeling the behavior to keep the code succinct for a StackOverflow question. If I replace the mock async call with an actual arbitrary file I/O operation, the result is still the same; multiple threads are used for the continuations (i.e. multi-threading). – RunicMachine Sep 10 '17 at 05:55
  • 1
    Just as an aside, this [resource](http://www.albahari.com/threading/) is a fantastic crash course for all levels of threading in C# – Daniel Park Sep 10 '17 at 05:55
  • Related: [How does asynchronous programming work with threads when using Thread.Sleep()?](https://stackoverflow.com/questions/72279745/how-does-asynchronous-programming-work-with-threads-when-using-thread-sleep) – Theodor Zoulias Feb 01 '23 at 12:28

1 Answers1

3

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.

Gusman
  • 14,905
  • 2
  • 34
  • 50
  • 2
    I've marked this as the answer due to a number of good points. After researching further though, the direct answer to the question is that whether or not async/await generates a new thread is dependent on the `TaskScheduler` used by the `SynchronizationContext`. Async/await does not necessitate new threads; in a single-threaded context like a WPF app, no new threads are created in this code example. Additionally, for console apps, Stephen Cleary's async library can achieve single-threading in this code sample. e.g. `Nito.AsyncEx.AsyncContext.Run(async () => await Task.WhenAll(tasks));` – RunicMachine Sep 12 '17 at 14:16