0

我的问题是由本文中的一个例子提出的:

private void button1_Click(object sender, EventArgs e)  { 
    button1.Text = await Task.Run(async delegate 
    { 
        string data = await DownloadAsync(); 
        return Compute(data); 
    });  
}

这是我的心理模型告诉我的,这段代码会发生什么。用户单击 button1,导致 UI 框架在 UI 线程上调用 button1_Click。然后代码启动一个工作项以在 ThreadPool 上运行(通过 Task.Run)。该工作项开始一些下载工作并异步等待它完成。ThreadPool 上的后续工作项然后对该下载结果执行一些计算密集型操作,并返回结果,从而导致 UI 线程上正在等待的任务完成。此时,UI 线程处理此 button1_Click 方法的剩余部分,将计算结果存储到 button1 的 Text 属性中。

如果 SynchronizationContext 不作为 ExecutionContext 的一部分流动,我的期望是有效的。但是,如果它确实流动,我将非常失望。Task.Run 在调用时捕获 ExecutionContext,并使用它来运行传递给它的委托。这意味着在调用 Task.Run 时处于当前状态的 UI SynchronizationContext 将流入任务,并且在调用 DownloadAsync 并等待生成的任务时处于当前状态。这意味着 await 将看到 Current SynchronizationContext 和 Post 异步方法的其余部分作为继续在 UI 线程上运行。这意味着我的 Compute 方法很可能会在 UI 线程上运行,而不是在 ThreadPool 上运行,从而导致我的应用出现响应问题。

假设我有文章中的示例,而不是:

private void button1_Click(object sender, EventArgs e)  { 
    button1.Text = await DownloadAndComputeAsync(); 
}

// Can't be changed
private async Task<string> DownloadAndComputeAsync() {
    return await Task.Run(async delegate 
    { 
        string data = await DownloadAsync(); 
        return Compute(data); 
    });  
}

出于某种原因,我无法将代码更改DownloadAndComputeAsync为 use ConfigureAwait(false),可能是因为它位于库中或我无法控制的其他代码中。

有没有办法可以禁止将 UI 的 SynchronizationContext 捕获作为 ExecutionContext 的一部分?

我在上面链接的文章提到了 Task.Run 的内部版本,它禁止捕获 SynchronizationContext,但它是内部版本。如果它不是内部的,你可以这样做:

private void button1_Click(object sender, EventArgs e)  { 
    button1.Text = await Task.Run(DownloadAndComputeAsync, InternalOptions.DoNotCaptureSynchronizationContextInExecutionContext);
}

// Can't be changed
private async Task<string> DownloadAndComputeAsync() {
    return await Task.Run(async delegate 
    { 
        string data = await DownloadAsync(); 
        return Compute(data); 
    });  
}

用例

我没有一个案例我知道我会想要/需要使用它,但是在阅读了我在顶部链接的文章之后,我可以想到几个可能有用的地方。

  1. 如果从 UI 上下文中我使用的库不使用ConfigureAwait(false)并最终在 UI 线程上做大量工作。诚然,如果可以的话,在这种情况下,我可能会停止使用该库。

  2. 当前推荐的最佳实践似乎是以下之一

    1. 在任何地方使用ConfigureAwait(false),除非您知道您需要回到当前上下文。这样做的缺点ConfigureAwait(false)是非常丑陋并且对代码可读性产生负面影响(imo)。
    2. 仅在库代码中使用ConfigureAwait(false),但在所有内部代码中注意是否可以从例如 UI 上下文中使用给定的异步方法,并且ConfigureAwait(false)仅在绝对需要时使用。这似乎不太理想,因为它意味着类必须知道它们的异步方法是否可能被调用,例如,UI 上下文。

    我想知道是否可能存在第三种可能性:在任何时候调用异步方法时都要求在内部代码中,例如,事件处理程序中的代码或类似的代码负责确保异步方法不会执行的 UI 上下文t 将 UI 上下文作为其当前上下文,因此如果ConfigureAwait(false)在 UI 上下文上调用它,则不需要使用它。这样做的好处是,所有内部非 UI 代码都不需要使用ConfigureAwait(false),即使它是从事件处理程序调用的,并且所有上下文感知代码都仅限于 UI 代码,例如事件处理程序。

4

1 回答 1

3

首先;不要在这里使用异步委托。您不必要地使代码复杂化。

当您有一个已经是异步的操作并且想要获得其结果时,await它。如果您有必须执行的 CPU 密集型工作,则只需在Task.Run调用中运行await它即可获得结果。

private void button1_Click(object sender, EventArgs e)  
{ 
    string data = await DownloadAsync(); 
    button1.Text = await Task.Run(() => Compute(data));
}

这里很清楚发生了什么。获取异步操作的结果,使用它在非 UI 线程中执行一些 CPU 绑定的工作,然后在 UI 上下文中使用该结果。

现在,说了这么多,我希望您的代码实际上可以正常工作(鉴于当前上下文不是整个Task.Run委托的 UI 上下文),但是要理解该代码或查看代码要困难得多它在做什么。

于 2013-10-23T01:04:53.887 回答