【问题标题】:In which case does TaskCompletionSource.SetResult() run the continuation synchronously?在哪种情况下 TaskCompletionSource.SetResult() 会同步运行延续?
【发布时间】:2017-01-10 19:36:42
【问题描述】:

最初我认为所有的延续都在线程池上执行(给定一个默认的同步上下文)。但是,当我使用 TaskCompletionSource 时,情况似乎并非如此。

我的代码如下所示:

Task<int> Foo() {
  _tcs = new TaskCompletionSource<int>();
  return _tcs.Task;
}

async void Bar() {
  Console.WriteLine(Thread.Current.ManagedThreadId);
  Console.WriteLine($"{Thread.Current.ManagedThreadId} - {await Foo()}");
}

Bar 在特定线程上被调用,TaskCompletionSource 保持未设置一段时间,这意味着返回的任务 IsComplete = false。然后过了一段时间,同一个线程将继续调用_tcs.SetResult(x),据我了解,这应该在线程池上运行延续。

但我在应用程序中观察到的是,运行延续的线程实际上仍然是同一个线程,就好像在调用 SetResult 时同步调用了延续一样。

我什至尝试在SetResult 上设置断点并越过它(并在延续中设置断点),这反过来实际上会继续同步调用延续。

SetResult()具体什么时候立即同步调用延续?

【问题讨论】:

标签: c# multithreading asynchronous async-await


【解决方案1】:

SetResult 通常从 TCS 同步运行延续。主要的例外情况是,如果您在创建 TCS(.NET 4.6 中的新功能)时显式传入TaskContinuationOptions.RunContinuationsAsynchronously 标志。异步运行的另一种情况是,如果它认为当前线程注定要失败。

这非常重要,因为如果您不小心,您最终可能会让调用代码控制一个本应执行其他工作(例如:处理套接字 IO)的线程。

【讨论】:

  • 顺便说一句:当您在不是原始 pre-await 调用者的线程上调用 SetResult() 时会发生什么?由于默认ConfigureAwait(...),不应该继续尝试到达原始调用者线程吗?
  • @Petrroll 如果在调用函数时没有SyncronisationContext.Current,它将没有原始线程可以去。
  • 如果有一些明确的 SyncContext?
  • AFAIK 如果有明确的SynchronizationContext 并且它被捕获,所有的延续都应该传递给它(除非任务在你等待它时完成)。这是正确的吗?
  • @JakubArnold 是的,同步上下文有点混淆了问题,但坦率地说,它有点模糊和困惑。真可惜,因为避免线程被盗很重要!此外,来电者可以随时ConfugureAwait(false)
【解决方案2】:

最初我认为所有的延续都在线程池上执行(给定一个默认的同步上下文)。但是,当我使用 TaskCompletionSource 时,情况似乎并非如此。

实际上,当使用await 时,大多数延续都是同步执行的。

马克的回答很棒;我只是想更详细一点...

TaskCompletionSource&lt;T&gt; 默认会在调用Set* 时同步操作。 Set* 将完成任务在单个方法调用中发出延续。 (这意味着在持有锁的同时调用Set* 会导致死锁。)

我在那里使用奇怪的短语“发出延续”,因为它可能会或可能不会实际执行它们;稍后会详细介绍。

TaskCreationOptions.RunContinuationsAsynchronously 标志将告诉TaskCompletionSource&lt;T&gt; 异步发出延续。这将任务的完成(仍由Set* 立即完成)与继续的发出(仅由Set* 调用触发)分开。所以使用RunContinuationsAsynchronously,调用Set*只会完成任务;它不会同步执行延续。 (这意味着在持有锁的同时调用Set* 是安全的。)

但回到默认情况,它同步发出延续。

每个延续有一个标志;默认情况下,继续执行异步,但可以通过TaskContinuationOptions.ExecuteSynchronously 使其同步。 (注意await does use this flag - 链接到我的博客;从技术上讲,这是一个实现细节,没有正式记录)。

但是,即使指定了ExecuteSynchronously,也有a number of situations where the continuation is not executed synchronously

  • 如果存在与延续关联的TaskScheduler,则该调度程序可以选择拒绝当前线程,在这种情况下,任务将排队到该TaskScheduler,而不是同步执行.
  • 如果当前线程正在中止,则任务在其他地方排队。
  • 如果当前线程的堆栈太深,则任务在其他地方排队。 (这只是一种启发式方法,不能保证避免StackOverflowException)。

这是相当多的条件,但通过您的简单控制台应用测试,它们都满足了:

  • TaskCompletionSource&lt;T&gt; 未指定 RunContinuationsAsynchronously
  • 延续 (await) 确实指定了ExecuteSynchronously
  • 延续没有指定TaskScheduler
  • 目标线程能够执行延续(未中止;堆栈正常)。

作为一般规则,我认为TaskCompletionSource&lt;T&gt; 的任何用法都应指定TaskCreationOptions.RunContinuationsAsynchronously。就个人而言,我认为使用该标志的语义更合适,也不那么令人惊讶。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-09-25
    相关资源
    最近更新 更多