头图

进一步了解Rider或申请免费试用,欢迎咨询JetBrains授权合作伙伴-龙智。

任务并行库 (TPL) 是所有 .NET 应用程序的基础,允许框架编写和执行多线程和并行代码。此外,想要充分利用资源的开发者可能会借助 System.Threading 和 System.Threading.Tasks 中的类型编写自定义代码。只有掌握并发和线程的基础,包括锁定、死锁、await 和计划,才能编写出快速且可扩缩的解决方案。要扩展对这些概念的理解,部分需要优秀的工具来协助呈现和分析任务执行的繁杂本质。
此前,JetBrains推出了 Tasks(任务)视图的第一次迭代,这个强大工具旨在帮助您了解当前应用程序流程中的既有任务。
本文将探究新的工具窗口,探讨其基本 UI 元素,并演示一些常见场景。最后,您将有能力探索自己的代码库并发现优化机会。

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

在 .NET 中,Tasks(任务)提供了对并发和多线程等概念的抽象。其中的想法是减少对 CPU 核心和线程的顾虑,并处理并发计划和执行工作的高级概念。这通常很棒,因为它可以帮助开发者编写更多命令式代码,同时有效利用所有系统资源。
虽然抽象相当不错,但没有一种抽象是完美的,而且有些时候,您必须同时处理多个任务的麻烦。其中包括死锁、竞争条件和导致背压的低效计划。处理抽象并不意味着您不应该理解它们的运作方式和原因。
通常,您负责计划任务,但不知道这些任务何时以及如何完成。执行是 .NET 运行时的工作。虽然您可能会看到预示问题即将发生的特定代码模式,但诊断 Tasks(任务)问题的最佳方式是在运行时。下文将展示代码库中的一些常见场景,以及 Tasks(任务)视图如何帮助您更好地理解您的应用程序。

常用任务

处理任务时,最可能出现的情况是使用的 API 会返回需要 async 和 await 关键字的任务。这些异步 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!");
}

在这个示例中,有两个任务:源自我们程序的主要任务和 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;
}

运行代码并查看 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);
}

通过 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)
            {
            }
        }
    }));
}

运行代码时,您会发现应用程序不会退出。在 Run(运行)工具栏中,点击暂停按钮来暂停应用程序。糟了,死锁!太震惊了!(其实也没有很震惊)。
任务表格视图中的一对死锁任务
图表视图更能说明问题,它展示了两个竞争的任务及其原因。
任务图表视图中的一对死锁任务
双击任意一个死锁的逻辑堆栈都会将您带到死锁的位置。
显示导致 JetBrains Rider 死锁的代码
这种便捷的导航应该可以让查找和解决死锁变得非常简单。

结论

Tasks(任务)视图目前在 JetBrains Rider 2024.2 EAP 中可用,期待您的反馈。任务可能是 .NET 开发的挑战性部分,希望额外的工具可以帮助您克服这些挑战。尝试一下,看看它是否可以帮助您优化现有代码或找到代码库中存在已久的问题。
本博文英文原作者:Khalid Abuhakmeh

咨询或申请试用 JetBrains Rider

关于 JetBrains Rider

JetBrains Rider 可以帮助您在 Windows、Mac 和 Linux 上开发完整的 .NET 应用程序和 Web 项目,以及 Unity、Unreal Engine 和基于 Godot 的游戏。它为 .NET 开发中使用的所有主要语言提供了优异的编辑支持和代码洞察,这些语言包括 C#、F#、Razor、Blazor 语法、JavaScript、TypeScript、XAML、HTML、CSS 和 SQL。JetBrains Rider 将 ReSharper 丰富的代码检查、上下文操作和重构与 IntelliJ 平台强大的 IDE 功能集和 Rider 自身的 .NET 特色融合在一起。尽管功能非常丰富,但 IDE 在不同平台上仍然能够快速运行和响应。
进一步了解
Rider或申请免费试用欢迎咨询JetBrains中国授权合作伙伴-龙智
官网:www.shdsd.com
电话:400-666-7732
邮箱:marketing@shdsd.com


龙智DevSecOps
6 声望3 粉丝

分享DevSecOps解决方案最新动态,帮助您学习与使用Atlassian, Perforce, Mend(原Whitesource), Cloudbees, TESSY, Jama Software及龙智自研产品,实现软件研发的高度协同与自动化,提高交付效率与质量,并确保...


引用和评论

0 条评论