首页 > 解决方案 > 这两种方法中哪一种是在 WPF 桌面应用程序中运行 CPU 密集型长时间运行任务的正确方法?

问题描述

关于异步操作,我知道Task.Run如果您将代码暴露在库中以供其他地方使用,那么在您的实现中使用这种做法是不好的做法,因为您应该将此类决定留给库的调用者。该信息可以在此处的“重复”问题中找到。但这不是我要问的。我正在为我们自己的代码寻求澄清,该代码位于 WPF 桌面应用程序中,永远不会作为库公开给其他人使用。

此外,如果这有什么不同的话,我们的任务需要 1-2 分钟的密集 CPU 处理。

我的问题是在我们的特殊情况下,使用后台线程使我们的非异步函数异步是否有意义,或者我们只是让我们的函数异步,然后用 await 调用它?

此外,长时间运行的函数本身不做任何异步/等待的事情。这纯粹是一个线性的、CPU 密集型任务,我不在乎它在哪里运行,只是它的返回值(和进度)会报告回主 UI。

这就是我要澄清的问题。

如果重要的话,我直接在 WPF 桌面应用程序中使用带有 C#9 的 .NET 5.0(即,此代码不在要与他人共享的库中)。

这是一些(为清楚起见已更新!)示例代码说明了这一点...

public static void Main() {

    // Local function to start the import
    // Note: I read I should be using `async void` and not 'async Task' here since
    // this is a top-level async statement and I understand you need this for proper
    // exception handling. Is that correct?
    async void startImport(){

        Debug.WriteLine($"Import starting... IsBackground: " +
            $"{Thread.CurrentThread.IsBackground}");

        var progress = new Progress<string>(message =>
            Debug.WriteLine($"Progress Update: {message}"));
            
        // Toggle the comments on these lines and the line defining
        // CPUIntenseSequentialImport to try it with and without Task.Run() 
        var result = await Task.Run(() => CPUIntenseSequentialImport(progress));
        //var result = await CPUIntenseSequentialImport(progress);

        Debug.WriteLine($"Finished! The result is {result} - IsBackground: " +
            $"{Thread.CurrentThread.IsBackground}");
    }

    startImport();

    Debug.WriteLine($"Import started... IsBackground: " +
        $"{Thread.CurrentThread.IsBackground}");
}

// Can you just mark this as async, then use await on it and not need Task.Run(),
// or because it's a long-running task, should you NOT mark it with async, and
// instead use Task.Start() so it gets its own thread from the pool?
static public int CPUIntenseSequentialImport(IProgress<string> progress) {
//static async public Task<int> CPUIntenseSequentialImport(IProgress<string> progress) {

    Thread.Sleep(1000);
    progress.Report($"First part of import done - IsBackground: " +
        $"{Thread.CurrentThread.IsBackground}");

    Thread.Sleep(1000);
    progress.Report($"Next part of import done - IsBackground: " +
        $"{Thread.CurrentThread.IsBackground}");

    Thread.Sleep(1000);
    progress.Report($"Final part of import done - IsBackground: " +
        $"{Thread.CurrentThread.IsBackground}");

    return 44;
}

当按原样运行时,你会得到这个(注意更新不是从后台线程报告的)......

Import starting... IsBackground: False
Import started... IsBackground: False
Progress Update: First part of import done - IsBackground: False
Progress Update: Next part of import done - IsBackground: False
Progress Update: Final part of import done - IsBackground: False
Finished! The result is 44 - IsBackground: False

...但是当您交换注释/未注释的行时,您会得到这个(注意更新从后台线程报告的)...

Import starting... IsBackground: False
Import started... IsBackground: False
Progress Update: First part of import done - IsBackground: True
Progress Update: Next part of import done - IsBackground: True
Progress Update: Final part of import done - IsBackground: True
Finished! The result is 44 - IsBackground: False

标签: c#asynchronousasync-awaittaskc#-9.0

解决方案


这里实际上有很多东西要解压,所以我会尽我所能回答。

我知道在您的实施中使用 Task.Start 是一种不好的做法,如果...

在任何代码中使用Task.Start 实际上都不好。该规则有一个非常模糊的例外,但一般来说Task.Start根本不应该使用。Task.Run是您应该用来将工作推送到线程池线程的东西。

我正在为我们自己的代码寻求澄清,该代码位于 WPF 桌面应用程序中,永远不会作为库公开给其他人使用。

那是有价值的信息。归根结底,你自己的代码就是你自己的代码,即使你使用类似的东西Task.Start,这取决于你。另一方面,好的实践变成了好的实践,因为人们发现他们使代码更易于维护。(至少,这是我坚持的理论——尽管有货物崇拜;)

具体来说,即使您没有在 NuGet 上分发您的库,您的代码也可能会受益于其视为库。即使它不在实际的库 (dll) 项目中,代码也可能会受益于被视为位于单独的层中。关注点分离和所有这些 - 长时间运行的处理代码不应该知道它是从 UI 线程调用的(并且,给定单元测试,它可能不是)。

使用后台线程使我们的非异步函数异步是否有意义,或者我们只是使我们的函数异步,然后用等待调用它?此外,长时间运行的函数本身不做任何异步/等待的事情。

那么我会说保持同步。您这样做的唯一原因async是解除对 UI 线程的阻塞。但这是长时间运行的处理代码;为什么它甚至应该知道有一个UI 线程?引用我自己的话:

暂时回到原来的问题。问题是什么?UI 线程被阻塞。我们是如何解决的?通过更改服务。谁需要异步 API?只有 UI 线程。听起来我们只是严重违反了“关注点分离”原则。

这里的关键是解决方案不属于服务。它属于 UI 层本身。让 UI 层解决自己的问题,将服务排除在外。

上代码。

公共静态无效 Main()

这是第一个问题。async由于. _ _ awaitSynchronizationContext由于您的实际应用程序是 WPF 应用程序,因此使用示例 WPF 应用程序进行试验要比使用示例控制台应用程序好得多。

我读到我应该在async void这里使用而不是“异步任务”,因为这是一个顶级异步语句,我知道您需要它来进行正确的异常处理。那是对的吗?

async void用于事件处理程序。在所有其他情况下,避免async void. 它不适合在控制台应用程序中使用,尽管它可以在 WPF 应用程序中使用。

您可以将其标记为异步,然后在其上使用 await 而不需要 Task.Run()

async,不会启动新线程。当你编译这段代码时,编译器本身会给你一个警告,告诉你该方法将同步运行。

您是否应该不使用异步标记它,而是使用 Task.Start() 以便它从池中获取自己的线程?

这是最好的解决方案。保持同步方法同步,并使用Task.Run.

当按原样运行时,你会得到这个(注意更新不是从后台线程报告的)......

是的,因为一切实际上都是同步运行的。这里根本没有异步,如果你在 WPF 应用程序中尝试过,你的 UI 线程会冻结。

...但是当您交换注释/未注释的行时,您会得到这个(注意更新是从后台线程报告的)...

是的,因为这是一个控制台应用程序。Progress<T>在其构造函数中捕获SynchronizationContext.Current,并使用它来编组进度更新。由于这是一个控制台应用程序,因此没有SynchronizationContext,并且每个都IProgress<T>.Report被发送到一个线程池线程。可能出现故障(Progress<T>根本不应该在控制台应用程序中使用)。

如果您在 WPF 应用程序中运行它,那么Progress<T>它将捕获 WPF SynchronizationContext,并将进度报告发送到 UI 线程。


推荐阅读