Task.Factory.StartNew() 是否保证使用另一个线程而不是调用线程? [英] Is Task.Factory.StartNew() guaranteed to use another thread than the calling thread?

查看:40
本文介绍了Task.Factory.StartNew() 是否保证使用另一个线程而不是调用线程?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在从一个函数开始一个新任务,但我不希望它在同一线程上运行.我不在乎它运行在哪个线程上,只要它是一个不同的线程(所以 这个问题没有帮助.

I am starting a new task from a function but I would not want it to run on the same thread. I don't care which thread it runs on as long as it is a different one (so the information given in this question does not help).

我能保证下面的代码在允许 Task t 再次输入之前总是退出 TestLock 吗?如果不是,推荐的防止重入的设计模式是什么?

Am I guaranteed that the below code will always exit TestLock before allowing Task t to enter it again? If not, what is the recommended design pattern to prevent re-entrency?

object TestLock = new object();

public void Test(bool stop = false) {
    Task t;
    lock (this.TestLock) {
        if (stop) return;
        t = Task.Factory.StartNew(() => { this.Test(stop: true); });
    }
    t.Wait();
}

根据 Jon Skeet 和 Stephen Toub 的以下回答,确定性地防止重入的一种简单方法是传递 CancellationToken,如此扩展方法所示:

Based on the below answer by Jon Skeet and Stephen Toub, a simple way to deterministically prevent reentrancy would be to pass a CancellationToken, as illustrated in this extension method:

public static Task StartNewOnDifferentThread(this TaskFactory taskFactory, Action action) 
 {
    return taskFactory.StartNew(action: action, cancellationToken: new CancellationToken());
}

推荐答案

我邮寄了 Stephen Toub - PFX 团队 - 关于这个问题.他很快就回复了我,提供了很多细节——所以我将他的文字复制并粘贴到这里.我还没有全部引用,因为阅读大量引用的文本最终比香草黑白更不舒服,但实际上,这是斯蒂芬 - 我不知道这么多东西:) 我已经做了这个答案社区维基反映了下面的所有优点都不是我的内容:

I mailed Stephen Toub - a member of the PFX Team - about this question. He's come back to me really quickly, with a lot of detail - so I'll just copy and paste his text here. I haven't quoted it all, as reading a large amount of quoted text ends up getting less comfortable than vanilla black-on-white, but really, this is Stephen - I don't know this much stuff :) I've made this answer community wiki to reflect that all the goodness below isn't really my content:

如果你调用 等待()已完成的任务,不会有任何阻塞(如果任务以 TaskStatus 不同于 RanToCompletion,或者作为 nop).如果你在一个已经在执行的任务上调用 Wait(),它必须阻塞,因为它不能合理地做任何其他事情(当我说阻塞时,我包括真正的基于内核的等待和旋转,因为它通常会混合使用两者).类似地,如果您对具有 CreatedWaitingForActivation 状态的任务调用 Wait(),它将阻塞直到任务完成.这些都不是正在讨论的有趣案例.

If you call Wait() on a Task that's completed, there won't be any blocking (it'll just throw an exception if the task completed with a TaskStatus other than RanToCompletion, or otherwise return as a nop). If you call Wait() on a Task that's already executing, it must block as there’s nothing else it can reasonably do (when I say block, I'm including both true kernel-based waiting and spinning, as it'll typically do a mixture of both). Similarly, if you call Wait() on a Task that has the Created or WaitingForActivation status, it’ll block until the task has completed. None of those is the interesting case being discussed.

有趣的情况是,当您对处于 WaitingToRun 状态的任务调用 Wait() 时,这意味着它之前已排队到 TaskScheduler 但 TaskScheduler 还没有开始实际运行任务的委托呢.在这种情况下,对 Wait 的调用将询问调度程序是否可以通过调用调度程序的 TryExecuteTaskInline 在当前线程上运行 Task then-and-there方法.这称为内联.调度程序可以选择通过调用 base.TryExecuteTask 来内联任务,或者它可以返回 'false' 以指示它没有执行任务(通常这是用类似的逻辑完成的)..

The interesting case is when you call Wait() on a Task in the WaitingToRun state, meaning that it’s previously been queued to a TaskScheduler but that TaskScheduler hasn't yet gotten around to actually running the Task's delegate yet. In that case, the call to Wait will ask the scheduler whether it's ok to run the Task then-and-there on the current thread, via a call to the scheduler's TryExecuteTaskInline method. This is called inlining. The scheduler can choose to either inline the task via a call to base.TryExecuteTask, or it can return 'false' to indicate that it is not executing the task (often this is done with logic like...

return SomeSchedulerSpecificCondition() ? false : TryExecuteTask(task);

TryExecuteTask 返回布尔值的原因是它处理同步以确保给定的任务只执行一次).所以,如果调度器想在 Wait 期间完全禁止任务的内联,它可以实现为 return false; 如果调度器希望总是尽可能地允许内联,它可以实现为:

The reason TryExecuteTask returns a Boolean is that it handles the synchronization to ensure a given Task is only ever executed once). So, if a scheduler wants to completely prohibit inlining of the Task during Wait, it can just be implemented as return false; If a scheduler wants to always allow inlining whenever possible, it can just be implemented as:

return TryExecuteTask(task);

在当前的实现中(.NET 4 和 .NET 4.5,我个人不希望这会改变),如果当前线程是一个 ThreadPool 线程,那么针对 ThreadPool 的默认调度程序允许内联线程是先前将任务排队的线程.

