59

I've been considering the new async stuff in C# 5, and one particular question came up.

I understand that the await keyword is a neat compiler trick/syntactic sugar to implement continuation passing, where the remainder of the method is broken up into Task objects and queued-up to be run in order, but where control is returned to the calling method.

My problem is that I've heard that currently this is all on a single thread. Does this mean that this async stuff is really just a way of turning continuation code into Task objects and then calling Application.DoEvents() after each task completes before starting the next one?

Or am I missing something? (This part of the question is rhetorical - I'm fully aware I'm missing something :) )

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Neil Barnwell
  • 41,080
  • 29
  • 148
  • 220
  • 1
    Where did you hear of this running on a single thread? `Task`, as used in the Task Parallel Library, uses the threadpool to create multiple threads as appropriate. – Jeremy McGee Oct 05 '11 at 14:43
  • 2
    It's not _necessarily_ on one thread, nor is it _necessarily_ on multiple threads. You can't guarantee which thread a continuation may execute on. – Polynomial Oct 05 '11 at 14:44
  • 1
    From a speaker at a usergroup. Also, while not a categoric statement, I remember Eric Lippert making reference to it being possible on a single thread in this article: http://msdn.microsoft.com/en-gb/magazine/hh456401.aspx. The idea being that you don't get thread starvation, and can start to write responsive apps on devices with less power than PCs have these days (think of the variety of devices Windows 8 is being aimed at). – Neil Barnwell Oct 05 '11 at 14:46
  • 2
    @NeilBarnwell - If it's the same presentation I saw, they meant that the async/await pattern doesn't explicitly fire off a thread to handle it. The continuation may occur on the same thread, it may not. If I understand it correctly, it may even be handled by a (somewhat) arbitrary existing thread. – Polynomial Oct 05 '11 at 14:49

5 Answers5

61

It is concurrent, in the sense that many outstanding asychronous operations may be in progress at any time. It may or may not be multithreaded.

By default, await will schedule the continuation back to the "current execution context". The "current execution context" is defined as SynchronizationContext.Current if it is non-null, or TaskScheduler.Current if there's no SynchronizationContext.

You can override this default behavior by calling ConfigureAwait and passing false for the continueOnCapturedContext parameter. In that case, the continuation will not be scheduled back to that execution context. This usually means it will be run on a threadpool thread.

Unless you're writing library code, the default behavior is exactly what's desired. WinForms, WPF, and Silverlight (i.e., all the UI frameworks) supply a SynchronizationContext, so the continuation executes on the UI thread (and can safely access UI objects). ASP.NET also supplies a SynchronizationContext that ensures the continuation executes in the correct request context.

Other threads (including threadpool threads, Thread, and BackgroundWorker) do not supply a SynchronizationContext. So Console apps and Win32 services by default do not have a SynchronizationContext at all. In this situation, continuations execute on threadpool threads. This is why Console app demos using await/async include a call to Console.ReadLine/ReadKey or do a blocking Wait on a Task.

If you find yourself needing a SynchronizationContext, you can use AsyncContext from my Nito.AsyncEx library; it basically just provides an async-compatible "main loop" with a SynchronizationContext. I find it useful for Console apps and unit tests (VS2012 now has built-in support for async Task unit tests).

For more information about SynchronizationContext, see my Feb MSDN article.

At no time is DoEvents or an equivalent called; rather, control flow returns all the way out, and the continuation (the rest of the function) is scheduled to be run later. This is a much cleaner solution because it doesn't cause reentrancy issues like you would have if DoEvents was used.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    If allowed to suggest a refinement - the new async/await features allow for very expressive ways to do concurrent programming, however it still requires for explicit forks and joins. But in my opinion, forks and joins are much more elegant with async/await. – Theo Yaung Jan 07 '12 at 21:35
  • excellent answer. should be published in more places then stack over flow. – Stav Alfi Oct 10 '15 at 01:37
  • 1
    There are dozens of articles on async/await. But this is the first that I have come across that mentions the SyncCtx. Without that, the rest does not really make sense. As to why the readline is needed for main() that is unclear, why would it cause the continuations to be called? – Tuntable Feb 19 '21 at 10:53
  • @Tuntable: It doesn't *cause* the continuations to be called. All it does is prevent the application from exiting until the continuations have a chance to run. Also, since this was written, .NET Core has come out with support for `async Task Main`, which means you can use `await` in `Main` and so blocking on the task (`Wait`) or waiting for the user to exit (`ReadLine`) is no longer necessary. *If* you're on .NET Core. – Stephen Cleary Feb 19 '21 at 13:11
  • @StephenCleary Thankyou. But does that mean an awaited main runs the continuations in the same or a different thread? That is a critical issue is rarely mentioned and confusing (I suppose it is only critical if you care about race conditions and writing code that is actually correct). The whole point of await is to run in the same thread for UI work. If you have an async main, can you "DoEvents" to run outstanding continuations without stalling? – Tuntable Feb 20 '21 at 22:52
  • @Tuntable: I explain the scheduling of `await` continuations [on my blog](https://blog.stephencleary.com/2012/02/async-and-await.html). Console apps do not have a `SynchronizationContext`, so continuations run on thread pool threads. – Stephen Cleary Feb 21 '21 at 00:37
  • @Stephen Cleary "WPF... continuation executes on the UI thread (and can safely access UI objects)" - Does it mean there is no need to call "Dispatcher.Invoke(...)" to update the ui from an async method in a WPF application? – The incredible Jan Sep 19 '22 at 09:20
  • @TheincredibleJan: That is correct. I never use `Invoke` (or `InvokeRequired`). – Stephen Cleary Sep 19 '22 at 12:10
5

The whole idea behind async/await is that it performs continuation passing nicely, and doesn't allocate a new thread for the operation. The continuation may occur on a new thread, it may continue on the same thread.

Polynomial
  • 27,674
  • 12
  • 80
  • 107
  • 5
    Ahh okay. I think this is one of the most opaque black-boxes of functionality I've seen in a long time. It's one of those things where you can't cheat - you really do need to understand quite well *how* it works, in order to use it (or in my case, even make sense of it). – Neil Barnwell Oct 05 '11 at 14:49
  • 2
    Does it therefore have optimisation to ensure that calls from a UI thread are marshalled back to that thread for the purpose of returning values or are all bets off once you use the `async` keyword? – Neil Barnwell Oct 05 '11 at 14:49
  • Not that you'd _ever_ want to use something you don't fully understand, of course ;) – Polynomial Oct 05 '11 at 14:50
  • I'd imagine that any async stuff on UI threads is either returned to a UI thread by rule, or modified to return in a manner that at least automatically performs delegation onto the UI thread, though I base that on absolutely nothing. – Polynomial Oct 05 '11 at 14:51
  • 2
    Err no, sure... Seriously though, some things allow you to get away with it, for a time, at least. I bet there are lots of C# developers that don't know about string interning or why `ReaderWriterLockSlim` is better than `Monitor` in certain situations. Many more know how to drive their car but can't fix it if it breaks. I happen to think this is something where you can't really even make use of it until you grok it first. – Neil Barnwell Oct 05 '11 at 14:52
  • Apologies if it came across as derisive, I was just making a joke. I totally get what you're saying with `async`, it is pretty opaque, even with a strong background in multi-threaded programming. – Polynomial Oct 05 '11 at 14:54
  • Lol. I was continuing your joke with the "errr, no, sure...". I probably should have included a smiley face to make it obvious. Here it is: :) – Neil Barnwell Oct 05 '11 at 15:09
