66

I have a method like this:

public static async Task SaveAllAsync()
{
    foreach (var kvp in configurationFileMap)
    {
        using (XmlWriter xmlWriter = XmlWriter.Create(kvp.Value, XML_WRITER_SETTINGS))
        {
            FieldInfo[] allPublicFields = 
                           kvp.Key.GetFields(BindingFlags.Public | BindingFlags.Static);
            await xmlWriter.WriteStartDocumentAsync();
            foreach (FieldInfo fi in allPublicFields)
            {
                await xmlWriter.WriteStartElementAsync("some", "text", "here");
            }
            await xmlWriter.WriteEndDocumentAsync();
        }
    }
}

But I'm struggling to follow what will happen when someone calls SaveAllAsync().

What I think will happen is this:

  1. When someone first calls it, SaveAllAsync() will return control to the caller at the line await xmlWriter.WriteStartDocumentAsync();
  2. Then... When they await SaveAllAsync() (or wait for the task)... What happens? Will SaveAllAsync() still be stuck on the first await until that is called? Because there's no threading involved, I guess that is the case...
Kirill Kobelev
  • 10,252
  • 6
  • 30
  • 51
Xenoprimate
  • 7,691
  • 15
  • 58
  • 95
  • 2
    Have you tried writing a simple program which simulates this behavior and checked the result? – default Aug 26 '13 at 14:12
  • 1
    @Default It's actually rather hard to write an effective simulator to show you what's going on unless you *already* know what it's doing. That said, there are a million books/blogs/articles/etc. out there on the subject, which will give you a basic idea of what's going on here. – Servy Aug 26 '13 at 14:14
  • @Servy the question was about a method with several awaits. Writing a program which awaits methods with `Task.Delay`s in them and outputting some debugmessages isn't very complicated.. unless I'm missing something. – default Aug 26 '13 at 14:22
  • @Default No, that's not complicated, but being able to determine what's really going on under the covers as a result of those printed messages *is*. It's not immediately obvious from the obvious observed behaviors what's really going on, at least not unless you have a basic idea of how to start. – Servy Aug 26 '13 at 14:24
  • Somebody please check me on this, but this example doesn't seem like a problem that's solved with async coding? If one awaits a call to an async method isn't that the same (in terms of order of execution) as a simple synchronous call? Doesn't the calling thread block even though the work is being done by the async thread? – William T. Mallard Jan 19 '17 at 23:34
  • The possibly more interesting/enlightening question is "what would happen WITHOUT the second and third awaits?" As `WriteStartElementAsync` and `WriteEndDocumentAsync` must be async methods themselves, without the other two await statements `SaveAllAsync`'s result object would unblock at the calling site BEFORE `WriteStartElementAsync` and `WriteEndDocumentAsync` had finished. Without those statements, the caller of `SaveAllAsync` would be able to examine the result object while the xml is still being written to. Is that bad? Uh ... It depends..!? : ) – Grimm The Opiner Apr 23 '18 at 15:39

4 Answers4

64

You can think of await as "pausing" the async method until that operation is complete. As a special case, if the operation is already completed (or is extremely fast), then the await will not "pause" the method; it will continue executing immediately.

So in this case (assuming that WriteStartDocumentAsync is not already completed), await will pause the method and return an uncompleted task to the caller. Note that the Task returned by an async method represents that method; when the method completes, then that Task is completed.

Eventually, WriteStartDocumentAsync will complete, and that will schedule the rest of the async method to continue running. In this case, it'll execute the next part of the method until the next await, when it gets paused again, etc. Eventually, the async method will complete, which will complete the Task that was returned to represent that method.

For more information, I have an async/await intro on my blog.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • So, in the meantime, when different parts of the called method ( some awaited and some not ) are executing, the caller ( as long as it has awaited the callee ) can **actually return control to *its* caller**. ...which is good, because the the upper level code can maintain control ( and check to see if there is any reason to abort the thing altogether ), but even though control is returned, **the awaited methods are guaranteed to get some time to do their tasks** – Andyz Smith Aug 26 '13 at 14:38
  • So additionally, if a method with multiple awaits is called by a caller, the responsibility for finishing every statement of that method is with the caller. So even if the callee has multiple awaits, the caller is ultimately responsible for completing every ststement, and will ask the .Net threading infrastructure to keep going into this method, trying to get something done, maybe getting awaited again and again, but the caller is still responsible for executing code **after the awaited sections in the calee**. – Andyz Smith Aug 26 '13 at 14:50
  • 2
    @AndyzSmith `"the awaited methods are guaranteed to get some time to do their tasks"` No, they are not. It's possible for some code somewhere to refuse to give up control and perform a blocking wait, thus preventing these continuations from running. That's why it's important, when using async code, to "async all the way up". If someone, somewhere, is doing a blocking wait on something else, and that something else can't run because the `SynchronizationContext` is busy doing a blocking wait, you have hit a "Deadlock". – Servy Aug 26 '13 at 14:52
  • Isn't any kind of UI thread a blocking routine? It keeps running indefinitely, doesn't ever call the awaited methods again, and consumes infinite cycles, forever if left alone. So what you are saying, is that some badly behaved code can actually preempt the preemptive multitasking provided by the SynchronizationContext? – Andyz Smith Aug 26 '13 at 14:56
  • 2
    @AndyzSmith: @Servy is correct. Also, it's incorrect to say the "caller is ultimately responsible for completing every statement". What you're probably thinking of is the situation where you have a UI thread context, and the `async` method resumes on the UI thread. `async` and `await` themselves are thread-agnostic, and work just fine (with different threading semantics) in Console apps, ASP.NET, etc. – Stephen Cleary Aug 26 '13 at 14:56
  • @AndyzSmith You most certainly should not be calling a method that will run indefinetly, from a UI synchronization context, if you're calling code that is using `await`, as you will cause a deadlock. Most UI routines *don't* do that though. Most run for a finite amount of time, and they may prevent continuations from running for a while, but since they aren't *waiting* on those continuations, they will eventually finish and let them continue executing at some point. – Servy Aug 26 '13 at 15:01
  • Saying the UI routine runs for a finite amount of time, means you can resolve all these multi tasking issues by just closing the program. I't not quite getting that. – Andyz Smith Aug 26 '13 at 15:06
