.NET Tools
Essential productivity kit for .NET and game developers
How to use the Tasks View in JetBrains Rider
The Task Parallel Library (TPL) is foundational to all .NET applications, as it allows frameworks to write and execute multithreaded and parallel code. Additionally, developers looking to get the most out of their resources may want to write custom code, taking advantage of the types found in System.Threading
and System.Threading.Tasks
. You must understand concurrency and threading fundamentals to write fast and scalable solutions, including locks, deadlocks, awaits, and scheduling. Part of expanding your understanding of these concepts requires excellent tooling to help you visualize and make sense of a potentially chaotic swarm of executing tasks.
Exciting news! We’ve recently launched the first iteration of our Tasks view, a powerful tool designed to help you understand the existing tasks in the current application process.
In this post, we’ll examine the new tool window, discuss its essential UI elements, and demonstrate some common scenarios. You’ll be ready to explore your code bases and uncover optimization opportunities by the end.
Task Execution in .NET Applications
In .NET, Tasks offer an abstraction over concepts like concurrency and multithreading. The idea is to worry less about CPU cores and threads and deal with the high-level concepts of scheduling and executing work concurrently. This is generally great since it can help developers write more imperative code while efficiently utilizing all their system resources.
While abstractions are reasonably good, no abstraction is perfect, and at some point, you’ll have to deal with the pitfalls of multiple tasks concurrently. These include deadlocks, race conditions, and inefficient scheduling that causes backpressure. Dealing with abstractions doesn’t mean you should not understand how and why they work.
Typically, you will be responsible for scheduling tasks but never know when and how those tasks will be completed. Execution is the job of the .NET runtime. While you might see particular code patterns that are a sign of trouble to come, the best way to diagnose issues with Tasks is at runtime. Let’s look at some common scenarios you might uncover in your codebase and see how the Tasks view can help you better understand your applications.
Common Tasks
The most likely scenario when working with tasks is consuming an API that returns tasks that require the async
and await
keywords. These asynchronous APIs are in ASP.NET Core, MAUI, and Entity Framework Core. Many open-source projects have also moved to async-first APIs to support developer needs.
Let’s take a look at a simple example.
await BasicWork();
async Task BasicWork()
{
await Task.Delay(TimeSpan.FromSeconds(1));
Console.WriteLine("👋 Hello Tasks View!");
}
In this example, we have two tasks: The main task originating from our program and the BasicWork
method. We can confirm this by using the new Tasks View. While in a debugging session, click the Tasks tab to view the following table.
You can also switch to a Graph view by choosing the option at the top right.
You can also see the row information in the graph view by hovering over any stack item.
When working with Tasks, there are five statuses any task can have at any one time:
- Active: Currently executing and running.
- Scheduled: The task has been created but has not yet been executed.
- Awaiting: The task has been awaited but may be awaiting other tasks.
- Blocked: This task is at the top of the stack, and the thread executing it is blocked (sleep, waiting on lock, etc.). There are also some other tasks higher in the stack.
- Deadlocked: The task is in contention for a resource, and there’s a severe problem.
Now that we’ve seen the most common scenario, let’s look at parented tasks.
Parented Tasks
The act of parenting allows developers to logically group tasks. When creating a task within a task, you can use the TaskCreationOptions.AttachedToParent
to tie any new task to the containing task.
await ParentedTasks();
Task ParentedTasks()
{
// Parent task
var parentTask = Task.Factory.StartNew(() =>
{
Console.WriteLine("Parent task started.");
// Child task
var task = Task.Factory.StartNew(() =>
{
Console.WriteLine("Child task started.");
Task.Delay(2000).Wait(); // Simulating some work
Console.WriteLine("Child task completed.");
}, TaskCreationOptions.AttachedToParent);
Console.WriteLine("Parent task doing some work.");
}, TaskCreationOptions.AttachedToParent);
// Wait for parent task to complete, which includes the children
parentTask.Wait();
Console.WriteLine("Parent task completed.");
return Task.CompletedTask;
}
If you run the code and look at the Task view, you’ll see that we’ve successfully parented the child task.
Note that each new task has an integer Id
assigned by the .NET runtime. These identifiers help you keep track of tasks that exist in your current process.
This time, the graph view shows two async logical stacks resulting from the ParentedTasks
code, which uses methods like Wait
and returning Task.CompletedTask
.
That’s cool. Understanding whether tasks are related or have created separate logical stacks can help you understand if you’ve created a potential race condition.
Let’s see how the Tasks view can help us see how work is scheduled.
Scheduling Tasks
Any time you await
a task, you effectively schedule that work for a future time. That work can happen immediately after scheduling a task or after other scheduled tasks have been executed. Let’s see an example where we schedule several tasks and wait for them to complete.
await ScheduledWork();
async Task ScheduledWork()
{
Console.Write("Let's work...");
var tasks = Enumerable
.Range(1, 10)
.Select((i) => Task.Run(() => Console.Write(i)));
await Task.WhenAll(tasks);
}
The use of the Task.WhenAll
attempts to execute all the supplied tasks, with all of them being scheduled for future execution. You can see this in the Tasks view.
Additionally, the use of Task.WhenAll
creates an async logical stack under which all these operations can run.
When you step through the code during the debugging session, you’ll see the list of Tasks reduced as tasks are completed. You may also notice multiple tasks being executed at the same time.
It’s satisfying to see tasks completed, but it’s not great when they overstay their welcome. Let’s move on to the scariest scenario when dealing with tasks: deadlocks.
Deadlocks
The most common cause of a deadlock is contention around a shared resource guarded by some locking mechanism. Locks are essential when dealing with shared resources but can lead to app-breaking issues.
Let’s create a deadlock now because we love to live dangerously. More importantly, we’ll see how the Tasks view can help us identify it. We’ll schedule two tasks, each trying to lock the same variables.
await Deadlock();
// This method will cause a deadlock
// proceed with caution, oOOoOOoOo! 👻
async Task Deadlock()
{
object one = new();
object two = new();
var timer = new System.Timers.Timer(
TimeSpan.FromSeconds(2)
) { Enabled = true, AutoReset = false };
timer.Elapsed += (_, _) =>
{
// only see this if we're deadlocked
Console.WriteLine("💀Deadlock");
};
await Task.WhenAll(Task.Run(() =>
{
Console.WriteLine("Getting lock for one.");
lock (one)
{
Thread.Sleep(1000);
Console.WriteLine("Getting lock two in first task.");
lock (two)
{
}
}
}), Task.Run(() =>
{
Console.WriteLine("Getting lock two in second task.");
lock (two)
{
Thread.Sleep(1000);
Console.WriteLine("Getting lock one in second task.");
lock (one)
{
}
}
}));
}
When you run the code, you’ll notice the application never exits. In the Run toolbar, hit the pause button to pause the application. Oh no, a deadlock! I’m shocked! (well, I’m not that shocked).
The graph view is even more telling, showing the two tasks in contention and how we got here.
Double-clicking on any of the deadlocked logical stacks will take you to the location of the deadlock.
That ease of navigation should make finding and solving deadlocks, well, dead simple.
Conclusion
The Tasks view is currently available in JetBrains Rider 2024.2 EAP, and we’d love to hear your feedback so you can help shape this tool’s future. We understand that tasks can be a challenging part of .NET development, and we hope this additional tooling can help you overcome them. Try it and see if it can help you optimize existing code or find longstanding issues in your codebase.
Thanks for reading, and we look forward to your thoughts and comments.
image credit: Eden Constantino