8

我想我在 TPL 中发现了一个严重的错误。我不知道。我花了很多时间挠头,无法理解这种行为。任何人都可以帮忙吗?

我的情况是:

  1. 我创建了一个做简单事情的任务。没有例外等等。
  2. 我注册了一个 ExecuteSynchronously 设置的延续。它必须在同一个线程上。
  3. 我在默认任务调度程序(线程池)上启动任务。开始线程继续并等待它。
  4. 任务开始。通过。
  5. 延续与任务在同一线程上开始(使先前的任务完成!)并进入无限循环。
  6. 等待线程没有任何反应。不想走得更远。卡住等待。我检查了调试器,任务是 RunToCompletion。

这是我的代码。感谢任何帮助!

// note: using new Task() and then Start() to avoid race condition dangerous
// with TaskContinuationOptions.ExecuteSynchronously flag set on continuation.

var task = new Task(() => { /* just return */ });
task.ContinueWith(
   _task => { while (true) { } /* never return */ },
   TaskContinuationOptions.ExecuteSynchronously);

task.Start(TaskScheduler.Default);
task.Wait(); // a thread hangs here forever even when EnterEndlessLoop is already called. 
4

3 回答 3

5

代表您提交了连接错误 - 希望没关系 :)

https://connect.microsoft.com/VisualStudio/feedback/details/744326/tpl-wait-call-on-task-doesnt-return-until-all-continuations-scheduled-with-executesynchronously-also-complete

我同意这是一个错误,只是想发布一个显示问题的代码片段,而无需“无休止”的等待。错误是 ExecuteSynchronously 意味着对第一个任务的等待调用不会返回,直到 ExecuteSynchronously 延续也完成。

运行下面的代码片段显示它等待 17 秒(因此所有 3 个都必须完成,而不仅仅是第一个)。无论第三个任务是从第一个任务还是第二个任务安排,都是一样的(所以这个 ExecuteSynchronously 将继续通过这样安排的任务树)。

void Main()
{
    var task = new Task(() => Thread.Sleep(2 * 1000));
    var secondTask = task.ContinueWith(
        _ => Thread.Sleep(5 * 1000),
        TaskContinuationOptions.ExecuteSynchronously);
    var thirdTask = secondTask.ContinueWith(
        _ => Thread.Sleep(10 * 1000),
        TaskContinuationOptions.ExecuteSynchronously);

    var stopwatch = Stopwatch.StartNew();
    task.Start(TaskScheduler.Default);
    task.Wait();
    Console.WriteLine ("Wait returned after {0} seconds", stopwatch.ElapsedMilliseconds / 1000.0);
}

唯一让我认为这可能是故意的(因此更多的是文档错误而不是代码错误)是斯蒂芬在这篇博文中的评论

ExecuteSynchronously 是对优化的请求,以在完成我们继续的前项任务的同一线程上运行延续任务,实际上运行延续作为前项过渡到最终状态的一部分

于 2012-05-27T17:23:13.947 回答
3

当您考虑任务内联时,这种行为是有意义的。当您Task.Wait在任务开始执行之前调用时,调度程序将尝试内联它,即在调用的同一线程上运行它Task.Wait。这是有道理的——当您可以重用线程来执行任务时,为什么还要浪费线程等待任务呢?

现在,当ExecuteSynchronously指定时,调度程序被指示在与先前任务相同的线程上执行延续 - 这是内联情况下的原始调用线程。

请注意,当内联未发生时,您所期望的行为确实发生了。您所要做的就是禁止内联,这很容易 - 指定等待超时或传递取消令牌,例如

task.Wait(new CancellationTokenSource().Token); //This won't wait for the continuation

最后请注意,不能保证内联。在我的机器上,它没有发生,因为任务在Wait调用之前已经开始,所以我的Wait调用没有阻塞。如果您想要一个可重现的块,请调用Task.RunSynchronously.

于 2012-05-28T13:48:56.897 回答
0

所以事实证明这确实是一个错误。我想每个人都同意。但是,如果打算这样做,那么 API 和文档似乎具有高度误导性。

我使用的解决方法只是使用 ManualResetEventSlim。

var eventSlim = new ManualResetEventSlim(false);
var task = new Task(() => { eventSlim.Set(); });
task.ContinueWith(
   _task => { while (true) { } /* never return */ },
   TaskContinuationOptions.ExecuteSynchronously);

task.Start(TaskScheduler.Default);
eventSlim.Wait();

谢谢大家看这个!对于所有评论。

问候。

于 2012-05-27T18:45:32.883 回答