.NET Tools

如何在 JetBrains Rider 中使用 Tasks(任务)视图

Read this post in other languages:

任务并行库 (TPL) 是所有 .NET 应用程序的基础,允许框架编写和执行多线程和并行代码。 此外,想要充分利用资源的开发者可能会借助 System.ThreadingSystem.Threading.Tasks 中的类型编写自定义代码。 只有掌握并发和线程的基础,包括锁定、死锁、await 和计划,才能编写出快速且可扩缩的解决方案。 要扩展对这些概念的理解,部分需要优秀的工具来协助呈现和分析任务执行的繁杂本质。

好消息! 我们最近推出了 Tasks(任务)视图的第一次迭代,这个强大工具旨在帮助您了解当前应用程序流程中的既有任务。

在这篇博文中,我们将研究新的工具窗口,探讨其基本 UI 元素,并演示一些常见场景。 最后,您将有能力探索自己的代码库并发现优化机会。

.NET 应用程序中的任务执行

在 .NET 中,Tasks(任务)提供了对并发和多线程等概念的抽象。 其中的想法是减少对 CPU 核心和线程的顾虑,并处理并发计划和执行工作的高级概念。 这通常很棒,因为它可以帮助开发者编写更多命令式代码,同时有效利用所有系统资源。

虽然抽象相当不错,但没有一种抽象是完美的,而且有些时候,您必须同时处理多个任务的麻烦。 其中包括死锁、竞争条件和导致背压的低效计划。 处理抽象并不意味着您不应该理解它们的运作方式和原因。

通常,您负责计划任务,但不知道这些任务何时以及如何完成。 执行是 .NET 运行时的工作。 虽然您可能会看到预示问题即将发生的特定代码模式,但诊断 Tasks(任务)问题的最佳方式是在运行时。 下文将展示代码库中的一些常见场景,以及 Tasks(任务)视图如何帮助您更好地理解您的应用程序。

常见任务

处理任务时,最可能出现的情况是使用的 API 会返回需要 asyncawait 关键字的任务。 这些异步 API 位于 ASP.NET Core、MAUI 和 Entity Framework Core 中。 许多开源项目也已转向异步优先 API 来支持开发者需求。

我们来看一个简单的示例。

await BasicWork();

async Task BasicWork()
{
    await Task.Delay(TimeSpan.FromSeconds(1));
    Console.WriteLine("👋 Hello Tasks View!");
}
Copy to clipboard

在这个示例中,我们有两个任务:源自我们程序的主要任务和 BasicWork 方法。 我们可以使用新的 Tasks(任务)视图来确认这一点。 在调试会话中,点击 Tasks(任务)标签页查看下表。

表模式下的 JetBrains Rider 的 Tasks(任务)视图

选择右上角的选项可以切换到 Graph(图表)视图。

图表模式下的 JetBrains Rider 的 Tasks(任务)视图

将鼠标悬停在任意堆栈条目上即可在图表视图中查看行信息。

在图表视图中显示行

处理任务时,任何任务在任何时候都会具有以下五种状态之一:

  • Active(有效):目前正在执行和运行。
  • Scheduled(已计划):任务已创建,但尚未执行。
  • Awaiting(等待):任务已经等待,但可能正在等待其他任务。
  • Blocked(阻塞):任务位于堆栈顶部,执行线程被阻塞(睡眠、等待锁等)。 堆栈中还有一些更高级别的任务。
  • Deadlocked(死锁):任务正在争用资源,并且存在严重问题。

现在,我们已经了解最常见的场景,接下来看一看父任务。

父任务

通过父级操作,开发者可以按逻辑对任务进行分组。 在任务中创建任务时,您可以使用 TaskCreationOptions.AttachedToParent 将新任务与包含任务绑定。

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;
}
Copy to clipboard

运行代码并查看 Tasks(任务)视图,您可以看到我们已经成功为子任务设置父任务。

显示作为父任务绑定到另一个任务的任务

请注意,每个新任务都有一个由 .NET 运行时指定的整数 Id。 这些标识符有助于跟踪当前流程中存在的任务。

这次,图表视图显示由 ParentedTasks 代码产生的两个异步逻辑堆栈,该代码使用类似于 Wait 的方法并返回 Task.CompletedTask

显示两个异步逻辑堆栈

很好, 了解任务是相关还是已创建单独逻辑堆栈,可以帮助您了解是否创建了潜在的竞争条件。

我们来看看 Tasks(任务)视图如何帮助我们了解工作的计划方式。

计划任务

await 一项任务时,您可以有效地将工作安排在未来的时间。 这项工作可以在计划任务之后立即进行,也可以在执行其他计划任务之后进行。 在下面的示例中,我们计划了几个任务并等待它们完成。

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);
}
Copy to clipboard

通过 Task.WhenAll 尝试执行所有提供的任务,所有任务都计划在未来执行。 您可以在 Tasks(任务)视图中看到此信息。

已创建的 10 个任务的计划任务

此外,使用 Task.WhenAll 会创建异步逻辑堆栈,所有操作都可以在该堆栈下运行。

两个异步逻辑堆栈,其中一个显示 10 个值

在调试会话期间逐步执行代码时,您将看到 Tasks(任务)列表随着任务完成而缩减。 您可能还会注意到多个任务在同时执行。

Tasks(任务)视图中的任务逐渐减少

看着任务完成当然感觉棒极了,但这个过程也不应该没完没了。 接下来,是处理任务时最可怕的情况:死锁。

死锁

死锁最常见的原因是对某种锁定机制保护的共享资源的争用。 锁定在处理共享资源时必不可少,但可能导致应用中断问题。

创造一个死锁, 我们来了解 Tasks(任务)视图如何帮助我们识别它。 我们将计划两个任务,每个任务都尝试锁定相同的变量。

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)
            {
            }
        }
    }));
}
Copy to clipboard

运行代码时,您会发现应用程序不会退出。 在 Run(运行)工具栏中,点击暂停按钮来暂停应用程序。 糟了,死锁! 太震惊了! (其实也没有很震惊)。

任务表格视图中的一对死锁任务

图表视图更能说明问题,它展示了两个竞争的任务及其原因。

任务图表视图中的一对死锁任务

双击任意一个死锁的逻辑堆栈都会将您带到死锁的位置。

显示导致 JetBrains Rider 死锁的代码

这种便捷的导航应该可以让查找和解决死锁变得非常简单。

结论

Tasks(任务)视图目前在 JetBrains Rider 2024.2 EAP 中可用,我们期待您的反馈,欢迎您帮助我们塑造工具的未来。 我们深知任务可能是 .NET 开发的挑战性部分,希望额外的工具可以帮助您克服这些挑战。 尝试一下,看看它是否可以帮助您优化现有代码或找到代码库中存在已久的问题。

感谢阅读,我们期待您的想法和评论。

图片来源:Eden Constantino

本博文英文原作者:

image description

Discover more