Is Task.Factory.StartNew () a guarantee to use a different thread than the calling thread?

I am starting a new task from a function, but I do not want it to run in the same thread. I don’t care what kind of stream it works, while it is different (therefore, the information given in this question does not help).

I'm sure that the code below will always exit TestLock before letting Task t re-enter it? If not, what is the recommended design pattern to prevent reinstallation?

 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 following answer by John Skeet and Stephen Tub, a simple way to deterministically prevent re-participation is to pass a CancellationToken, as shown in this extension method:

 public static Task StartNewOnDifferentThread(this TaskFactory taskFactory, Action action) { return taskFactory.StartNew(action: action, cancellationToken: new CancellationToken()); } 
+70
multithreading c # locking task-parallel-library task
Sep 03 '12 at 10:19
source share
4 answers

I posted Stephen Toub - a member of the PFX Team - about this issue. He comes back to me very quickly, with a lot of details, so I just copy and paste his text here. I haven’t quoted all of this, since reading a large amount of quoted text ends up becoming less convenient than vanilla black and white, but actually it’s Steven - I don’t know this much :) I made this answer from the community wiki to reflect that all the goodness below is not my content.

If you call Wait() in the completed task, there will be no lock (it simply throws an exception if the task completed in a state other than RanToCompletion , or otherwise returns as nop) If you call Wait() in the task that is already running, it should be blocked, because there is nothing that could reasonably be done (when I say “block”, I include both true expectation and kernel-based hangs, as it is usually a mixture of both). Similarly, if you call Wait() in a Task that is in the Created or WaitingForActivation , it blocks until the task completes. None of these are the interesting issues being discussed.

An interesting case is that you call Wait() in the Task in the WaitingToRun state, which means that it was previously queued for the TaskScheduler, but the TaskScheduler has not yet reached the actual execution of the Task delegate yet. In this case, the Wait call will ask the scheduler whether it is normal to run Task, and then in the current thread, by calling the scheduler method TryExecuteTaskInline . The scheduler can choose to either start the task then and there using the base.TryExecuteTask call, or it can return false to indicate that it is not performing the task (often this is done using logic like return SomeSchedulerSpecificCondition() ? false : TryExecuteTask(task); .. The reason TryExecuteTask returns a Boolean is because it handles the synchronization to ensure that the task runs only once). Thus, if the scheduler wants to completely prohibit nesting tasks while waiting, it can simply be implemented as return false; If the scheduler wants to always allow embedding when possible, it can simply be implemented as return TryExecuteTask(task); In the current implementation (both .NET 4 and .NET 4.5, and I personally do not expect this to change), the default scheduler that targets ThreadPool allows embedding if the current thread is a ThreadPool thread, and if that thread was those who previously put the task in line

Please note that there is no arbitrary re-inclusion here, since the default scheduler will not pump up arbitrary threads when waiting for a task ... it will only allow this task to be built-in and, of course, it decides to do any insertion of this task. Also note that Wait does not even ask the scheduler in certain conditions, instead prefers to block. For example, if you pass a canceled CancellationToken, or if you pass an infinite timeout, it will not try to embed it, because it may take as long as it takes to complete a task that is all or nothing, and that can lead to a significant delay in the cancellation request or timeout . All in all, TPL is trying to strike a decent balance between wasting a thread that is waiting and reusing that thread too much. Such an insertion is really important for recursive separation and victory problems, for example. QuickSort, where you run several tasks, and then wait for them to finish ... if it were done without inlay, you would very quickly get stuck when you ran out of all the threads in the pool and any future ones that he wanted to give you.

Apart from Wait, it is also possible (remotely) that a call to Task.Factory.StartNew can complete the task then and there if the scheduled scheduler decides to execute the task synchronously as part of the QueueTask call. None of the schedulers built into .NET will ever do this, and I personally think that this will be a bad design for the scheduler, but it is theoretically possible, for example. protected override void QueueTask(Task task, bool wasPreviouslyQueued) { return TryExecuteTask(task); } protected override void QueueTask(Task task, bool wasPreviouslyQueued) { return TryExecuteTask(task); } . Overloading Task.Factory.StartNew , which does not accept TaskScheduler, uses the scheduler from TaskFactory, which in the case of Task.Factory targets TaskScheduler.Current. This means that if you call Task.Factory.StartNew from a task queued for this mythical RunSynchronouslyTaskScheduler, it will also be queued for RunSynchronouslyTaskScheduler, as a result of which StartNew will execute the task synchronously. If you are generally worried about this (for example, you are implementing a library, and you do not know where you will be called from), you can explicitly pass TaskScheduler.Default to a StartNew call, use Task.Run (which always goes to TaskScheduler.Default) or use TaskFactory created for the target TaskScheduler.Default.




EDIT: Well, it looks like I was completely wrong, and the thread that is currently waiting for the job can be captured. Here is a simpler example of this:

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

Output Example:

 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 many times when the wait thread is reused to perform a new task. This can happen even if the thread has acquired a lock. Unpleasant re-intervention. I'm shocked and worried enough :(

+78
Sep 03 '12 at 10:26
source share

Why not just design it and not lean back so that this does not happen?

TPL is a red herring, re-intervention can happen in any code if you can create a loop and you don't know exactly what the "south" of your stack frame will happen. Synchronized re-enable is the best result here - at least you can't stall on your own (how easy).

Locks control the synchronization of cross streams. They are orthogonal to the management of reraction. If you are not protecting a one-use genuine resource (perhaps a physical device, in which case you should probably use a queue), why not just make sure that the state of your instance is consistent, so residency can just work.

(Sid thoughts: semaphores are reentrant without decrement?)

+4
Sep 04
source share

You can easily verify this by writing a quick application that shares a socket connection between threads / tasks.

The task will receive a lock before sending a message to the socket and waiting for a response. As soon as it is blocked and becomes inactive (IOBlock), the same thing is done to perform another task in the same block. It should block the receipt of the lock, if it is not, and the second task is allowed to skip the lock, because it is triggered by the same thread, then you have a problem.

0
Sep 04 2018-12-12T00: 00Z
source share

The solution with new CancellationToken() , proposed by Erwin, did not work for me, in any case there was an inlining.

So, I ended up using another condition recommended by John and 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; 

Note: Omit exception handling, etc. here for simplicity, you should remember that in the production code.

0
Sep 01 '16 at 7:09
source share



All Articles