如何处理使 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();
鉴于这段代码,我注意到我的 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();