2

The real "meat" (the asynchronous) part of async/await is normally done separately and the communication to the caller is done through TaskCompletionSource. As written here http://blogs.msdn.com/b/pfxteam/archive/2009/06/02/9685804.aspx

The TaskCompletionSource type serves two related purposes, both alluded to by its name: it is a source for creating a task, and the source for that task’s completion. In essence, a TaskCompletionSource acts as the producer for a Task and its completion.

and the example is quite clear:

public static Task<T> RunAsync<T>(Func<T> function) 
{ 
    if (function == null) throw new ArgumentNullException(“function”); 
    var tcs = new TaskCompletionSource<T>(); 
    ThreadPool.QueueUserWorkItem(_ => 
    { 
        try 
        {  
            T result = function(); 
            tcs.SetResult(result);  
        } 
        catch(Exception exc) { tcs.SetException(exc); } 
    }); 
    return tcs.Task; 
}

Through the TaskCompletionSource you have access to a Task object that you can await, but it isn't through the async/await keywords that you created the multithreading.

Note that when many "slow" functions will be converted to the async/await syntax, you won't need to use TaskCompletionSource very much. They'll use it internally (but in the end somewhere there must be a TaskCompletionSource to have an asynchronous result)

xanatos
  • 109,618
  • 12
  • 197
  • 280
1

I feel like this question needs a simpler answer for people. So I'm going to oversimplify.

The fact is, if you save the Tasks and don't await them, then async/await is "concurrent".

var a = await LongTask1(x);
var b = await LongTask2(y);

var c = ShortTask(a, b);

is not concurrent. LongTask1 will complete before LongTask2 starts.

var a = LongTask1(x);
var b = LongTask2(y);

var c = ShortTask(await a, await b);

is concurrent.

While I also urge people to get a deeper understanding and read up on this, you can use async/await for concurrency, and it's pretty simple.

PRMan
  • 535
  • 1
  • 8
  • 16
  • 1
    I'm not sure this makes as much difference as it seems, does it? Ultimately, the `await a` and `await b` will be executed synchronously because they are args for `ShortTask`, and there are no additional instructions between the call to `LongTask2` and `ShortTask`. – Neil Barnwell Mar 03 '20 at 09:04
  • @NeilBarnwell the `await a, await b` fragment awaits each task sequentially. But the tasks have been already started, and how you await them is relatively unimportant. While you are awaiting the `a` task, the `b` task is running as well (concurrently). Actually there is nothing preventing the `b` task to complete before the `a`, in which case the `await b` statement will not await at all, since the result will be already there. – Theodor Zoulias Nov 27 '20 at 23:12
  • 1
    So how does that address the poster's question about threading? – Tuntable Feb 19 '21 at 10:57
1

The way I like to explain it is that the "await" keyword simply waits for a task to finish but yields execution to the calling thread while it waits. It then returns the result of the Task and continues from the statement after the "await" keyword once the Task is complete.

Some people I have noticed seem to think that the Task is run in the same thread as the calling thread, this is incorrect and can be proved by trying to alter a Windows.Forms GUI element within the method that await calls. However, the continuation is run in the calling thread where ever possible.

Its just a neat way of not having to have callback delegates or event handlers for when the Task completes.

Aaron Murgatroyd
  • 1,506
  • 19
  • 22
  • Stephen Cleary wrote: "WinForms, WPF, and Silverlight (i.e., all the UI frameworks) supply a SynchronizationContext, so the continuation executes on the UI thread (and can safely access UI objects)." – The incredible Jan Sep 19 '22 at 09:28