miércoles, 10 de noviembre de 2010 19:49
Async makes asynchronous code look like synchronous code. But what effect does Async have on debugging? Can you single step and breakpoint as you would normally? What changes does Async force on traditional debugging practices, if any?
Todas las respuestas
lunes, 15 de noviembre de 2010 10:13
Hi Don, excellent question.
The quick answer is yes, you can single step and set breakpoints in async methods just like you can in normal methods, except there is a caveat - stepping over an await expression will return from the method if the call to BeginAwait returns true. Further, when debugging a continuation of an async method, the call stack will be that of the message pump or other magic that called the continuation, and not the original call. Both points have some interesting ramifications that are important to understand when debugging async methods.
Let's consider an example:
static void Main(string args)
static async Task Foo()
string s = await Bar();
static async Task<string> Bar()
return "Hello from Bar";
static void SomeLongRunningTask()
/* long running task */
This is a trivial program that doesn't do much of anything interesting but it will help us discuss debugging through an async method.
Before we talk about debugging there is one important thing to remember that will help to make better sense of how the control flow works. The await expression is rewritten into three calls and a return: one call to GetAwaiter which gets an "awaiter", and then one call each to BeginAwait and EndAwait on the awaiter, where BeginAwait's return value controls whether the async method returns or not. The statement "string s = await Bar();" in the example above is translated into something like this (omitting a number of details):
// string s = await Bar();
var t = Bar();
var awaiter = t.GetAwaiter();
string s = awaiter.EndAwait();
If you've read the spec that is included in the CTP then you know that BeginAwait takes an Action delegate to the continuation of the async method, and it is responsible for ensuring that the delegate is called at most once (typically by scheduling it be called on completion of the operation being awaited). BeginAwait returns true if the continuation will be called or false if not. BeginAwait would typically return false if the operation being awaited has already completed and there is no need to return from the async method. The relevant point here is that an await expression results in a possible return from the async method.
So now that we know how control flow works lets try stepping through the example. Starting in Main, if we step into Foo() we arrive at the beginning of the async method Foo*, just as if it were a normal method. In Foo we can step over the Console.WriteLine call like normal and then we get to the first statement containing an await expression. Await is a prefix operator, so in this case the call to Bar is where the instruction pointer will be pointing first. This is consistent with the translation I wrote above, where "var t = Bar();" is the first thing to happen. So if we execute a step into here, then the debugger will step into Bar - again like normal - where we can again step over the Console.WriteLine call and arrive at another await expression. This time we have a call to TaskEx.Run being awaited. TaskEx.Run takes a delegate, executes that delegate on a background thread, and then returns a Task representing that background operation. The task is marked completed when the background thread finsihes, i.e., when the delegate returns. If the delegate returns a value then Run will return a Task<T> where T is the return type of the delegate and you can obtain the result of delegate once the Task is complete by accessing the Result property on the task. Assume we don't have the source to TaskEx.Run and so if we step into this statement we will step over the call to TaskEx.Run, and step through the rest of the await. This is where debugging gets interesting.
What happens now depends on whether BeginAwait returns true or false. If it returns false then we just step over the statement and stay in Bar. If it returns true, well, where we end up could be anywhere, depending on whether other async methods are executing simultaneously. In this simple example there are no other async methods executing at the same time, so we actually end up staying in the same call to Bar, stopping on the next line. But how did that happen? Didn't we return from Bar?
Let's walk through what happens knowing what we know about how the await expressions are translated, and assuming Just My Code is turned on for simplicity's sake:
- We execute step into on the statement "await TaskEx.Run(SomeLongRunningTask);" which steps over TaskEx.Run and through to the call to BeginAwait which returns true, and so Bar returns.
- We're now in Foo at the previous await. Through compiler magic** the call to Bar returned a task representing the still incomplete call to Bar, and this is what is awaited in Foo. So an awaiter is retrieved for this task, BeginAwait is called, and it returns true because Bar is not done yet. So Foo returns as well.
- Now we're back in Main, and through the same compiler magic the call to Foo returned a Task representing the incomplete call to Foo. Here we call Task.Wait() which waits for the task to complete. The call to Wait also does some scheduling magic to make sure that pending Tasks have their continuations called when they complete.
- The thread running SomeLongRunningTask eventually ends, signalling completion on the task that Bar is waiting on, and so Bar's continuation is called, and this is where we resume stepping. Note that the callstack will now have the scheduling magic on it
So that's how stepping into an await works in a simple scenario. After returning to Bar there is more complication to get back to Foo. In the CTP, if you step over the return statement in Bar (or step out of Bar) then you will go back to Main, skipping Foo. This is because a return statement with a result in an async method is translated into a call to TaskCompletionSource.TrySetResult, and this finishes the task for Bar, which calls the continuation waiting on this task, which is Foo's continuation in this case. So in the CTP, on the return statement you must step into to it (F11) in order to get back to Foo. Once in Foo, you will see that the call stack has the call to Bar on it. It is not necessarily always the case that the continuation waiting on your async method will be called synchrously in this fashion however. In some cases the continuation may need to run on a different SynchronizationContext (like if the UI thread were awaiting on a background thread operation) and will be scheduled on that context's scheduler/message pump, instead of being called synchronously. If just my code is turned on, stepping will resume at the next point in code that is user code, which may or may not be the continuation awaiting the async method that you just stepped out of.
So what do you do if you step out of an async method and stepping does not resume where you expected it to? Set breakpoints. In async methods breakpoints work just like they do in normal methods. Using breakpoints will be more reliable than using the normal step in/over/out operations when debugging async methods because they always break when hit.
Phew, this is a complicated topic isn't it?
There is one last thing I want to mention. If you have multiple calls to the same async method running simultaneously, then stepping over an await in this method, or running with breakpoints in this method, can cause you to switch to a different call instance from the one you started debugging in. Imagine there was already a call to Bar in the example above that was in the middle of awaiting the SomeLongRunningTask and that you're stepping through a second call to Bar and you step over the await. It is possible you will end up on the next statement in Bar, but in the first call to Bar! Not the one you stepped from! This is an interesting quirk of async methods that is very important to make note of when debugging them, otherwise you might get very confused.
We are currently looking into the async debugging experience to see if we can provide new debugger stepping operations that will make debugging async more like debugging synchronous code. For example, stepping out of an async method could return you to the awaiter, if there is one.
SDE VB & VC#
* I've found with the CTP that stepping into Foo() in this example only works when "Just My Code" is turned on. If JMC is turned off, stepping into Foo() just steps over the whole statement. I'm not sure why this happens and I believe it to be a bug.
** I've left out the state machine implementation details of async methods here. Really, the methods Foo and Bar and turned into stubs that create a corresponding state machine class (similar to how iterators work). The state machine class has a method called MoveNext which holds all the user code rewritten into a state machine that handles the continuation logic, again similar to how iterators work. After creating the state machine, the stub methods Foo and Bar create a Task representing the async method, then call MoveNext, which synchronously executes the user's code until the first await that returns. When MoveNext returns, the stub method returns the Task, and this is what the await operator in Foo works on. You can see exactly how this works in the CTP by using ildasm or reflector to look at the compiler generated code for async methods.
- Editado IanWJHalliday lunes, 15 de noviembre de 2010 10:18 sup tag doesn't work on msdn forum, changed footnotes to * and **.
- Propuesto como respuesta Stephen ClearyMVP lunes, 15 de noviembre de 2010 14:10
- Marcado como respuesta Stephen Toub - MSFTMicrosoft Employee, Moderator martes, 16 de noviembre de 2010 19:39
domingo, 01 de julio de 2012 19:20
I believe this answer is now out of date. Since the Visual Studio 2012 Beta (or maybe earlier), the debugger can merrily step over awaits.
I'm not sure if I interpreted the original answer correctly, but I think it was saying that doing Step Over on an await would just return you permanently to the caller.
- Editado Alex.davies domingo, 01 de julio de 2012 19:37 Not sure I interpreted the original answer right