84

What is difference between the below

ThreadPool.QueueUserWorkItem

vs

Task.Factory.StartNew

If the above code is called 500 times for some long running task, does it mean all the thread pool threads will be taken up?

Or will TPL (2nd option) be smart enough to just take up threads less or equal to number of processors?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
stackoverflowuser
  • 22,212
  • 29
  • 67
  • 92

2 Answers2

95

If you're going to start a long-running task with TPL, you should specify TaskCreationOptions.LongRunning, which will mean it doesn't schedule it on the thread-pool. (EDIT: As noted in comments, this is a scheduler-specific decision, and isn't a hard and fast guarantee, but I'd hope that any sensible production scheduler would avoid scheduling long-running tasks on a thread pool.)

You definitely shouldn't schedule a large number of long-running tasks on the thread pool yourself. I believe that these days the default size of the thread pool is pretty large (because it's often abused in this way) but fundamentally it shouldn't be used like this.

The point of the thread pool is to avoid short tasks taking a large hit from creating a new thread, compared with the time they're actually running. If the task will be running for a long time, the impact of creating a new thread will be relatively small anyway - and you don't want to end up potentially running out of thread pool threads. (It's less likely now, but I did experience it on earlier versions of .NET.)

Personally if I had the option, I'd definitely use TPL on the grounds that the Task API is pretty nice - but do remember to tell TPL that you expect the task to run for a long time.

EDIT: As noted in comments, see also the PFX team's blog post on choosing between the TPL and the thread pool:

In conclusion, I’ll reiterate what the CLR team’s ThreadPool developer has already stated:

Task is now the preferred way to queue work to the thread pool.

EDIT: Also from comments, don't forget that TPL allows you to use custom schedulers, if you really want to...

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 4
    I'm wary of the hard fast rule that `TaskCreationOptions.LongRunning` will always avoid the thread-pool. It seems to be more of a directive than an implementation guarantee. Am I off-base on that? – Marc Feb 08 '12 at 20:14
  • 2
    @Marc: Well, it's up to the scheduler - but it would be a pretty crazy scheduler to schedule explicitly long-running tasks on the thread pool, IMO. – Jon Skeet Feb 08 '12 at 20:18
  • Just to add a little more info - http://blogs.msdn.com/b/pfxteam/archive/2009/10/06/9903475.aspx – Brad Semrad Feb 08 '12 at 21:06
  • @Brad: Thanks, will add a link to my answer. – Jon Skeet Feb 08 '12 at 21:30
  • 1
    I'd also add that TPL allows you to specify your own scheduler, including custom schedulers that allow you to control your own concurrency: http://msdn.microsoft.com/en-us/library/ee789351.aspx – Chris Shain Feb 08 '12 at 21:34
  • "I'd hope that ... would avoid" - actually there's no immediate problem in using a pool thread as long as the scheduler + TP 'know' it won't return quickly. – H H Feb 08 '12 at 21:41
  • @HenkHolterman: Well, it depends - if that means "we won't really pool it, so we'll create as many threads as we like" then I'd argue it's not really being a thread pool - and if it *does* pool them against the limit, then there is an immediate problem, IMO. – Jon Skeet Feb 08 '12 at 21:49
  • I remember (but don't have a link) that the TPL will take the LongRunning hint into consideration when deciding to create extra threads or not. Why manage 2 kinds when 1 will do? – H H Feb 08 '12 at 21:57
  • @HenkHolterman: Because the threads for long running tasks basically wouldn't be considered part of the thread pool at all - I'd expect it to be a new thread, which isn't recycled after termination. – Jon Skeet Feb 08 '12 at 22:17
  • @JonSkeet is there any drawbacks to replacing QueueUserWorkItem with Task.Factory.StartNew? – theB3RV Jul 01 '14 at 14:01
  • @theB3RV: I don't know, to be honest - I don't know of any, but there may be *some* corner cases. – Jon Skeet Jul 01 '14 at 14:04
  • @theB3RV Task.Factory.StartNew can execute on the thread that is calling it, which can cause issues with reentrancy https://stackoverflow.com/questions/12245935/is-task-factory-startnew-guaranteed-to-use-another-thread-than-the-calling-thr – ghord Jan 31 '17 at 13:19
1

No, there is no extra cleverness added in the way the ThreadPool threads are utilized, by using the Task.Factory.StartNew method (or the more modern Task.Run method). Calling Task.Factory.StartNew 500 times (with long running tasks) is certainly going to saturate the ThreadPool, and will keep it saturated for a long time. Which is not a good situation to have, because a saturated ThreadPool affects negatively any other independent callbacks, timer events, async continuations etc that may also be active during this 500-launched-tasks period.

The Task.Factory.StartNew method schedules the execution of the supplied Action on the TaskScheduler.Current, which by default is the TaskScheduler.Default, which is the internal ThreadPoolTaskScheduler class. Here is the implementation of the ThreadPoolTaskScheduler.QueueTask method:

protected internal override void QueueTask(Task task)
{
    if ((task.Options & TaskCreationOptions.LongRunning) != 0)
    {
        // Run LongRunning tasks on their own dedicated thread.
        Thread thread = new Thread(s_longRunningThreadWork);
        thread.IsBackground = true; // Keep this thread from blocking process shutdown
        thread.Start(task);
    }
    else
    {
        // Normal handling for non-LongRunning tasks.
        bool forceToGlobalQueue = ((task.Options & TaskCreationOptions.PreferFairness) != 0);
        ThreadPool.UnsafeQueueCustomWorkItem(task, forceToGlobalQueue);
    }
}

As you can see the execution of the task is scheduled on the ThreadPool anyway. The ThreadPool.UnsafeQueueCustomWorkItem is an internal method of the ThreadPool class, and has some nuances (bool forceGlobal) that are not publicly exposed. But there is nothing in it that changes the behavior of the ThreadPool when it becomes saturated¹. This behavior, currently, is not particularly sophisticated either. The thread-injection algorithm just injects one new thread in the pool every 1000 msec, until the saturation incident ends.

¹ The ThreadPool is said to be saturated when the demand for work surpasses the current availability of threads, and the threshold SetMinThreads above which new threads are no longer created on demand has been reached.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • I don't get it.. you posted the correct snippet wich shows that if you pass LongRunning there is no thread pool thread used. But you say the opposit in you answer? – Manuel Amstutz Feb 22 '22 at 16:21
  • @ManuelAmstutz the OP has not mentioned the `TaskCreationOptions.LongRunning` configuration in the question, so I assume that they are asking about the `Task.Factory.StartNew` when it's used in its simplest form: `var task = Task.Factory.StartNew(() => /* ... */);`. This is the most common usage pattern of this method. Or at least this is my impression, after having been exposed to hundreds of TPL-related questions in this site. – Theodor Zoulias Feb 22 '22 at 17:01