78

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());
}
Community
  • 1
  • 1
Erwin Mayer
  • 18,076
  • 9
  • 88
  • 126
  • I doubt you can guarantee that a new thread will be created when calling `StartNew`. A task is defined as an *asynchronous* operation, which doesn't necessarily imply a new thread. May use an existing thread somewhere too, or another way of doing async. – Tony The Lion Sep 03 '12 at 10:24
  • If you're using C# 5, consider replacing `t.Wait()` with `await t`. `Wait` doesn't really fit with the philosophy of TPL. – CodesInChaos Sep 03 '12 at 10:30
  • In the code given, the most efficient behaviour would be if it **did** use the calling thread. That thread is sitting there doing nothing after all. – Jon Hanna Sep 03 '12 at 10:37
  • 2
    Yes, this is true, and I wouldn't mind in this specific case. But I prefer a deterministic behavior. – Erwin Mayer Sep 03 '12 at 10:38
  • 2
    If you use a scheduler that only runs tasks on a specific thread then no, the task *can't* be run on a different thread. It's quite common to use a `SynchronizationContext` to make sure tasks are run on the UI thread. If you ran the code that called `StartNew` on the UI thread like that, then they would both run on the same thread. The only guarantee is that the task will be run asynchronously from the `StartNew` call (at least if you don't provide a `RunSynchronously` flag. – Peter Ritchie Sep 03 '12 at 14:27
  • 8
    If you want to "force" a new thread to be created, use the 'TaskCreationOptions.LongRunning' flag. e.g.: `Task.Factory.StartNew(() => { this.Test(stop: true); }, TaskCreationOptions.LongRunning);` Which is a good idea if your locks could put the thread into a wait state for an extended period of time. – Peter Ritchie Sep 03 '12 at 20:33
  • Erwin, if you really want to run on separate threads won't it be better to actually create and use threads explicitly? – stic Sep 04 '12 at 10:53
  • @stic: The only requirement is to run on another thread, so it would be overkill as the ThreadPool (especially via the Task library) is well able to handle that properly. – Erwin Mayer Sep 04 '12 at 11:46
  • Why is the 'if(stop) return' check within the lock here? That bool is local, no need to protect on read (surely) – piers7 Sep 04 '12 at 13:04
  • @piers7: The idea was to prevent exiting the function (which is just a sample action) until the lock can be aquired. In a real situation you'd be right to move this statement before the lock. – Erwin Mayer Sep 04 '12 at 16:32
  • @PeterRitchie 's solution worked for me :) – Nikos Baxevanis Sep 05 '12 at 16:24
  • LongRunningTask does NOT guarantee a new thread, just provides a hint to TPL. A new thread might be used, but are no guarantees. As ilustrated by the correct answer, only CancellationToken does that – Ricardo Rodrigues Nov 04 '13 at 12:24

4 Answers4

84

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

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

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

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 :(

Glenn Slayden
  • 17,543
  • 3
  • 114
  • 108
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    In the case of a single available thread, his code would deadlock because "after the code using that thread already has finished with it" never happens. – CodesInChaos Sep 03 '12 at 10:28
  • What about task inlining? The task can “hijack” the thread when it calls `Wait()`, no? – svick Sep 03 '12 at 10:33
  • @svick: I don't *think* so. I would certainly be surprised if it did so, and I'd consider that to be broken. I think the PFX team is sufficiently wary of reentrancy to stop that :) – Jon Skeet Sep 03 '12 at 11:36
  • @JonSkeet I was focusing on the part of the question that is about the same thread and somehow ignored the part about the lock. But [my test](https://gist.github.com/3609385) shows that task inlining does what I thought: when you call `Wait()` the task can “hijack” the current thread, so the second call will run on the same thread, before the first one finishes. But since the call to `Wait()` is outside the lock, you are guaranteed that the second call won't run on the same thread while the first call still holds the lock. – svick Sep 03 '12 at 13:47
  • @svick: It's hard for me to to follow your test, as it's got two different calls to StartNew, both starting the same kind of task. I think I'll have a look tonight to see what's going on, but if you're easily able to refactor your test, that would be helpful... – Jon Skeet Sep 03 '12 at 13:55
  • @JonSkeet Well, I just took the code from this question (which already does that weird recursion), added some logging and started it on the ThreadPool. – svick Sep 03 '12 at 13:58
  • @svick Thanks for your test; if t.Wait() is outside the lock the task will run on the same thread, but if t.Wait() is inside the lock another thread will be used and as expected the program will deadlock. – Erwin Mayer Sep 03 '12 at 16:14
  • Actually, I tested several other times and the behavior is not consistent; it will sometimes use the same thread even if t.Wait() is inside the lock. Argh. – Erwin Mayer Sep 03 '12 at 16:46
  • I forked your test application on git: https://gist.github.com/3610738/95a8db1be9359e31e11d1884e984fdd36d980794 – Erwin Mayer Sep 03 '12 at 16:55
  • @ErwinMayer: Thanks for that - I've edited my answer to show a simpler (IMO) example which demonstrates the problem really easily, without any locking involved. – Jon Skeet Sep 03 '12 at 19:51
  • Calling `Wait` on a threadpool thread could inline the call to the task's delegate on the current thread. If the thread that called `Wait()` is not a threadpool thread then the invocation of the task will not occur on the same thread. – Peter Ritchie Sep 03 '12 at 20:21
  • 1
    e.g. if you "force" a new thread to be created (i.e. not a threadpool thread) then you won't see any inlining from `Wait`: `Task.Factory.StartNew(Launch, TaskCreationOptions.LongRunning).Wait()` – Peter Ritchie Sep 03 '12 at 20:34
  • Why would this re-entrancy be so bad? I think the only case where it would be different than normal multithreading is if you were using `ThreadStatic` fields, or something like that. – svick Sep 03 '12 at 21:56
  • 1
    @svick: I'm thinking of things like calling `Wait` while you own a lock. Locks are re-entrant, so if the task you're awaiting tries to acquire the lock as well, it will succeed - because it already owns the lock, even though that's logically in another task. That *ought* to deadlock, but it won't... when it's re-entrant. Basically re-entrancy makes me nervous in general; it feels like it's violating all kinds of assumptions. – Jon Skeet Sep 03 '12 at 22:51
  • @JonSkeet That is exactly my worry too... Re-entrency is not compatible with the idea of dealing with tasks. What would you recommend that could act as a reliable TaskLock? Do we have to go the ManualResetEvent route? – Erwin Mayer Sep 04 '12 at 03:49
  • @JonSkeet Thanks for your clear test program and contacting Stephen Toub about the issue! Hopefully he'll help us see the light. – Erwin Mayer Sep 04 '12 at 03:52
  • 1
    Makes me regret not to be using F# for this project to be forced to write re-entrancy-friendly code ;) – Erwin Mayer Sep 04 '12 at 03:59
  • @svick check [this Wikipedia article](http://en.wikipedia.org/wiki/Reentrancy_(computing)) on reentrancy to get a feel of why it can be painful. – Erwin Mayer Sep 04 '12 at 04:01
  • @ErwinMayer: Stephen has replied, and I'll edit his (comprehensive) mail into my answer. – Jon Skeet Sep 04 '12 at 05:23
  • @JonSkeet: Very interesting reading, thanks, though apart from implementing another Scheduler (or maybe a ManualResetEvent as I suggested earlier), there doesn't appear to be a simple way to always prevent re-entrancy... – Erwin Mayer Sep 04 '12 at 06:39
  • 1
    @ErwinMayer: You can pass a cancelable token, and just never cancel it. That looks like it would do the trick. Or pass in a timeout which is several years :) – Jon Skeet Sep 04 '12 at 06:39
  • @JonSkeet: Indeed! I missed that. – Erwin Mayer Sep 04 '12 at 06:40
  • @JonSkeet, Do you know if Task.Run() is now a safe option achieving exactly the same goal (without the need to pass a new CancellationToken each time)? – Erwin Mayer Jul 01 '14 at 19:30
  • @ErwinMayer: I'm not sure what you mean by "now a safe option". I don't know of any changes in this area, if that's what you were asking. – Jon Skeet Jul 01 '14 at 19:41
  • @JonSkeet I was wondering if using Task.Run() is guaranteed to always run the task on a different thread (never on the same thread), like what Task.Factory.StartNew(action: action, cancellationToken: new CancellationToken()) does. – Erwin Mayer Jul 02 '14 at 09:00
  • 1
    @ErwinMayer: I wouldn't like to guarantee that. I suspect it *may* be true, but never say never :) (In particular, if you're calling it *from* a task, and then that task completes, it would be entirely reasonable for the new task to run on the same thread.) – Jon Skeet Jul 02 '14 at 09:10
4

Why not just design for it, rather than bend over backwards to ensure it doesn't happen?

The TPL is a red herring here, reentrancy can happen in any code provided you can create a cycle, and you don't know for sure what's going to happen 'south' of your stack frame. Synchronous reentrancy is the best outcome here - at least you can't self-deadlock yourself (as easily).

Locks manage cross thread synchronisation. They are orthogonal to managing reentrancy. Unless you are protecting a genuine single use resource (probably a physical device, in which case you should probably use a queue), why not just ensure your instance state is consistent so reentrancy can 'just work'.

(Side thought: are Semaphores reentrant without decrementing?)

piers7
  • 4,174
  • 34
  • 47
0

You could easily test this by writting a quick app that shared a socket connection between threads / tasks.

The task would acquire a lock before sending a message down the socket and waiting for a response. Once this blocks and becomes idle (IOBlock) set another task in the same block to do the same. It should block on acquiring the lock, if it does not and the second task is allowed to pass the lock because it run by the same thread then you have an problem.

gnat
  • 6,213
  • 108
  • 53
  • 73
JonPen
  • 1
0

Solution with new CancellationToken() proposed by Erwin did not work for me, inlining happened to occur anyway.

So I ended up using another condition advised by Jon 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: Omitting exception handling etc here for simplicity, you should mind those in production code.

Vitaliy Ulantikov
  • 10,157
  • 3
  • 61
  • 54