【问题标题】:Issues with async blocking GUI thread, WinForms异步阻塞 GUI 线程 WinForms 的问题
【发布时间】:2017-10-25 22:19:57
【问题描述】:

我的代码应该工作(据我的最佳理解),但它没有。

我有一个 Windows 窗体应用程序。它有一个扫描选定目录的按钮,这可能会导致找到大量文件。扫描约 50k 文件时,大约需要 10 秒。

我尝试实现异步,以与主 GUI 异步运行文件扫描本身,但这会造成麻烦。任务本身运行良好,但仍会阻塞 GUI,导致整个应用程序冻结。

这是我的按钮代码

private async void BeginScanButton_Click(object sender, EventArgs e)
    {
        if (_osuDirectory == null)
            MessageBox.Show("You have not chosen an Osu! directory yet.");
        else
        {
            await ScanFilesTask();
        }

还有很多,但这是其中的相关部分。

任务本身是

 private Task ScanFilesTask()
    {
        FileList.Clear();
            return Task.Run(() =>
            {
                if (_jpgFilesChecked)
                    FileParser.ParseFiles(_osuDirectory, "*.jpg");
                if (_pngFilesChecked)
                    FileParser.ParseFiles(_osuDirectory, "*.png");
                if (_wavFilesChecked)
                    FileParser.ParseFiles(_osuDirectory, "*.wav");
                if (_aviFilesChecked)
                    FileParser.ParseFiles(_osuDirectory, ".avi");
            });
    }

最后,FileParser 类只是

        public static void ParseFiles(string dir, string extension)
        {
            Form1.FileList.AddRange(Directory.GetFiles(dir, extension, SearchOption.AllDirectories));
        }

(如果有人想看,所有代码都在github repo 上。相关的异步代码在 AsyncTest 分支中。)

这里的任何帮助将不胜感激。这是我的第一个真正的 C# 项目,非常感谢任何正确方向的指针。提前致谢!

【问题讨论】:

  • 我很惊讶这甚至可以工作。您正在从位于不同线程上的Task.Run() 执行Form1.FileList.AddRange。所以我假设你正在线程死锁你自己的应用程序。什么是文件列表?
  • FileList 是一个静态列表。它是静态的,所以我可以从 FileParser.cs 中添加它(我意识到这可能是一种完全愚蠢的做法,但我不知道如何去改变它。)
  • 在这种情况下,因为您正在 await(字面意思是等待)等待 ScanFilesTask() 完成,所以它阻塞了 UI 线程。
  • 谢谢!它现在正在工作 - 我将文件解析逻辑移到了任务中,它按预期运行。我真的很感激!
  • 不完全!我正在用最佳做法回答你的问题。

标签: c# .net winforms asynchronous


【解决方案1】:

阅读内容:

Task.Run Etiquette and Proper Usage

Task.Run vs BackgroundWorker, Round 1: The Basic Pattern

A Tour of Task, Part 0: Overview

摘录:

  • 自 .NET 4.0 中引入 Task 和 TPL(任务并行库)以来一直使用它的开发人员。这些开发人员熟悉 Task 以及如何在并行处理中使用它。这些开发人员面临的危险是 Task(被 TPL 使用)与 Task(被 async 使用)完全不同。

  • 在异步出现之前从未听说过 Task 的开发人员。对他们来说,Task 只是 async 的一部分——还有一个(相当复杂的)需要学习的东西。 “继续”是一个外来词。这些开发人员面临的危险是假设 Task 的每个成员都适用于异步编程,但事实并非如此。

异步没有意义,你想要的是并行运行。它们非常不同。您正在寻找的是:

按钮:

private void BeginScanButton_Click(object sender, EventArgs e)
{
    BeginScanButton.Enabled = false;
    if (_osuDirectory == null)
        MessageBox.Show("You have not chosen an Osu! directory yet.");
        BeginScanButton.Enabled = false;
    else
    {
        ScanFilesInParallel();
    }
}

扫描文件

private void ScanFilesInParallel()
{
    FileList.Clear();
    Task.Run(() =>
    {
      var result = new List<string>();

      if (_jpgFilesChecked)
        result.AddRange(FileParser.ParseFiles(_osuDirectory, "*.jpg"));
      if (_pngFilesChecked)
        result.AddRange(FileParser.ParseFiles(_osuDirectory, "*.png"));
      if (_wavFilesChecked)
        result.AddRange(FileParser.ParseFiles(_osuDirectory, "*.wav"));
      if (_aviFilesChecked)
        result.AddRange(FileParser.ParseFiles(_osuDirectory, ".avi"));

      return result;
     })
     .ContinueWith((task) => {
       FileList.AddRange(task.Result);
       BeginScanButton.Enabled = true;
     }, TaskScheduler.FromCurrentSynchronizationContext());
}

解析文件

public IEnumerable<string> ParseFiles(string dir, string extension)
{
  var result = Directory.GetFiles(dir, extension, SearchOption.AllDirectories).ToList();

  return result;
}

【讨论】:

  • 我会注意到我应该更新它以避免从Task.Run() 内部访问_osu* 变量。
  • 嗨 - 非常感谢您的帮助。但是,我遇到了一个问题。试图做到这一点.ContinueWith&lt;IEnumerable&lt;string&gt;&gt;((files) =&gt; {FileList.AddRange(files);BeginScanButton.Enabled = true;},TaskScheduler.FromCurrentSynchronizationContext());。给出错误'Cannot resolve method ContinueWith(lambda expression System.Threading.Tasks.Taskscheduler)', 我自己看了看,我无法解码这个错误。任何澄清将不胜感激。
  • 已更新。我有一段时间没有使用 ContinueWith 了,它将完成的任务传递给 Lamba。
  • 看来我可能遗漏了什么。我遇到了同样的错误,this one
  • 你能把.ContinueWith()复制到pastebin吗?
【解决方案2】:

只是给你一个我认为读起来更好一点的替代方案;尝试使用 Microsoft 的响应式框架。

那么你可以这样做:

private async void BeginScanButton_Click(object sender, EventArgs e)
{
    if (_osuDirectory == null)
        MessageBox.Show("You have not chosen an Osu! directory yet.");
    else
    {
        var exts = new[]
        {
            _jpgFilesChecked ? "*.jpg" : null,
            _pngFilesChecked ? "*.png" : null,
            _wavFilesChecked ? "*.wav" : null,
            _aviFilesChecked ? "*.avi" : null,
        }.Where(x => x != null);

        var query =
            from ext in exts.ToObservable()
            from fs in Observable.Start(() => FileParser.ParseFiles(_osuDirectory, ext))
            from f in fs
            select f;

        var files = await query.ToArray();

        FileList.AddRange(files);
    }
}

public static class FileParser
{
    public static string[] ParseFiles(string dir, string extension)
    {
        return Directory.GetFiles(dir, extension, SearchOption.AllDirectories);
    }
}

所有这些都是并行处理的,并在 UI 线程上完成。

NuGet "System.Reactive"、"System.Reactive.Windows.Forms" 和/或 "System.Reactive.Windows.Threading" (WPF) 获取位。

【讨论】:

    猜你喜欢
    • 2021-01-02
    • 1970-01-01
    • 2017-07-07
    • 1970-01-01
    • 2016-12-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-13
    相关资源
    最近更新 更多