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

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

问题描述

我开始从功能的新的任务,但我不希望它在同一个线程上运行。我不关心它,只要它是在<给定的不同的一个(这样的信息上运行哪个线程href=\"http://stackoverflow.com/questions/9410930/is-task-factory-startnew-guaranteed-to-create-at-least-one-new-thread\">this问题没有帮助)。

难道我保证下面的code将永远退出 TestLock 才让任务T 进入该再次?如果没有,什么是推荐的设计模式,以prevent重新entrency?

 对象TestLock =新的对象();公共无效测试(布尔停止=假){
    任务T;
    锁定(this.TestLock){
        如果(停止)返回;
        T = Task.Factory.StartNew(()=&GT; {this.Test(站:TRUE);});
    }
    t.Wait();
}

编辑:基于以下答案由Jon飞碟双向和斯蒂芬Toub,以确定性prevent重入将传递的CancellationToken,在这种扩展方法说明一个简单的方法:

 公共静态任务StartNewOnDifferentThread(这TaskFactory taskFactory,行动对行动)
 {
    返回taskFactory.StartNew(动作:动作的CancellationToken:新的CancellationToken());
}


解决方案

我邮寄斯蒂芬Toub - 在 PFX团队<成员/ A> - 这个问题。他回来我真的很快,有很多细节 - 所以我就复制和粘贴在这里他的文字。我还没有引述这一切,作为阅读大量引用的文本最终获得比香草不太舒服黑的白的,但实际上,这是史蒂芬 - 我不知道这个东西很多:)我做了这个答案社区维基以反映下面的所有的善良是不是真的我的内容。

如果你调用等待()上的完成了任务,不会有任何阻拦(它会只是抛出一个异常,如果该任务的状态完成比 RanToCompletion其他,否则返回作为NOP)。如果你调用等待()在一个已经执行,它必须阻止,因为没有别的,就可以合理做(当我说块,我既包括真正的内核任务基础的等待和纺纱,因为它通常会做两者的混合物)。同样,如果你调用等待()的任务那是在创建 WaitingForActivati​​on 状态时,它会阻塞,直到任务完成。这些都不是正在讨论的有趣的案例。

有趣的情况是,当你调用等待() WaitingToRun 状态的任务,这意味着它是previously已经排队到的TaskScheduler但的TaskScheduler尚未得到各地实际运行任务的委托呢。在这种情况下,要等待的通话将询问调度无论是确定在当前线程上运行的任务,然后有,通过对调度的 TryExecuteTaskInline 方法的调用。调度可以选择要么通过调用运行任务,然后有以 base.TryExecuteTask ,也可以返回false以表明它没有执行任务(通常这与逻辑做过类似返回SomeSchedulerSpecificCondition()假:TryExecuteTask(任务); ..原因 TryExecuteTask 返回布尔值是它处理的同步来确保在一个给定的任务仅曾经执行一次)。所以,如果一个调度程序想要等待期间完全禁止任务的内联,它可以只被实现为返回false; 如果调度要始终允许内联只要有可能,它正好可以实现为返回TryExecuteTask(任务); 在目前的实现(包括.NET 4和.NET 4.5的,我不亲自预计这种改变),靶向线程池的缺省调度程序允许内联如果当前线程是一个线程池线程,如果该线程是一个拥有previously排队的任务。

请注意,不是任意重入这里,在等待任务时默认的调度程序不会随意抽...线程,它会只允许任务被内联,当然任何内联的任务反过来决定做。还要注意的是等待甚至不会要求在一定的条件下的调度器,而不是preferring阻塞。例如,如果你在一个取消的CancellationToken传递,或者如果您在非无限超时通过,也不会尝试内联,因为它可能需要一个任意长的时间内联任务的执行,这是全有或全无,这最终可能会推迟显著取消请求或超时。总体来看,TPL试图在这里浪费罢工是在做的Wait'ing线程和重用线程太多间像样的平衡。这种内联的是递归的分而治之的问题,例如,真正重要的快速排序,在那里你产生多个任务,然后等待他们全部完成......如果这是没有内联做,你会很快死锁当你用尽池中的所有线程和任何将来的它想给你。

从等待分开,这也是(远程)可能是Task.Factory.StartNew呼叫最终可能执行的任务,然后有,当且仅当所用的调度程序选择同步为QueueTask调用的一部分运行任务。该调度没有内置到.NET将永远做到这一点,我个人认为这是一个不好的设计为调度程序,但它是理论上的可能,例如保护覆盖无效QueueTask(任务任务,布尔是previouslyQueued){返回TryExecuteTask(任务); } Task.Factory.StartNew 的不接受的TaskScheduler超载使用从TaskFactory,这在的情况下, Task.Factory <调度/ code>目标TaskScheduler.Current。这意味着,如果你从内部任务排队到这个神话RunSynchronouslyTaskScheduler叫 Task.Factory.StartNew ,它也将排队RunSynchronouslyTaskScheduler,导致Star​​tNew调用同步执行任务。如果你在所有关心这个(例如,您正在实施一个图书馆和你不知道你要去哪里要从称呼),你可以明确地传递TaskScheduler.Default到StartNew通话,使用Task.Run (它总是转到TaskScheduler.Default),或使用创造了一个TaskFactory目标TaskScheduler.Default。


编辑:好的,它看起来像我完全错了,这是目前在等待任务的线程的可以的被劫持。下面是这种情况发生的一个简单的例子:

 使用系统;
使用的System.Threading;
使用System.Threading.Tasks;命名空间的ConsoleApplication1 {
    类节目{
        静态无效的主要(){
            的for(int i = 0;我小于10;我++)
            {
                Task.Factory.StartNew(启动).Wait();
            }
        }        静态无效启动()
        {
            Console.WriteLine(启动线程:{0},
                              Thread.CurrentThread.ManagedThreadId);
            Task.Factory.StartNew(嵌套).Wait();
        }        静态无效的嵌套()
        {
            Console.WriteLine(嵌套线程:{0},
                              Thread.CurrentThread.ManagedThreadId);
        }
    }
}

示例输出:

 启动线程:3
嵌套螺纹:3
启动线程:3
嵌套螺纹:3
启动线程:3
嵌套螺纹:3
启动线程:3
嵌套螺纹:3
启动线程:4
嵌套螺纹:4
启动线程:4
嵌套螺纹:4
启动线程:4
嵌套螺纹:4
启动线程:4
嵌套螺纹:4
启动线程:4
嵌套螺纹:4
启动线程:4
嵌套螺纹:4

正如你可以看到,有很多时候,等待线程被再次用于执行新的任务。如果线程已经获得了锁这甚至发生。讨厌重入。我是合适的震惊和担心:(

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).

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();
}

Edit: 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());
}

解决方案

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.

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 in a state 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's in the Created or WaitingForActivation state, it’ll block until the task has completed. None of those is the interesting case being discussed.

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. The scheduler can choose to either run the task then and there 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);.. 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); 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.

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 that 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.

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); }. 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.


EDIT: 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);
        }
    }
}

Sample output:

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天全站免登陆