我从一个函数开始一个新任务,但我不希望它在同一个线程上运行。我不关心它运行在哪个线程上,只要它是一个不同的线程(因此this question中给出的信息没有用)。
我是否保证在允许TestLock
再次输入之前,以下代码将始终退出Task t
?如果没有,建议的设计模式是什么来防止再次发生?
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());
}
答案 0 :(得分:82)
我邮寄了Stephen Toub-- PFX Team的成员 - 关于这个问题。他很快就回到了我身边,带着很多细节 - 所以我只是复制并粘贴他的文字。我没有引用它,因为阅读大量的引用文本最终变得不如香草黑白色,但真的,这是斯蒂芬 - 我不知道这么多东西:)我做了这个答案社区维基反映下面所有的好处并不是我的内容:
如果您在已完成的
Wait()
上致电Task,则不会有任何阻止(如果任务以TaskStatus以外的{{}}完成,则只会抛出异常{1}},或以nop的形式返回。如果你在已经执行的任务上调用RanToCompletion
,它必须阻塞,因为它没有其他任何事情可以合理地做(当我说块时,我包括真正的基于内核的等待和旋转,因为它通常混合两者)。同样,如果您在具有Wait()
或Wait()
状态的任务上调用Created
,则会阻止该任务完成。这些都不是正在讨论的有趣案例。有趣的情况是,当您在
WaitingForActivation
状态的任务上调用Wait()
时,意味着它之前已排队到TaskScheduler,但TaskScheduler尚未到达实际上还在运行Task的委托。在这种情况下,对WaitingToRun
的调用将询问调度程序是否可以通过调用调度程序的Wait
方法在当前线程上运行任务。这称为内联。调度程序可以选择通过调用TryExecuteTaskInline
来内联任务,也可以返回'false'来表示它没有执行任务(通常这是通过逻辑来完成的......base.TryExecuteTask
return SomeSchedulerSpecificCondition() ? false : TryExecuteTask(task);
返回布尔值的原因是它处理同步以确保给定的任务只执行一次)。因此,如果调度程序想要在TryExecuteTask
期间完全禁止内联任务,则可以将其实现为Wait
如果调度程序希望始终允许内联,则可以将其实现为:< / p>return false;
在当前的实现中(.NET 4和.NET 4.5,我个人并不希望这会改变),以ThreadPool为目标的默认调度程序允许内联当前线程是否为ThreadPool线程,如果是线程是之前排队任务的线程。
请注意,这里没有任意的重入,因为默认调度程序在等待任务时不会抽取任意线程......它只允许内联任务,当然还有任何内联任务反过来决定做。另请注意,
return TryExecuteTask(task);
甚至不会在某些条件下询问调度程序,而是更愿意阻止。例如,如果传入可取消的CancellationToken,或者传入非无限超时,它将不会尝试内联,因为它可能需要任意长的时间来内联任务的执行,是全有或全无,这可能最终显着延迟取消请求或超时。总的来说,TPL试图在浪费正在进行Wait
'的线程和重用该线程之间取得相当大的平衡。这种内联对于递归分而治之的问题(例如QuickSort)非常重要,在这些问题中,您生成多个任务,然后等待它们全部完成。如果这样做没有内联,那么当你耗尽池中的所有线程以及它想要给你的任何未来线程时,你很快就会陷入僵局。与
Wait
分开,Task.Factory.StartNew调用可能(远程)可能最终执行任务然后在那里,如果正在使用的调度程序选择同步运行任务作为QueueTask调用。 .NET内置的调度程序都不会这样做,我个人认为这对调度程序来说是一个糟糕的设计,但理论上它是可行的,例如:Wait
不接受
protected override void QueueTask(Task task, bool wasPreviouslyQueued) { return TryExecuteTask(task); }
的{{1}}超载使用Task.Factory.StartNew
中的调度程序,TaskScheduler
定位TaskFactory
。这意味着如果您从排队到此神话Task.Factory
的任务中调用TaskScheduler.Current
,它也会排队到Task.Factory.StartNew
,从而导致同步执行任务的RunSynchronouslyTaskScheduler
调用。如果您对此感到担忧(例如,您正在实施一个库而您不知道将从何处调用),则可以明确地将RunSynchronouslyTaskScheduler
传递给StartNew
致电,使用TaskScheduler.Default
(始终转到StartNew
),或使用Task.Run
创建定位TaskScheduler.Default
。
TaskFactory
示例输出:
TaskScheduler.Default
正如您所看到的,很多时候重用等待线程来执行新任务。即使线程已获得锁定,也可能发生这种情况。讨厌的重新入侵。我感到非常震惊和担心:(
答案 1 :(得分:4)
为什么不设计它,而不是向后弯曲以确保它不会发生?
TPL在这里是一个红色的鲱鱼,只要你可以创建一个循环就可以在任何代码中进行重入,并且你不确定你的堆栈帧的“南方”会发生什么。同步重入是最好的结果 - 至少你不能自我陷入困境(很容易)。
锁定管理跨线程同步。它们与管理重入是正交的。除非您正在保护真正的单一用途资源(可能是物理设备,在这种情况下您可能应该使用队列),为什么不只是确保您的实例状态是一致的,因此重入可以“正常工作”。
(Side想:Semaphores是否可以折返而不会减少?)
答案 2 :(得分:0)
您可以通过编写在线程/任务之间共享套接字连接的快速应用程序来轻松测试。
任务将在向套接字发送消息并等待响应之前获取锁定。一旦这个阻塞并变为空闲(IOBlock),在同一个块中设置另一个任务来做同样的事情。它应该阻止获取锁,如果没有,第二个任务被允许通过锁,因为它由同一个线程运行,那么你有问题。
答案 3 :(得分:0)
Erwin提出的new CancellationToken()
解决方案对我不起作用,无论如何都发生了内联。
所以我最后使用Jon和Stephen建议的另一个条件
(... 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;
注意:为简单起见省略了异常处理等,您应该注意生产代码中的那些。