【问题标题】:How to deal with sync (blocking) call making the UI unresponsive如何处理使 UI 无响应的同步(阻塞)调用
【发布时间】:2019-02-06 23:50:06
【问题描述】:

鉴于这段代码,我注意到我的 UI 阻塞了一段时间(Windows 甚至弹出一条消息说应用程序没有响应。

using (var zip = await downloader.DownloadAsZipArchive(downloadUrl))
{
    var temp = FileUtils.GetTempDirectoryName();
    zip.ExtractToDirectory(temp);   // BLOCKING CALL

    if (Directory.Exists(folderPath))
    {
        Directory.Delete(folderPath, true);
    }

    var firstChild = Path.Combine(temp, folderName);
    Directory.Move(firstChild, folderPath);
    Directory.Delete(temp);
}

经过一番检查,我发现上面写着:

zip.ExtractToDirectory(temp);

是罪魁祸首。

我认为把它变成就足以让它发挥作用:

await Task.Run(() => zip.ExtractToDirectory(temp));

但是……这是解决这个问题的好方法吗?

我有 System.Reactive 的背景(我完全从事反应式编程),我想知道是否有更优雅的方式来处理这个问题。

【问题讨论】:

  • 如果同步调用被阻塞,则使其异步。这几乎是唯一的答案。
  • ZipFile.ExtractToDirectory 不是异步的,我无法使其异步,因为它不是我的,而是 .NET Framework 的一部分:(
  • 不像将async放在它前面那样“异步”。与“异步”中的异步一样,就像从 UI 线程中获取它一样。这是否意味着将其放入TaskThreadBackgroundWorker,或者任何通常没有一个正确答案的实现细节。
  • 如果你完全使用 Rx,你就不会到处都有 TPL 和异步。将两者结合起来通常比它的价值更痛苦。
  • @SuperJMN - 使用 Rx 执行此操作有什么问题?

标签: c# .net async-await system.reactive


【解决方案1】:

在 Rx 中这样做有点讨厌。结合Task<IDisposable> 很粗糙。这是我得到的:

Observable
    .FromAsync(() => downloader.DownloadAsZipArchive(downloadUrl))
    .SelectMany(z =>
        Observable
            .Using(() => z, zip => Observable.Start(() =>
            {
                var temp = FileUtils.GetTempDirectoryName();
                zip.ExtractToDirectory(temp);   // BLOCKING CALL

                if (Directory.Exists(folderPath))
                {
                    Directory.Delete(folderPath, true);
                }

                var firstChild = Path.Combine(temp, folderName);
                Directory.Move(firstChild, folderPath);
                Directory.Delete(temp);             
            })))
    .Subscribe();

【讨论】:

  • 疯狂但可爱。我绝对必须尝试(并理解)这一点。谢谢楼主!
  • 我想知道这个稍微不同的 DeploymentTask 会如何产生结果,它不使用异步 DownloadZipArchive 作为起点:github.com/WoA-project/WOA-Deployer/blob/master/Source/Deployer/…
  • 赞成,为另一个处理得很好的 RX 答案。我必须承认我从未使用过.Using
  • @MichaelRandall - Observable.Using 在使用任何 IDisposable 资源时是必不可少的 - 当 observable 结束时资源被释放。
【解决方案2】:

是的,您可以想象ExtractToDirectory 需要一些时间,不幸的是,这种方法没有async 版本,因为它是受CPU 限制的工作负载。

您可以做的(有争议的)是将其卸载到线程池,但您会招致线程池线程惩罚,这意味着您会占用线程池线程并阻塞它(占用宝贵的资源)。但是,由于等待 Task,它将释放 UI 上下文。

await Task.Run(() => zip.ExtractToDirectory(temp));

注意,虽然这可以解决问题,但最好的方法是使用TaskCompletionSource。这基本上是任务的事件(因为缺少更好的词),它会节省不必要的线程。

更新olitee的精彩评论

争议稍小...您可以将其扩展为使用:

await Task.Factory.StartNew(() => zip.ExtractToDirectory(temp), TaskCreationOptions.LongRunning); 

这将强制创建一个 用于操作的新专用线程。虽然会有 创建该线程的额外惩罚,而不是回收一个 合并一个 - 但对于像这样的长时间运行的操作来说,这不是一个问题。

【讨论】:

  • 争议较小 ;) ...您可以将其扩展为使用:Task.Factory.StartNew(() => zip.ExtractToDirectory(temp), TaskCreationOptions.LongRunning); - 这将强制为该操作创建一个新的专用线程。尽管创建该线程会产生额外的惩罚,而不是回收池中的线程 - 但对于像这样的长时间运行的操作来说,这不是问题。
  • @olitee 谢谢,我已将其添加到答案中,并归因于
【解决方案3】:

我很可能会将 zip 提取和目录创建代码重构为它自己的方法。这将使以后更容易卸载到线程。它还有一个额外的好处,就是让调用者决定是否要在另一个线程上运行它。

public void ExtractZip(ZipFile zip)
{
   var temp = FileUtils.GetTempDirectoryName();
   zip.ExtractToDirectory(temp);   // BLOCKING CALL

   if (Directory.Exists(folderPath))
   {
       Directory.Delete(folderPath, true);
   }

   var firstChild = Path.Combine(temp, folderName);
   Directory.Move(firstChild, folderPath);
   Directory.Delete(temp);
}

然后让顶级方法下载文件并解压缩 zip

// this method contains async IO code aswell as CPU bound code
// that has been offloaded to another thread
public async Task ProcessAsync()
{
   using (var zip = await downloader.DownloadAsZipArchive(downloadUrl))
   {
      // I would use Task.Run until it proves to be a performance bottleneck
      await Task.Run(() => ExtractZip(zip));
   }
}

【讨论】:

    猜你喜欢
    • 2020-09-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-19
    相关资源
    最近更新 更多