In the current implementation (both .NET 4 and .NET 4.5, and I don’t personally expect this to change), the default scheduler that targets the ThreadPool allows for inlining if the current thread is a ThreadPool thread and if that thread was the one to have previously queued the task.

请注意,这里没有任意的可重入性,因为默认调度程序在等待任务时不会抽取任意线程......它只会允许内联该任务,当然还有任何内联该任务反过来决定做.还要注意 Wait 在某些情况下甚至不会询问调度程序,而是更喜欢阻塞.例如,如果您传入可取消的 CancellationToken,或者如果您传入一个非无限超时,它不会尝试内联,因为内联任务的执行可能需要任意长的时间,要么全有要么全无,最终可能会显着延迟取消请求或超时.总体而言,TPL 试图在浪费正在执行 Wait 的线程和过多地重用该线程之间取得适当的平衡.这种内联对于递归分而治之问题非常重要(例如 QuickSort),其中您生成多个任务,然后等待它们全部完成.如果在没有内联的情况下完成此操作,当您耗尽池中的所有线程以及它想提供给您的任何未来线程时,您将很快陷入僵局.

Note that there isn't arbitrary reentrancy here, in that the default scheduler won’t pump arbitrary threads when waiting for a task... it'll only allow that task to be inlined, and of course any inlining that task in turn decides to do. Also note that Wait won’t even ask the scheduler in certain conditions, instead preferring to block. For example, if you pass in a cancelable CancellationToken, or if you pass in a non-infinite timeout, it won’t try to inline because it could take an arbitrarily long amount of time to inline the task's execution, which is all or nothing, and that could end up significantly delaying the cancellation request or timeout. Overall, TPL tries to strike a decent balance here between wasting the thread that’s doing the Wait'ing and reusing that thread for too much. This kind of inlining is really important for recursive divide-and-conquer problems (e.g. QuickSort) where you spawn multiple tasks and then wait for them all to complete. If such were done without inlining, you’d very quickly deadlock as you exhaust all threads in the pool and any future ones it wanted to give to you.

Wait 不同,Task.Factory.StartNew 调用可能会在当时和那里结束执行任务,如果正在使用的调度程序选择同步运行任务作为 QueueTask 调用的一部分..NET 中内置的调度程序都不会这样做,我个人认为这对调度程序来说是一个糟糕的设计,但理论上是可能的,例如:

Separate from Wait, it’s also (remotely) possible that the Task.Factory.StartNew call could end up executing the task then and there, iff the scheduler being used chose to run the task synchronously as part of the QueueTask call. None of the schedulers built into .NET will ever do this, and I personally think it would be a bad design for scheduler, but it’s theoretically possible, e.g.:

protected override void QueueTask(Task task, bool wasPreviouslyQueued)
{
    return TryExecuteTask(task);
}

不接受 TaskSchedulerTask.Factory.StartNew 的重载使用来自 TaskFactory 的调度程序,在这种情况下Task.Factory 的目标是 TaskScheduler.Current.这意味着如果您从排队到这个神话的 RunSynchronouslyTaskScheduler 的任务中调用 Task.Factory.StartNew,它也会排队到 RunSynchronouslyTaskScheduler,导致StartNew 调用同步执行任务.如果您对此非常担心(例如,您正在实现一个库,但您不知道将从哪里调用),您可以显式地将 TaskScheduler.Default 传递给StartNew 调用,使用 Task.Run(始终转到 TaskScheduler.Default),或使用创建的 TaskFactoryTaskScheduler.Default 为目标.

The overload of Task.Factory.StartNew that doesn’t accept a TaskScheduler uses the scheduler from the TaskFactory, which in the case of Task.Factory targets TaskScheduler.Current. This means if you call Task.Factory.StartNew from within a Task queued to this mythical RunSynchronouslyTaskScheduler, it would also queue to RunSynchronouslyTaskScheduler, resulting in the StartNew call executing the Task synchronously. If you’re at all concerned about this (e.g. you’re implementing a library and you don’t know where you’re going to be called from), you can explicitly pass TaskScheduler.Default to the StartNew call, use Task.Run (which always goes to TaskScheduler.Default), or use a TaskFactory created to target TaskScheduler.Default.

<小时>

好的,看起来我完全错了,当前正在等待任务的线程可能会被劫持.这是发生这种情况的一个更简单的例子:


Okay, it looks like I was completely wrong, and a thread which is currently waiting on a task can be hijacked. Here's a simpler example of this happening:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1 {
    class Program {
        static void Main() {
            for (int i = 0; i < 10; i++)
            {
                Task.Factory.StartNew(Launch).Wait();
            }
        }

        static void Launch()
        {
            Console.WriteLine("Launch thread: {0}", 
                              Thread.CurrentThread.ManagedThreadId);
            Task.Factory.StartNew(Nested).Wait();
        }

        static void Nested()
        {
            Console.WriteLine("Nested thread: {0}", 
                              Thread.CurrentThread.ManagedThreadId);
        }
    }
}

示例输出:

Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 3
Nested thread: 3
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4
Launch thread: 4
Nested thread: 4

如您所见,有很多次等待线程被重用来执行新任务.即使线程获得了锁,这也可能发生.讨厌的重入.我感到震惊和担心:(

As you can see, there are lots of times when the waiting thread is reused to execute the new task. This can happen even if the thread has acquired a lock. Nasty re-entrancy. I am suitably shocked and worried :(

这篇关于Task.Factory.StartNew() 是否保证使用另一个线程而不是调用线程?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