是Task.Factory.StartNew()保证使用另一个线程比调用线程?
我从一个函数开始一个新的任务,但我不希望它在同一个线程上运行。 我不关心它运行在哪个线程,只要它是一个不同的线程(所以在这个问题给出的信息不起作用)。
我保证,下面的代码将总是退出TestLock
之前允许Task t
再次进入它? 如果不是,build议的devise模式是什么,以防止重复?
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,如下面的扩展方法所示:
public static Task StartNewOnDifferentThread(this TaskFactory taskFactory, Action action) { return taskFactory.StartNew(action: action, cancellationToken: new CancellationToken()); }
我把这个问题邮寄给PFX团队的成员Stephen Toub。 他真的很快就回到我身边了,所以我只是在这里复制并粘贴他的文字。 我没有引用这一切,因为阅读大量的引用文本最终变得比香草黑白舒适,但实际上,这是斯蒂芬 – 我不知道这些东西:)我已经做了这回答社区维基百科反映,下面的所有善良不是真正的我的内容。
如果在完成的任务上调用Wait()
,则不会有任何阻塞(如果任务在RanToCompletion
以外的状态下完成,或者作为nop返回,则会引发exception)。 如果你在已经执行的任务上调用Wait()
,它必须被阻塞,因为没有别的东西可以合理地执行(当我说block时,我包括了真正的基于内核的等待和旋转,因为它通常会做两者的混合物)。 同样,如果在处于“已Created
或“ WaitingForActivation
状态的任务中调用Wait()
,它将阻塞,直到任务完成。 这些都不是讨论的有趣的案例。
有趣的情况是当您在WaitingToRun
状态的任务上调用Wait()
时,意味着它先前已经排队等待到TaskScheduler,但是TaskScheduler尚未得到实际运行任务的委托。 在这种情况下,对Wait的调用会询问调度程序是否可以通过调用调度程序的TryExecuteTaskInline
方法在当前线程上运行任务。 调度程序可以select通过调用base.TryExecuteTask
来运行任务,也可以返回false来指示它不执行任务(通常这是通过return SomeSchedulerSpecificCondition() ? false : TryExecuteTask(task);
来完成的return SomeSchedulerSpecificCondition() ? false : TryExecuteTask(task);
TryExecuteTask
返回一个布尔值的原因是它处理同步以确保一个给定的Task只能执行一次)。 因此,如果一个调度程序想要在Wait期间完全禁止任务的内联,那么只能作为return false;
来实现return false;
如果一个调度程序想要尽可能地允许内联,它可以被实现为return TryExecuteTask(task);
在当前的实现中(.NET 4和.NET 4.5,我不认为这会改变),以ThreadPool为目标的默认调度程序允许内联当前线程是否是ThreadPool线程,如果该线程是一个先前排队的任务。
请注意,这里没有任意的重入,因为默认的调度程序在等待任务时不会抽取任意线程……它只会允许内联该任务,当然任何内联任务依次决定去做。 另外请注意,Wait在某些情况下甚至不会询问调度器,而宁愿阻塞。 例如,如果传递可取消的CancellationToken,或者如果传入非无限的超时值,则不会尝试内联,因为可能需要任意长的时间来内联任务的执行,这是全部或全部,这可能最终会延迟取消请求或超时。 总的来说,TPL试图在这里浪费一个正在进行等待和重复使用该线程的线程之间的一个体面的平衡。 这种内联对于recursion分而治之问题是非常重要的,例如QuickSort,在这里你产生了多个任务,然后等待它们全部完成……如果这样做没有内联,你很快就会死锁用尽池中的所有线程以及将要提供给您的任何未来的线程。
与Wait不同的是,如果正在使用的调度程序select作为QueueTask调用的一部分同步运行任务,那么Task.Factory.StartNew调用也可能(远程)结束,然后在那里执行任务。 .NET中没有内置的调度器会这样做,我个人认为这对调度器来说是一个糟糕的devise,但它在理论上是可能的,例如protected override void QueueTask(Task task, bool wasPreviouslyQueued) { return TryExecuteTask(task); }
protected override void QueueTask(Task task, bool wasPreviouslyQueued) { return TryExecuteTask(task); }
。 不接受TaskScheduler的Task.Factory.StartNew
的重载将使用Task.Factory.StartNew
中的调度程序,在Task.Factory的情况下,它调度TaskScheduler.Current。 这意味着,如果您从排队等待到此神话的RunSynchronouslyTaskScheduler的任务中调用Task.Factory.StartNew
,它也将排队到RunSynchronouslyTaskScheduler,导致StartNew调用同步执行任务。 如果你对此一无所知(例如,你正在实现一个库,而你不知道从哪里调用),则可以明确地将TaskScheduler.Default传递给StartNew调用,使用Task.Run (总是进入TaskScheduler.Default),或者使用创build的TaskFactory来定位TaskScheduler.Default。
编辑:好吧,它看起来像我是完全错误的,目前正在等待任务的线程可以被劫持。 这是一个更简单的例子:
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
正如你所看到的,等待线程被重用来执行新任务的时候有很多次。 即使线程获得locking,也可能发生这种情况。 令人讨厌的重入。 我适当震惊和担心:(
为什么不devise它,而不是向后弯,以确保不会发生?
TPL在这里是一个红色的鲱鱼,重入是可以在任何代码中发生的,只要你可以创build一个循环,而且你不知道你的堆栈框架将会发生什么。 同步重入是这里最好的结果 – 至less你不能自我死锁(很容易)。
锁pipe理交叉线程同步。 它们与pipe理重入正交。 除非你正在保护一个真正的单一使用资源(可能是一个物理设备,在这种情况下,你应该使用一个队列),为什么不只是确保你的实例状态是一致的,所以重入只能工作。
(Side认为:Semaphores是否可以重新缩进?)
你可以通过编写一个在线程/任务之间共享一个套接字连接的快速应用程序来轻松地进行testing。
这个任务会在向socket发送消息并等待响应之前获得一个锁。 一旦阻塞并变为空闲(IOBlock),在同一个块中设置另一个任务来执行相同的操作。 它应该阻止获取锁,如果没有,第二个任务被允许通过锁,因为它运行在同一个线程,那么你有问题。
Erwin提出的new CancellationToken()
解决scheme对我来说并不奏效,反正内联也发生了。
所以我最终使用了Jon和Stephenbuild议的另一个条件( ... or if you pass in a non-infinite timeout ...
):
Task<TResult> task = Task.Run(func); task.Wait(TimeSpan.FromHours(1)); // Whatever is enough for task to start return task.Result;
注意:为了简单起见,省略exception处理等,你应该介意在生产代码中的那些。