TaskScheduler.Default不能保证任务总是在线程池线程上执行吗?

4

TaskScheduler.Default不一定保证任务总是在线程池线程上执行吗?

在修复一个错误时,我发现至少有一种情况下它并不能保证。可以通过以下方式进行复现(这是从实际代码编写的假例子):

var tcs = new TaskCompletionSource<bool>();
var sc = SynchronizationContext.Current;
sc.Post(_ => tcs.SetResult(true), null);
await tcs.Task.ContinueWith(_ =>
    {
        // breaks here
        Debug.Assert(Thread.CurrentThread.IsThreadPoolThread);
    }, 
    CancellationToken.None, 
    TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);

还有其他的情况吗?

另外,是否有一种简洁的方法可以确保ContinueWith操作在先前的任务已在线程池线程上完成或被排队到线程池后以同步方式执行(我知道我可以在ContinueWith操作中使用QueueUserWorkItem,但我不喜欢这种方法)。

编辑,我想我可以实现自己的TaskScheduler,并在TryExecuteTaskInline内检查是否已在线程池线程上,以控制此操作。


你所说的“...如果前置任务在池线程上完成,那么ContinueWith操作将会同步执行”是什么意思?这里的“同步执行”具体指什么?如果前置任务在池线程上运行,这又如何有意义呢? - Aaronaught
另外,你说这个重现案例不能在池线程上执行,但那么它是在哪个线程上执行的?这段代码片段是否是 UI 代码的一部分,在其中当前同步上下文是 Winforms 或 WPF 调度程序?TaskSchedulerSynchronizationContext 之间有一些古怪的交互作用,这就是为什么通常使用 TaskScheduler.FromCurrentSynchronizationContext 而不是 TaskScheduler.Default 的原因。 - Aaronaught
@Aaronaught,实际代码使用TaskCompletionSource将第三方代码事件进行了包装。该事件可以在任何线程上触发,包括具有同步上下文的线程。我绝不想使用TaskScheduler.FromCurrentSynchronizationContext。我希望在线程池上处理TaskCompletionSource.Task的完成。如果已经在线程池上,则希望使用TaskContinuationOptions.ExecuteSynchronously作为优化,以避免不必要的线程切换。 - avo
1
请注意,ExecuteSynchronously永远不能保证在同一线程上同步运行。它只是一个提示。 - usr
@usr,感谢您提出的好观点。我刚读到了一个不适用的情况:http://blogs.msdn.com/b/pfxteam/archive/2012/02/07/10265067.aspx - avo
1个回答

5
我认为发生这种情况的原因是您使用了TaskContinuationOptions.ExecuteSynchronously。根据文档说明:
ExecuteSynchronously指定应同步执行连续任务。使用此选项,连续将在导致前驱任务过渡到其最终状态的线程上运行。如果当创建连续时前驱已经完成,则连续将在创建连续的线程上运行。只有非常短暂的连续才应该同步执行。
如果前驱任务完成时,在线程池线程之外的线程上运行,则连续也将在该线程上运行,因此在这种情况下不会进行调度。另一个情况可能是连续任务已经完成,这也会同步运行。
更新:
要实现您在问题的第二部分中所要求的内容,我认为您需要一个自定义的awaiter。请参见此文章获取更多详情,但以下内容可能适用于您:
public static class Extensions
{
    public static ThreadPoolTaskAwaiter WithThreadPool(this Task task)
    {
        return new ThreadPoolTaskAwaiter(task);
    }

    public class ThreadPoolTaskAwaiter : INotifyCompletion
    {
        private readonly TaskAwaiter m_awaiter;

        public ThreadPoolTaskAwaiter(Task task)
        {
            if (task == null) throw new ArgumentNullException("task");
            m_awaiter = task.GetAwaiter();
        }

        public ThreadPoolTaskAwaiter GetAwaiter() { return this; }

        public bool IsCompleted { get { return m_awaiter.IsCompleted; } }

        public void OnCompleted(Action continuation)
        {
            if (Thread.CurrentThread.IsThreadPoolThread)
            {
                continuation();
            }
            else
            {
                Task.Run(continuation);
            }                
        }

        public void GetResult()
        {
            m_awaiter.GetResult();
        }
    }
}

然后您可以像这样使用它:
public static async Task Execute()
{          
    await Task.Delay(500).WithThreadPool();

    // does not break here
    if (!Thread.CurrentThread.IsThreadPoolThread)
    {
        Debugger.Break();
    }
}

жҲ‘зҹҘйҒ“TaskContinuationOptions.ExecuteSynchronouslyжҳҜеҺҹеӣ пјҢдҪҶжҲ‘д»ҚеңЁеҜ»жүҫжҲ‘её–еӯҗжңҖеҗҺдёҖж®өй—®йўҳзҡ„зӯ”жЎҲгҖӮ - avo
我想我可以实现自己的TaskScheduler,并在TryExecuteTaskInline中检查是否已经在线程池线程上,以控制这个过程。 - avo
1
你可能也可以使用自定义的awaiter。请参见我的更新。 - NeddySpaghetti

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