使用ContinueWith时配置ConfigureAwait(false)

8

如何等价地使用:

await task.ConfigureAwait(false);

当使用类似于以下方式的continuations时(而不是使用C#编译器的async/await转换)?

var taskOfString = ScheduleWorkOnThreadPoolAsync();

// I'd like this continuation to not have to
// "flow" the synchronization context but to simply
// execute wherever it can, i.e. I'd like to tell is
// ConfigureAwait(false) for its previous task.
// How do I do that?
taskOfString.ContinueWith(t => { });


public async Task<string> ScheduleWorkOnThreadPoolAsync()
{
  return Task.Run(() => return "Foo" );
}

我假设“不做任何事情”,即保持原状,相当于调用ConfigureAwait(false),这也是我在调试代码时看到的情况。它会使用任何可用的线程。

只有在我们想要指定调度程序或同步上下文来运行延续时,我们才需要向接受TaskScheduler的重载传递额外的信息。否则,它会默认在没有执行上下文的情况下运行。

然而,如果我理解错误,请您确认或更正。

2个回答

16
当使用continuations时(不使用C#编译器的async/await转换),您几乎总是应该使用async/await。它们具有更安全的默认行为。ContinueWith是一种危险的低级API。
我假设什么也不做,即保持原样等同于调用ConfigureAwait(false)...只有当我们想要指定调度程序或同步上下文以在其上运行继续时,我们才需要传递额外信息到接受TaskScheduler的重载中。否则,默认情况下会运行而不考虑执行上下文。
不是这样的。尽管简单的测试无法揭示问题,但这是不正确的。
作为我的博客文章为什么不应该使用 ContinueWith中所述,ContinueWith 的默认 TaskScheduler 不是 TaskScheduler.Default,而是 TaskScheduler.Current。由于这在任何情况下都很混乱,你应该始终ContinueWithStartNew 传递一个 TaskScheduler,以使内容更加通俗易懂。

如果你想要 await x.ConfigureAwait(false) 的行为,实际上应该这样做:

var continuation = x.ContinueWith(callback, CancellationToken.None,
    TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.DenyChildAttach,
    TaskScheduler.Default);
TaskContinuationOptions.ExecuteSynchronously会模拟尽可能同步执行的await行为TaskContinuationOptions.DenyChildAttach可以避免问题,如果继续任务被附加到(异步任务的意图是当子任务使用AttachedToParent附加到它们时具有令人惊讶的行为)。 TaskScheduler.Default模拟始终在线程池上下文中执行的ConfigureAwait(false)行为。
最后需要注意的是,您可能需要对continuation进行一些操作-至少观察它以查看异常并以某种方式处理它们。
此时应该清楚我为什么推荐await。 最坏的情况下,您只需要添加一个帮助方法来使用await而不是ContinueWith - 在我看来,await更易于维护。

据我所知,提供TaskScheduler.Default与提供标志TaskContinuationOptions.HideScheduler相同。 - Mike
1
@Mike:不会;TaskScheduler.Default会在线程池线程上运行委托。TaskContinuationOptions.HideScheduler将确保TaskScheduler.CurrentTaskScheduler.Default,即使代码未在默认任务调度程序上运行。 - Stephen Cleary
@StephenCleary 这是什么意思?确保当前是默认值?你最近说默认值是线程池,而当前值是针对该执行的当前值...所以它永远不可能是另一件事... :/ - Hassan Faghihi
1
@deadManN:这里的术语有些混淆。底线是,你应该总是TaskScheduler传递给ContinueWith(和StartNew)。TaskScheduler.Default属性是线程池,但是ContinueWithStartNew使用的TaskScheduler实例的默认值不是TaskScheduler.Default。核心要点是始终传递TaskScheduler。或者只需使用await代替ContinueWithRun代替StartNew,这将使您的代码更易于维护。 - Stephen Cleary

4

我查看了Task的参考源码,然后通过GetAwaiter.OnCompleted进入ConfiguredTaskAwaitable,再通过Task.SetContinuationForAwait(其中continueOnCapturedContextfalse)进行回溯,最终进入以下代码:

if (!AddTaskContinuation(tc, addBeforeOthers: false))
        tc.Run(this, bCanInlineContinuationTask: false);

基本上来说,这个就是ContinueWith。所以:是的。

2
@WaterCoolerv2 如果你不确定的话,记得你也可以使用.ConfigureAwait(false).GetAwaiter().OnCompleted(callback) - 当然不是那么直接 - Marc Gravell
哇!谢谢。太棒了! :-) - Water Cooler v2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接