Question

I'm trying to understand the differences between Parralel.For and ThreadPool.QueueUserWorkItem.

Hardware & Software:

  • Intel i5 (quad core)
  • Windows 7 64bit prof
  • DotNet 4.5

Case 1 code: ThreadPool

for (int index = 0; index < 5; index++)
{
  ThreadPool.QueueUserWorkItem((indexParam) =>
  {
    int threadID = Thread.CurrentThread.ManagedThreadId;
    Thread.Sleep(1000);
    BeginInvoke((Action)delegate { listBox1.Items.Add("Completed " + indexParam.ToString() + " using thread " + threadID.ToString() + "  (" + DateTime.Now.Second.ToString() + "." + DateTime.Now.Millisecond.ToString("000") + ")"); });
  }, index);
}

Output:

Completed 0 using thread 10 (45.871)
Completed 1 using thread 11 (45.875)
Completed 2 using thread 12 (45.875)
Completed 3 using thread 13 (45.875)
Completed 4 using thread 10 (46.869)

Case 2 code: Parallel.For

  ParallelLoopResult result = Parallel.For(0, 5, (int index, ParallelLoopState loopState) =>
  {
    int threadID = Thread.CurrentThread.ManagedThreadId;
    Thread.Sleep(1000);
    BeginInvoke((Action)delegate { listBox1.Items.Add("Completed " + index.ToString() + " using thread " + threadID.ToString() + "  (" + DateTime.Now.Second.ToString() + "." + DateTime.Now.Millisecond.ToString("000") + ")"); });
  });

Output:

Completed 0 using thread 10 (16.923)
Completed 1 using thread 11 (16.925)
Completed 2 using thread 12 (16.925)
Completed 3 using thread 13 (16.926)
Completed 4 using thread 14 (16.926)

Question:

From the results in case 1, it appears that only four threads are active, after which the first free thread is then used to complete the final task. In case 2, it appears five threads are immediately dedicated and execute 'simultaneously'.

Why does the thread handling for QueueUserWorkItem not use a fifth thread, like the parallel class?

(ThreadPool.GetAvailableThreads(...) confirms that 1023 worker threads are available).

Was it helpful?

Solution 3

http://msdn.microsoft.com/en-us/library/system.threading.threadpool.setminthreads%28v=vs.110%29.aspx

In this case, the ThreadPool min worker threads default is 4 - which explains why only 4 threads are used. The documentation states that ThreadPool may decide whether or not to create more threads once the minimum is reached.

When setting the minimum number of worker threads to 5, then 5 threads are created and all tasks complete simultaneously as expected.

OTHER TIPS

All parallel tasks are finished in multiple threads, that means, thread is a basic unit for parallel tasks. So, I think thread pool is more efficient than TPL. WHY? BECAUSE TPL's default task scheduler is ThreadPoolTaskScheduler:

private static readonly TaskScheduler s_defaultTaskScheduler = new ThreadPoolTaskScheduler();

and let's see ThreadPoolTaskScheduler:

    protected internal override void QueueTask(Task task)
    {
        if ((task.Options & TaskCreationOptions.LongRunning) != TaskCreationOptions.None)
        {
            new Thread(ThreadPoolTaskScheduler.s_longRunningThreadWork)
            {
                IsBackground = true
            }.Start(task);
            return;
        }
        bool forceGlobal = (task.Options & TaskCreationOptions.PreferFairness) != TaskCreationOptions.None;
        ThreadPool.UnsafeQueueCustomWorkItem(task, forceGlobal);
    }

Then, let's see threadpool:

internal static void UnsafeQueueCustomWorkItem(IThreadPoolWorkItem workItem, bool forceGlobal)
{
    ThreadPool.EnsureVMInitialized();
    try
    {
    }
    finally
    {
        ThreadPoolGlobals.workQueue.Enqueue(workItem, forceGlobal);
    }
}

OK...let's see our other choice:

public static bool UnsafeQueueUserWorkItem(WaitCallback callBack, object state)
{
    StackCrawlMark stackCrawlMark = StackCrawlMark.LookForMyCaller;
    return ThreadPool.QueueUserWorkItemHelper(callBack, state, ref stackCrawlMark, false);
}

OK..let's dig more:

private static bool QueueUserWorkItemHelper(WaitCallback callBack, object state, ref StackCrawlMark stackMark, bool compressStack)
{
    bool result = true;
    if (callBack != null)
    {
        ThreadPool.EnsureVMInitialized();
        try
        {
            return result;
        }
        finally
        {
            QueueUserWorkItemCallback callback = new QueueUserWorkItemCallback(callBack, state, compressStack, ref stackMark);
            ThreadPoolGlobals.workQueue.Enqueue(callback, true);
            result = true;
        }
    }
    throw new ArgumentNullException("WaitCallback");
}

Now, finally the same point we found. So, which is better, it's your choice.

That's why I never use TPL but instead user threadpool directly.

I believe there is a difference between an "active" thread and an "available" thread. The threadpool will re-use threads, and if it decides it needs more threads it will begin to move available threads to active threads. This can be frustrating if you want to start many things at once, as each available thread may take around 2 seconds to start up.

MSDN Managed Threadpool

As part of its thread management strategy, the thread pool delays before creating threads. Therefore, when a number of tasks are queued in a short period of time, there can be a significant delay before all the tasks are started.

If you were to run these tasks repeatedly, or add tasks, you should see additional threads become active.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top