ThreadPool: thread creation throttling

MichaelMuegelMichaelMuegel Michael MuegelUS

With Task.Factory.StartNew() or ThreadPool.QueueUserWorkItem(), there is an up to 500 msec delay between the time that the previous task thread was started and the next task will start.

For example, on MonoTouch:

for (int n = 1; n <= 5; n++)
{
    int worker = n;
    ThreadPool.QueueUserWorkItem(delegate { 
        Log("Worker #" + worker + " running");
        Thread.Sleep(2000);
       });
}

Yields:

20.57.11.828: Thread 1: Queueing tasks ...
20.57.11.832: Thread 1: Done queueing tasks
20.57.11.835: Thread 6: Worker #1 running
20.57.12.306: Thread 9: Worker #2 running  <-- ~500 msec wait
20.57.12.844: Thread 10: Worker #3 running <-- ~500 msec wait
20.57.13.345: Thread 11: Worker #4 running <-- ~500 msec wait
20.57.13.840: Thread 6: Worker #5 running  <-- ~500 msec wait

All timing is on iPad3. Similar issue on Mono for Mac.

Why is this very long delay between thread creates present? Shouldn't this at least be tunable?

Compare this to just spining up some threads manually:

 Log("Queueing tasks ...");
 for (int n = 1; n <= 5; n++)
 {
     int worker = n;
     ThreadPool.QueueUserWorkItem(delegate { 
        Log("Worker #" + worker + " running");
        Thread.Sleep(2000);
     });
  }
  Log("Done queueing tasks");

Shows minimal overhead:

20.50.15.468: Thread 1: Queueing tasks ...
20.50.15.493: Thread 1: Done queueing tasks
20.50.15.499: Thread 11: Worker #3 running
20.50.15.501: Thread 9: Worker #1 running
20.50.15.501: Thread 10: Worker #2 running
20.50.15.502: Thread 12: Worker #4 running
20.50.15.503: Thread 13: Worker #5 running

I was using a custom max concurrency TaskScheduler, but the start-up under Mono is just so slow I will look for alternatives.

If I am not off target here, is this being addressed or already addressed in future Mono/MonoTouch/etc?