28

Stephens answer is of course correct. Here's another way to think about it that might help.

The continuation of a hunk of code is what happens after the code completes. When you hit an await two things happen. First, the current position in the execution becomes the continuation of the awaited task. Second, control leaves the current method and some other code runs. The other code is maybe the continuation of the first call, or maybe is something else entirely, an event handler, say.

But when the call to xmlWriter.WriteStartDocumentAsync() has completed; what happens? Is the current execution interrupted and returned back to SaveAllAsync()?

It is not clear what you mean by the call "completing". WriteStartDocumentAsync starts an asynchronous write, probably on an I/O completion thread, and returns you a Task that represents that asynchronous work. Awaiting that task does two things, like I said. First, the continuation of this task becomes the current position of the code. Second, control leaves the current method and some other code runs. In this case, whatever code called SaveAllAsync runs the continuation of that call.

Now lets suppose that code -- the caller of SaveAllAsync continues to run, and lets suppose further that you are in an application with a UI thread, like a Windows Forms application or a WPF application. Now we have two threads: the UI thread and an IO completion thread. The UI thread is running the caller of SaveAllAsync, which eventually returns, and now the UI thread is just sitting there in a loop handling Windows messages to trigger event handlers.

Eventually the IO completes and the IO completion thread sends a note to the UI thread that says "you can run the continuation of this task now". If the UI thread is busy, that message gets queued up; eventually the UI thread gets to it and invokes the continuation. Control resumes after the first await, and you enter the loop.

Now WriteStartElementAsync is invoked. It again starts some code running that depends on something happening on the IO completion thread (presumably; how it does its work is up to it, but this is a reasonable guess), that returns a Task representing that work, and the UI thread awaits that task. Again, the current position in the execution is signed up as the continuation of that task and control returns to the caller that invoked the first continuation -- namely, the UI thread's event processor. It continues merrily processing messages until one day the IO thread signals it and says that hey, the work you asked for is done on the IO completion thread, please invoke the continuation of this task, and so we go around the loop again...

