桌面应用程序中 sync/async 问题的更好解决方案?
Better solution for sync/async problem in desktop app?
我有 WinForms 应用程序,其中单击按钮会调用外部库的一些异步方法。
private async void button1_Click(object sender, EventArgs e)
{
await CallLibraryAsync();
}
private static async Task CallLibraryAsync()
{
var library = new Library();
await library.DoSomethingAsync();
}
图书馆看起来像这样:
public class Library
{
public async Task DoSomethingAsync()
{
Thread.Sleep(2000);
await Task.Delay(1000).ConfigureAwait(false);
// some other code
}
}
在任何异步代码之前都有一些通过 Thread.Sleep
调用模拟的计算。在这种情况下,此调用将阻塞 UI 线程 2 秒。 我无法更改 DoSomethingAsync
中的代码 。
如果我想解决阻塞问题,我可以这样调用 Task.Run
中的库:
private static async Task CallLibraryAsync()
{
var library = new Library();
// added Task.Run
await Task.Run(() => library.DoSomethingAsync());
}
它解决了问题,UI 不再阻塞,但我已经从 ThreadPool 中消耗了一个线程。这不是很好的解决方案。
如果我想在没有另一个线程的情况下解决这个问题,我可以这样做:
private static async Task CallLibraryAsync()
{
var library = new Library();
// added
await YieldOnlyAsync().ConfigureAwait(false);
await library.DoSomethingAsync();
}
// added
private static async Task YieldOnlyAsync()
{
await Task.Yield();
}
这个解决方案有效。 Task.Yield()
导致该方法 YieldOnlyAsync()
始终异步运行,并且 ConfigureAwait(false)
导致下一个代码 (await library.DoSomethingAsync();
) 在某个 ThreadPool 线程上运行,而不是 UI 线程。
但这是一个相当复杂的解决方案。有没有更简单的?
编辑:
如果库方法看起来像这样
public class Library
{
public async Task DoSomethingAsync()
{
await Task.Delay(1000).ConfigureAwait(false);
Thread.Sleep(2000);
await Task.Delay(1000);
// some other code
}
}
UI 线程不会被阻塞,我不需要做任何事情。但问题是它是一些我没有直接看到的实现细节,因为它可能在某些 nuget 包中。当我看到 UI 在某些情况下冻结时,我可能会在经过一些调查后发现这个问题(意思是 CPU 在异步方法中的任何 await 之前进行绑定计算)。没有Wait()
或Result
,那好找,这个比较有问题。
我希望尽可能以更简单的方式为这种情况做好准备。这就是为什么我不想在调用第三方库时使用 Task.Run
的原因。
If I want to solve blocking problem, I could call the library in Task.Run like this:
It solves the problem, UI is not blocke anymore, but I've consumed one thread from ThreadPool. It is not good solution.
这正是您想在 WinForms 应用程序中执行的操作。 CPU-intensive 代码应移动到单独的线程以释放 UI 线程。在 WinForms 中使用新线程没有任何缺点。
使用 Task.Run
将其移动到不同的线程,并从 UI 线程异步等待它完成。
引用微软的Asynchronous programming文章:
If the work you have is CPU-bound and you care about responsiveness, use async
and await
, but spawn off the work on another thread with Task.Run
.
当您使用 async/await 进行 I/O 或 CPU-bound 操作时,您的 UI 线程不会被阻塞。在您的示例中,您使用 Thread.Sleep(2000);
命令来模拟您的 CPU-bound 操作,但这将阻止您的 thread-pool 线程而不是 UI 线程。您可以使用 Task.Delay(2000);
来模拟您的 I/O 操作而不阻塞 thread-pool 线程。
I have no option to change the code
人们这么说,但您可能实际上并没有因此而受挫..
这是一个简单的应用程序,它与您遇到的问题相同:
绝对是困了:
所以让我们在加载 Reflexil 插件的情况下将它打入 ILSpy:
我们或许可以稍微缩短超时时间..右键单击,编辑..
设为 1ms,右键单击程序集并另存为..
这有点快了!
玩一玩,NOP it out等等
您写道:
If I want to solve blocking problem, I could call the library in Task.Run
like this:
private static async Task CallLibraryAsync()
{
var library = new Library();
// added Task.Run
await Task.Run(() => library.DoSomethingAsync());
}
It solves the problem, UI is not blocked anymore, but I've consumed one thread from ThreadPool
. It is not good solution.
(强调)
...然后您继续发明一个复杂的 hack 来做同样的事情:将 DoSomethingAsync
方法的调用卸载到 ThreadPool
。所以你要么想要:
- 调用
DoSomethingAsync
方法而不使用任何线程,或者
- 在非
ThreadPool
线程上调用DoSomethingAsync
方法。
第一个是不可能的。不使用线程就不能调用方法。代码在 CPU 上运行,而不是凭空运行。第二种方法可以通过多种方式完成,最简单的方法是使用 Task.Factory.StartNew
method, in combination with the LongRunning
标志:
await Task.Factory.StartNew(() => library.DoSomethingAsync(), default,
TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
这样您将在新创建的线程上调用 DoSomethingAsync
,该线程将在方法调用完成后立即销毁。需要明确的是,线程将在 调用 完成时被销毁,而不是在 异步操作 完成时销毁。根据问题中包含的 DoSomethingAsync
实现(第一个),调用将在创建 Task.Delay(1000)
任务并启动此任务的 await
后立即完成。线程到此点后就无事可做了,就被回收了。
旁注:
CallLibraryAsync
方法违反了 not exposing asynchronous wrappers for synchronous methods 的准则。由于 DoSomethingAsync
方法实现为部分同步和部分异步,因此指南仍然适用恕我直言。
- 如果你喜欢命令式控制当前上下文的想法,而不是像
Task.Run
方法那样使用包装器来控制它,你可以查看这个问题:Why was SwitchTo removed from Async CTP / Release? There are (not very many) people who like it as well, and there are libraries available that make it possible (SwitchTo
- Microsoft.VisualStudio.Threading).
我有 WinForms 应用程序,其中单击按钮会调用外部库的一些异步方法。
private async void button1_Click(object sender, EventArgs e)
{
await CallLibraryAsync();
}
private static async Task CallLibraryAsync()
{
var library = new Library();
await library.DoSomethingAsync();
}
图书馆看起来像这样:
public class Library
{
public async Task DoSomethingAsync()
{
Thread.Sleep(2000);
await Task.Delay(1000).ConfigureAwait(false);
// some other code
}
}
在任何异步代码之前都有一些通过 Thread.Sleep
调用模拟的计算。在这种情况下,此调用将阻塞 UI 线程 2 秒。 我无法更改 DoSomethingAsync
中的代码 。
如果我想解决阻塞问题,我可以这样调用 Task.Run
中的库:
private static async Task CallLibraryAsync()
{
var library = new Library();
// added Task.Run
await Task.Run(() => library.DoSomethingAsync());
}
它解决了问题,UI 不再阻塞,但我已经从 ThreadPool 中消耗了一个线程。这不是很好的解决方案。
如果我想在没有另一个线程的情况下解决这个问题,我可以这样做:
private static async Task CallLibraryAsync()
{
var library = new Library();
// added
await YieldOnlyAsync().ConfigureAwait(false);
await library.DoSomethingAsync();
}
// added
private static async Task YieldOnlyAsync()
{
await Task.Yield();
}
这个解决方案有效。 Task.Yield()
导致该方法 YieldOnlyAsync()
始终异步运行,并且 ConfigureAwait(false)
导致下一个代码 (await library.DoSomethingAsync();
) 在某个 ThreadPool 线程上运行,而不是 UI 线程。
但这是一个相当复杂的解决方案。有没有更简单的?
编辑:
如果库方法看起来像这样
public class Library
{
public async Task DoSomethingAsync()
{
await Task.Delay(1000).ConfigureAwait(false);
Thread.Sleep(2000);
await Task.Delay(1000);
// some other code
}
}
UI 线程不会被阻塞,我不需要做任何事情。但问题是它是一些我没有直接看到的实现细节,因为它可能在某些 nuget 包中。当我看到 UI 在某些情况下冻结时,我可能会在经过一些调查后发现这个问题(意思是 CPU 在异步方法中的任何 await 之前进行绑定计算)。没有Wait()
或Result
,那好找,这个比较有问题。
我希望尽可能以更简单的方式为这种情况做好准备。这就是为什么我不想在调用第三方库时使用 Task.Run
的原因。
If I want to solve blocking problem, I could call the library in Task.Run like this:
It solves the problem, UI is not blocke anymore, but I've consumed one thread from ThreadPool. It is not good solution.
这正是您想在 WinForms 应用程序中执行的操作。 CPU-intensive 代码应移动到单独的线程以释放 UI 线程。在 WinForms 中使用新线程没有任何缺点。
使用 Task.Run
将其移动到不同的线程,并从 UI 线程异步等待它完成。
引用微软的Asynchronous programming文章:
If the work you have is CPU-bound and you care about responsiveness, use
async
andawait
, but spawn off the work on another thread withTask.Run
.
当您使用 async/await 进行 I/O 或 CPU-bound 操作时,您的 UI 线程不会被阻塞。在您的示例中,您使用 Thread.Sleep(2000);
命令来模拟您的 CPU-bound 操作,但这将阻止您的 thread-pool 线程而不是 UI 线程。您可以使用 Task.Delay(2000);
来模拟您的 I/O 操作而不阻塞 thread-pool 线程。
I have no option to change the code
人们这么说,但您可能实际上并没有因此而受挫..
这是一个简单的应用程序,它与您遇到的问题相同:
绝对是困了:
所以让我们在加载 Reflexil 插件的情况下将它打入 ILSpy:
我们或许可以稍微缩短超时时间..右键单击,编辑..
设为 1ms,右键单击程序集并另存为..
这有点快了!
玩一玩,NOP it out等等
您写道:
If I want to solve blocking problem, I could call the library in
Task.Run
like this:private static async Task CallLibraryAsync() { var library = new Library(); // added Task.Run await Task.Run(() => library.DoSomethingAsync()); }
It solves the problem, UI is not blocked anymore, but I've consumed one thread from
ThreadPool
. It is not good solution.
(强调)
...然后您继续发明一个复杂的 hack 来做同样的事情:将 DoSomethingAsync
方法的调用卸载到 ThreadPool
。所以你要么想要:
- 调用
DoSomethingAsync
方法而不使用任何线程,或者 - 在非
ThreadPool
线程上调用DoSomethingAsync
方法。
第一个是不可能的。不使用线程就不能调用方法。代码在 CPU 上运行,而不是凭空运行。第二种方法可以通过多种方式完成,最简单的方法是使用 Task.Factory.StartNew
method, in combination with the LongRunning
标志:
await Task.Factory.StartNew(() => library.DoSomethingAsync(), default,
TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
这样您将在新创建的线程上调用 DoSomethingAsync
,该线程将在方法调用完成后立即销毁。需要明确的是,线程将在 调用 完成时被销毁,而不是在 异步操作 完成时销毁。根据问题中包含的 DoSomethingAsync
实现(第一个),调用将在创建 Task.Delay(1000)
任务并启动此任务的 await
后立即完成。线程到此点后就无事可做了,就被回收了。
旁注:
CallLibraryAsync
方法违反了 not exposing asynchronous wrappers for synchronous methods 的准则。由于DoSomethingAsync
方法实现为部分同步和部分异步,因此指南仍然适用恕我直言。- 如果你喜欢命令式控制当前上下文的想法,而不是像
Task.Run
方法那样使用包装器来控制它,你可以查看这个问题:Why was SwitchTo removed from Async CTP / Release? There are (not very many) people who like it as well, and there are libraries available that make it possible (SwitchTo
- Microsoft.VisualStudio.Threading).