如何处理使 UI 无响应的同步(阻塞)调用

How to deal with sync (blocking) call making the UI unresponsive

鉴于这段代码,我注意到我的 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 的背景知识(我很喜欢响应式编程),我想知道是否有更优雅的方法来处理这个问题。

是的,您可以想象 ExtractToDirectory 需要时间,不幸的是没有此方法的 async 版本,因为它是 CPU 绑定工作量。

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

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

请注意,虽然这可以解决问题,但最好的方法是使用 TaskCompletionSource。这基本上是任务的事件(缺少更好的词),它将避免不必要地占用线程。

更新 olitee

的精彩评论

Slightly less controversially... you could extend this to use:

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

which will force the creation of a new dedicated thread for the operation. Although there will be an extra penalty for creating that thread, rather than recycling a pooled one - but this is less of an issue for a long running operation like this.

我很可能会将 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));
   }
}

在 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();