Top 5 ways to debug async/await and multi-threaded code in Visual Studio

Not all debug sessions were created equal, some are easier than others while other require remote connecting to a specific machine only on Wednesday at midnight. I’m sure you have some horror stories about elusive bugs and endless debug sessions. I did notice that when asked about the top ten hardest things to debug most developers would name “multi-threading” as one of the top three. I get it, debugging code that runs in a non-deterministic order with so many different things running at the same time is tough. There are a few nifty tools including some lesser known Visual Studio features that can help you reduce (if not completely eliminate) the pain of debugging multi-threaded code.

Sample code

For most of this post I’ll use the same simple example – tree traversal.

Cosider the following class:

class Tree
{
	public Tree(Tree left, Tree right)
	{
		Left = left;
		Right = right;
	}

	public Tree Left { get; }
	public Tree Right { get; }
}

For the sake of this example let’s say we want to count the number of nodes in the tree – the hard way:

private static int WalkTree(Tree tree)
{
	if (tree == null)
	{
		return 0;
	}

	var valLeft = 0;
	var valRight = 0;

	var tasks = new Task[2];
	tasks[0] = Task.Factory.StartNew(() => valLeft = CalculateLeft(tree));
	tasks[1] = Task.Factory.StartNew(() => valRight = CalculateRight(tree));

	Task.WaitAll(tasks);

	var result = valLeft + valRight + 1;

	return result;
}

So let’s dive in. I’ll place a breakpoint on the return at the end of the method.

Parallel Stacks

The first must-have tool that comes out of the box with Visual Studio is under the debug menu (Debug -> Windows -> Parallel Tasks),

Once it stops at the breakpoint the first time, I see the following view:

Parallel Stacks Window

In the right hands, this window is amazing, we can jump between threads and see exactly what’s executing either by Threads or Tasks. If you want to learn more there’s an excellent walkthrough on MSDN .

Parallel Watch

Any developer who ever pressed F5 knows (and loves) the various Watch windows – there’s also Autos, Locals and if that’s not enough four more Watch windows where you can go nuts!

Unfortunately once you start trying to understand values of variables over multiple threads or tasks, you find yourself frantically skipping between threads checking the watch window each time. That is both confusing and frustrating. There is a hidden gem under the Debug menu called Parallel Watch which gives a glance at the same variables across multiple threads:

Parallel Watch Window

First we get the Thread and Task Ids followed by recursion depth and we can see the various values throughout the threads/tasks in the system. Since it’s not that interesting yet, I’ll put a conditional breakpoint to stop once result is bigger than 50 and see what happens:

parallelwatch2

We can almost see the tree being traversed along. Jumping one more F5 ahead will give me those values:

parallelwatch3

Combined with Parallel Stack window and a bit of tinkering we can quickly find just about anything we want to learn about our code’s parallel execution path:

parallelstacks2

Tasks Window

Did you know that there’s a debug window dedicated to tasks? Ever opened it? (hint: Debug -> Windows -> Tasks).

No big deal – right? We had a similar Threads window since the beginning of time. Well that Tasks window is able to help us understand the existing tasks in the system – for example, the sample project looks something like this:

Tasks Window

See those colored icons? They tell me what is the state of the task. And yes, they can also show when one task is Deadlocked!

taskswindowdeadlock

Trace

All of the features above are great but they all suffer from one major flaw – they stop the execution of your code. And the problem with multi-threaded code is that messing with the way the threads execute can create a completely difference execution order which is unfortunate if it makes the bug you’re trying to pin-down disappear – only to reappear on a production system, late at night, or during the weekend…

And so you need a less intrusive way of gathering more information about the issue you’re investigating. If you try to use the same debugging techniques you use day in, day out – set a breakpoint and repeatedly hit F10 – that just won’t work! What will invariably happen is that you’ll suddenly hit a breakpoint on different thread, hit F10 a few more times, and you’ll get an exception on yet another thread. It quickly becomes a problem of trying to juggle too many balls in your head all at once.

The go to solution for those kind of problems is to add more logging. The problem is that it means you gotta stop debugging, add some more logging, hit F5, reproduce the steps, rinse and repeat again and again if you need more information…

Instead, Visual Studio provides Tracepoints which are like breakpoints that log information instead of stopping. While using Tracepoint will cause a performance hit, from my experience, most of the time it’s good enough in order to catch the bug. Using Tracepoint gets even better with OzCode.

Adding a new tracepoint using OzCode is simple: First you need to put a “regular” breakpoint and once you stop, open the quick watch window and use the magic wand to add a Tracepoint.

createtracepoint

OzCode’s Create Tracepoint window will open, and you can add text and any object or value in the current context to the Tracepoint.

tracepointwindow

Continue running and check OzCode’s tracepoints window at the end of the debug run for details and analysis.

tracepoints

To learn more about tracepoints check our feature page or GitHub examples.

Show All Instances

The last OzCode feature that targets a multi-threading debugging is Show All Instances. One of the issues with multi-threading is that the data you’re trying to get tends to be spread across several executing threads and for that we have the cool feature that will show all of the instances (or property value on those instances) throughout the system.

It’s simple to use. Navigate to an object and use the point at the class you care about. Use the Quick Actions button to choose “Show All Instances” and that’s it.

showallinstances

To navigate to the class definition, click on the quick actions for each property.

showallgenderinallinstances

Now you can grab any value from anywhere – regardless of the current execution context, and you’re even able to do a full text search through every instance!

Conclusion

Debugging can be painful and multi-threading can be hard. As you saw – collecting information in order to understand and fix the bug can be a lot easier than you think. It’s just a matter of knowing your tools and picking the right tool for each job.