桌面应用程序中 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。所以你要么想要:

  1. 调用 DoSomethingAsync 方法而不使用任何线程,或者
  2. 在非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 后立即完成。线程到此点后就无事可做了,就被回收了。

旁注:

  1. CallLibraryAsync 方法违反了 not exposing asynchronous wrappers for synchronous methods 的准则。由于 DoSomethingAsync 方法实现为部分同步和部分异步,因此指南仍然适用恕我直言。
  2. 如果你喜欢命令式控制当前上下文的想法,而不是像 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).