如何抑制捕获当前SynchronizationContext的ExecutionContext?

时间:2013-10-23 00:04:09

标签: c# .net async-await synchronizationcontext

我的问题来自this article的一个例子:

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

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

     

如果SynchronizationContext不作为部分流动,我的期望是有效的   ExecutionContext。然而,如果它确实流动,我会非常痛苦   失望。 Task.Run在调用时捕获ExecutionContext,和   用它来运行传递给它的委托。这意味着UI   SynchronizationContext,当调用Task.Run时是当前的   将流入Task,并在调用时为Current   DownloadAsync并等待生成的任务。那意味着   await将看到Current SynchronizationContext并发布   其余的异步方法作为延续运行的   UI线程。这意味着我的计算方法很有可能   在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中的代码以使用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上下文。
    3. 我想知道是否可能存在第三种可能性:在内部代码中要求在任何时候调用异步方法,例如,事件处理程序或类似代码中的代码负责确保异步的UI上下文方法不会将UI上下文作为其当前上下文,因此不需要使用ConfigureAwait(false)关于在UI上下文中调用它的机会。这样做的好处是所有内部非UI代码都不需要使用ConfigureAwait(false),即使它是从事件处理程序调用的,并且所有上下文感知代码仅限于UI代码,例如事件处理程序。 / p>

1 个答案:

答案 0 :(得分: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上下文),但它是一个很难理解代码或看到它正在做什么。