【问题标题】:Which of these two methods is the correct one for running a CPU-intense long-running task in a WPF desktop app? [closed]这两种方法中哪一种是在 WPF 桌面应用程序中运行 CPU 密集型长时间运行任务的正确方法? [关闭]
【发布时间】:2020-12-25 11:09:59
【问题描述】:

关于异步操作,我知道如果您将代码暴露在库中以供其他地方使用,那么在您的实现中使用 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

【问题讨论】:

  • 尝试等待Task.Delay(1000).ConfigureAwait(false);
  • Task.Run,默认情况下,将安排由线程池线程完成的工作 - 这显然不是总是需要的。必要时使用Task.Run - 推荐阅读blog.stephencleary.com/2013/11/…
  • “我们的任务需要 1-2 分钟的密集处理” await Task.Delay(1000)模拟I/O-bound工作,Thread.Sleep(1000)模拟CPU密集型工作。您所说的“密集处理”可能属于第二类,但最好澄清一下。您还应该澄清您想到的应用程序类型。对 GUI 应用程序有意义的东西可能对 ASP.NET 应用程序没有意义。
  • 好点!是的,我们的长期运行任务 执行任何内部异步/等待,所以你是对的……我使用 await Task.Delay() 令人困惑。我更新了代码和问题,以更好地说明我在追求/询问的内容。
  • 没有太多要补充的,因为斯蒂芬已经为您的问题提供了所有正确的答案。但您可能仍想阅读my arguments,了解为什么在 WPF 应用程序的事件处理程序中使用 Task.Run 是个好主意。

标签: c# asynchronous async-await task c#-9.0


【解决方案1】:

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

我了解在您的实现中使用 Task.Start 是一种不好的做法,如果...

实际上是bad to use Task.Start in any code at all。该规则有一个非常模糊的例外,但一般来说,Task.Start 根本不应该使用。 Task.Run 是您应该用来将工作推送到线程池线程的东西。

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

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

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

使用后台线程使我们的非异步函数异步是否有意义,或者我们只是让我们的函数异步,然后用 await 调用它?此外,长时间运行的函数本身不执行任何 async/await 操作。

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

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

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

进入代码。

public static void Main()

这是第一个问题。由于the way await interacts with the SynchronizationContext,在async 方面,控制台应用程序的行为与WPF 应用程序完全不同。由于您的实际应用是 WPF 应用,因此尝试使用示例 WPF 应用比使用示例控制台应用要好得多。

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

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

你能把它标记为异步,然后在它上面使用等待而不需要 Task.Run()

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

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

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

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

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

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

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

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

【讨论】:

  • 嗨斯蒂芬!喜欢你的文章。首先,我不是那个投反对票的人。我发誓......人们在这里狙击手,真的很烦人。我希望MS的新页面没有同样的问题。其次,我实际上是指Task.Run 而不是Task.Start。错字就我而言。第三,我在这里发布了这个相关问题......stackoverflow.com/questions/65453171。我今晚学习的一部分。你也可以看看那里吗?
  • 另外,需要明确的是,这不是控制台应用程序。这来自一个示例 WPF 游乐场应用程序,我可以从 UI 动态启动“代码游乐场”,并且启动器在我的测试类型上调用静态主要方法。很抱歉造成混乱。
  • 旁注:@Stephen,实际上是您的文章让我了解了.ConfigureAwait,所以我很想知道我是否正确使用它。
  • @MarkA.Donohoe:这里没有使用ConfigureAwait。对于 WPF 应用程序,ConfigureAwait 通常不是必需的,除非您有 ton 的延续。
  • 这是我链接到的另一个问题。你有机会去那里看看吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-08-06
  • 1970-01-01
  • 2012-02-22
  • 1970-01-01
  • 1970-01-01
  • 2014-10-24
相关资源
最近更新 更多