Make sense?

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • But when the call to `xmlWriter.WriteStartDocumentAsync()` has completed; what happens? Is the current execution interrupted and returned back to `SaveAllAsync()`? – Xenoprimate Aug 26 '13 at 14:31
  • @Motig No, it won't interrupt anything. If there is a `SynchronizationContext` then that will be used. A sync context is specifically a way of saying, "If you give me a delegate, I can execute it under a particular set of conditions." Usually those "conditions" means running it in a particular thread, but not always. If there is no sync context then the rest of the method will be scheduled to run in the thread pool. If there *is* a sync context it generally means it will run whenever whatever else is running finishes up what *it* was doing. That's why blocking waits cause problems. – Servy Aug 26 '13 at 14:50
  • Yeah... I really don't get how any of this works without a threading model. All it seems to do is what threading does on a single-core machine; i.e. switching between two different tasks... I don't get what async/await provides over a simple threading model or the use of the TPL. I think it is prudent in my case (considering my lack of understanding) to not provide an async overload until I can really understand what I would expect to happen, and what I would expect the caller to do with it. – Xenoprimate Aug 26 '13 at 15:09
  • Nonetheless I'm going to edit my question to make it a bit easier to follow; hopefully someone can provide a really clear answer in the future. – Xenoprimate Aug 26 '13 at 15:10
  • @Motig: The threading model determines on what thread the continuation of the task may be invoked. – Eric Lippert Aug 26 '13 at 15:15
  • 1
    @Motig: The difference between two threads running on one core, one pre-empting the other, and async/await is that in the first case, the pre-emption is non-cooperative. The processor decides who gets to run when. With tasks it is more like Windows 3 style cooperative multitasking; the `await` means "I can stop here and wait for something to happen in the future; you go ahead and run other work, and let me know when that something has happened so I can continue". – Eric Lippert Aug 26 '13 at 15:17
  • @Motig: What `await` provides is the ability to write your cooperatively asynchronous code in a manner that does not turn its control flow inside-out. – Eric Lippert Aug 26 '13 at 15:18
  • @Motig: And yes, it is prudent to understand how it works before you make heavy use of it. :-) – Eric Lippert Aug 26 '13 at 15:18
  • @EricLippert So, I can understand that the 'awaited' call defers its processing time to the encapsulating method's caller once it reaches a blocking situation (e.g. I/O). And I get that the awaitable can be notified once that I/O operation has completed. But what I don't get is what happens next... Because by this time the caller of the async method is back executing its own code... So either that caller must be interrupted, or a new thread is used/spawned? – Xenoprimate Aug 26 '13 at 15:20
  • By the way, thanks for putting up with my dim-wittedness; I loved CLR via C# :) – Xenoprimate Aug 26 '13 at 15:21
  • @Motig: Are you perhaps confusing me with Jeffrey Richter? – Eric Lippert Aug 26 '13 at 15:22
  • 2
    @Motig: What happens next depends on the context in which the task was awaited. If it was a UI thread then as I said, the thread which wishes to trigger the continuation *sends a message to the UI thread*. That message sits there in a queue, and when the UI thread gets around to it, the continuation runs. In a context without a UI thread then the continuation could run on the thread that did the work, or it could run on a new worker thread, or whatever. The context associated with the task decides how to safely run the continuation. – Eric Lippert Aug 26 '13 at 15:24
  • @EricLippert Ah, whoops, yes, sorry... I recently bought loads of programming books... Got myself a bit confused there! Please accept my apologies. :) Anyway, thank you for your explanations. I figure I need to do some extended reading on the matter. Maybe work a little with the TPL some more... You know, learn to walk before I run. – Xenoprimate Aug 26 '13 at 15:27
  • 1
    @Motig: No worries. The TPL blog is a great source for articles. Also you might want to read my articles on CPS (http://blogs.msdn.com/b/ericlippert/archive/tags/continuation+passing+style/) and on how we motivated and designed async. (http://blogs.msdn.com/b/ericlippert/archive/tags/async/) Start from the bottom; they are listed in newest-to-oldest order. – Eric Lippert Aug 26 '13 at 15:35
0

Whenever a function is 'async' it means that when an 'await' is done on a System.Threading.Tasks.Task a two main things happen:

  1. The current position in the execution becomes a "continuation" of the awaited Task, meaning that after the Task is completed it will do what is necessary to make sure the remainder of the async method gets called. This work could be be done in a specific thread, some random thread pool thread or maybe the UI thread, this depends on the SynchronizationContext that the Task gets for its "continuation".

  2. The control is returned to either:

    • If it is the first await in the async method it returns to the original calling method with a System.Threading.Tasks.Task that represents the async method (NOT any of the Tasks created within the async method). It can just ignore it and carry on, wait for it with a Task.Result/Task.Wait() (careful you don't block the UI thread) or even do an await on it if it itself is an async method.

    • If it is not the first await in the async method it will just return to whichever handler in whichever thread was executing the "continuation" of the last Task that was awaited.

So to answer:

When they await SaveAllAsync() (or wait for the task)... What happens?

Doing await SaveAllAsync() won't NECESSARILY cause it to get stuck on any of it's internal awaits. This is because an await on SaveAllAsync() just drops back to the caller of whichever method called SaveAllAsync() which can carry on like nothing happened, just like SaveAllAsync()'s internal awaits dropped back to it. This keeps the thread moving and able to respond (potentially) to the request at some later time to run the "continuation" of SaveAllAsync()'s first internal await: await xmlWriter.WriteStartDocumentAsync()'. This way, bit by bit, SaveAllAsync() will eventually finish and nothing will get stuck.

BUT... if you OR some other code deeper down ever does a Task.Result/Task.Wait() on any of the Tasks returned by an await this could cause things to get stuck if the "continuation" tries to run on the same thread as the waiting code.

simon hearn
  • 69
  • 1
  • 7
  • This.issue of getting stuck waiting for same thread can be resolved with ConfigureAwait(false); which will allow any thread to continue. – jahansha Feb 07 '19 at 08:24
-1

A simple answer using parent method example:

await SaveAllAsync();
string x = ""; // <- this will run only when SaveAllAsync() is done including all awaits
dkostas
  • 57
  • 3
  • 1
    `Task.Delay(1);` is an awful example of a line of code. It fires a task and immediately forgets it. You should better replace this line with some synchronous code of any kind, like a `Console.WriteLine` or whatever. – Theodor Zoulias Feb 29 '20 at 18:54
  • Maybe you intended to await that Delay() but that still wouldn't make this an answer. – H H Feb 29 '20 at 23:39
  • The 2nd line of code was just an example. I have edited it to something more simple. My example answers to the 2nd question, e.g. that SaveAllAsync() will not stop on it's first internal await. – dkostas Mar 01 '20 at 20:37