Posts

  • MichaelMuegelMichaelMuegel Michael Muegel US

    Oops, bad paste for second C# code example. Here was the code used to create threads manually.

    for (int n = 1; n <= 5; n++)
    {
        int worker = n;
        var thread = new Thread(new ThreadStart(delegate { 
            Log("Worker #" + worker + " running");
            Thread.Sleep(2000);
         }));
         thread.Start();
     }
     Log("Done queueing tasks");           
    
  • MichaelMuegelMichaelMuegel Michael Muegel US

    Regarding ThreadPool alternatives, there are probably hundreds out there. I took a look at:

    The first two look nice but overkill for my needs. Arcond is simple and therefore easy to support. I did add a few members:

    public int NumPendingTasks
    {
        get { return _tasks.Count; }
    }
    
    /// <summary>
    /// True when there are pending tasks or tasks are being
    /// run in a thread.
    /// </summary>
    public bool IsBusy
    {
        get { return NumPendingTasks > 0 || RunningThreads > 0; }
    }
    

    Some test code:

    public static class TestArcondThreadPool
    {
        private static Stopwatch Stopwatch = new Stopwatch();
    
        public static void Run()
        {
            Stopwatch.Start();
            Log("Creating thread pool");
            var tpool = new ArcondThreadPool(4);
            var random = new Random();
    
            Log("Starting tasks ...");
            for (int n = 1; n <= 10; n++)
            {
                int worker = n;
                tpool.Enqueue(new ThreadStart(delegate {
                    int duration = random.Next(1000,2000);
                    Log("Starting worker #" + worker + " with duration " + duration);
                    Thread.Sleep(duration);
                    Log("Worker #" + worker + " exiting");
                }));
            }
    
            Log("Waiting for tasks to finish ...");
            while (tpool.IsBusy)
            {
                Thread.Sleep(1);
            }
            Log("All tasks done");
        }
    
        private static void Log(string msg)
        {
            Console.WriteLine("+{0:00000}: Thread {1}: {2}", Stopwatch.ElapsedMilliseconds, 
                Thread.CurrentThread.ManagedThreadId, msg);
        }
    }
    

    Running on iPad3 (elapsed milliseconds is first field):

    +00000: Thread 1: Creating thread pool
    +00004: Thread 1: Starting tasks ...
    +00021: Thread 1: Waiting for tasks to finish ...
    +00033: Thread 23: Starting worker #1 with duration 1353
    +00045: Thread 24: Starting worker #2 with duration 1881
    +00054: Thread 25: Starting worker #3 with duration 1720
    +00073: Thread 26: Starting worker #4 with duration 1512
    +01427: Thread 23: Worker #1 exiting
    +01442: Thread 27: Starting worker #5 with duration 1101
    +01606: Thread 26: Worker #4 exiting
    +01616: Thread 28: Starting worker #6 with duration 1958
    +01804: Thread 25: Worker #3 exiting
    +01814: Thread 29: Starting worker #7 with duration 1166
    +01948: Thread 24: Worker #2 exiting
    +01960: Thread 30: Starting worker #8 with duration 1347
    +02578: Thread 27: Worker #5 exiting
    +02587: Thread 31: Starting worker #9 with duration 1385
    +03025: Thread 29: Worker #7 exiting
    +03038: Thread 32: Starting worker #10 with duration 1791
    +03343: Thread 30: Worker #8 exiting
    +03597: Thread 28: Worker #6 exiting
    +04012: Thread 31: Worker #9 exiting
    +04870: Thread 32: Worker #10 exiting
    +04882: Thread 1: All tasks done
    
  • MichaelMuegelMichaelMuegel Michael Muegel US

    Arcond does have a very expensive monitor thread though. Ah, to tweak or go elsewhere ...

  • MichaelMuegelMichaelMuegel Michael Muegel US

    I switched over to Smart Thread Pool in my app. It's monitor thread is very well behaved (verified via Instruments). I compiled in WINDOWS_PHONE mode.

    And unlike Arcond, this is a true thread pool: threads are reused from task-to-task, with a whole bunch of options to control the thread management, including shutdown of idle threads after a set time.

    Similar test code under Smart Thread Pool:

    public static class TestSmartThreadPool
    {
        private static Stopwatch Stopwatch = new Stopwatch();
    
        public static void Run()
        {
            Stopwatch.Start();
            Log("Creating thread pool");
            var stpool = new SmartThreadPool { MaxThreads = 4 };
            var random = new Random();
    
            Log("Starting tasks ...");
            for (int n = 1; n <= 10; n++)
            {
                int worker = n;
                stpool.QueueWorkItem(delegate {
                    int duration = random.Next(1000,2000);
                    Log("Starting worker #" + worker + " with duration " + duration);
                    Thread.Sleep(duration);
                    Log("Worker #" + worker + " exiting");
                });
            }
    
            Log("Waiting for tasks to finish ...");
            stpool.WaitForIdle();
            Log("All tasks done");
        }
    
        private static void Log(string msg)
        {
            Console.WriteLine("+{0:00000}: Thread {1}: {2}", Stopwatch.ElapsedMilliseconds, 
                Thread.CurrentThread.ManagedThreadId, msg);
        }
    }
    

    Output:

    +00000: Thread 1: Creating thread pool
    +00004: Thread 1: Starting tasks ...
    +00039: Thread 1: Waiting for tasks to finish ...
    +00069: Thread 26: Starting worker #2 with duration 1873
    +00071: Thread 24: Starting worker #3 with duration 1027
    +00072: Thread 23: Starting worker #1 with duration 1134
    +00072: Thread 25: Starting worker #4 with duration 1246
    +01144: Thread 24: Worker #3 exiting
    +01150: Thread 24: Starting worker #5 with duration 1821
    +01241: Thread 23: Worker #1 exiting
    +01243: Thread 23: Starting worker #6 with duration 1287
    +01367: Thread 25: Worker #4 exiting
    +01369: Thread 25: Starting worker #7 with duration 1215
    +01988: Thread 26: Worker #2 exiting
    +01990: Thread 26: Starting worker #8 with duration 1887
    +02574: Thread 23: Worker #6 exiting
    +02576: Thread 23: Starting worker #9 with duration 1780
    +02627: Thread 25: Worker #7 exiting
    +02629: Thread 25: Starting worker #10 with duration 1981
    +03014: Thread 24: Worker #5 exiting
    +03894: Thread 26: Worker #8 exiting
    +04393: Thread 23: Worker #9 exiting
    +04643: Thread 25: Worker #10 exiting
    +04646: Thread 1: All tasks done
    
  • NicWiseNicWise Nic Wise NZInsider ✭✭✭

    No answer, sorry - but I find it interesting that the threadpool one always does them in order, and the non-thread pool one often does them out of order.

    No idea WHY, but interesting none the less :)

  • DavidLambert.7023DavidLambert.7023 David Lambert USMember ✭✭

    Hi @MichaelMuegel, I also found out that the ThreadPool was probably spending a lot of time creating new threads at inconvenient moments during execution. Doubling the ThreadPool's Mininum Threads Count fixed the problem.

Sign In or Register to comment.