In my Parallel Programming introduction post I explored how to easily get performance gains when running loop code by using the TParallel.For() loop construct. A key part of the Parallel Programming Library engine is the new ThreadPool that manages some of the complexity behind the scenes when using this syntax, but more on that later.
Following on from this first post I want to explore a common question I have heard. Is it possible to manage the Parallel Programming library thread pool Size? and if so how?
In short Yes, but I want to pose another question: Should you? – Lets explore this below.
Parallel programming thread pool
The Parallel programming thread pool is very smart! It automatically grows and shrinks based on CPU demand when your application runs and requires its use; it also throttles growth as your CPU usage rises ensuring it doesn’t over cook your CPU and ensuring you don’t lock up your machine. This inbuilt intelligence makes it very efficient and courteous out the box. All of this, without ANY management from us developers! 🙂 So why would you want to change this?
Thats not to say you can’t use a custom(ised) pool. If you do want to limit the size of a pool then you can override the defaults of a TThreadPool.
TThreadPool Defaults
Defined in System.Threading, TThreadPool initiates with a default of 25 threads per CPU.
MaxThreadsPerCPU = 25;
TThreadPool multiplies the MaxThreadsPerCPU with the number of CPU’s on the machine (it gets this form calling TThread.ProcessorCount) and exposes the result via a read only property TThreadPool.MaxWorkerThreads.
To query the default pool size on your machine at runtime you can use TThreadPool’s handy class method that returns the default pool. With this pool you can then query the MaxWorkerThreads. e.g.
var i : integer; begin i := TThreadPool.Default.MaxWorkerThreads; ShowMessage('Pool size = '+i.ToString()); end;
On my Windows VM running 2 cores I see 50, but on my Mac OS X with 8 cores, this code returns 200.
Customising a TThreadPool
Let me start this section by saying, modifying the default TThreadPool properties is not recommended.
While possible, it is not recommended to modify the Default TThreadPool options as this is a global instance that is used throughout the application, and you never can be sure where and when its being used. You can however create your own instance of a TThreadPool that you modify and use and this is a better approach.
Creating and modifiying a TThreadPool
Creating a TThreadPool is as simple as declaring the variable and calling the constructor.
With an instance of a TThreadPool, you can then modify the MaxWorkerThreads by overriding the value using the method SetMaxWorkerThreads() which takes in an Integer. This sets it at a flat number regardless of the number of CPU’s you have available. e.g. the following code reports 10 as the Max size on both my Windows and Mac OSX machines mentioned above.
var FPool : TThreadPool; ... if Pool = nil then begin Pool := TThreadPool.Create; Pool.SetMaxWorkerThreads(10); end;
Note, the MaxWorkerThreads number must always be greater than the MinimumWorkerThreads value that (by default) is set from TThread.ProcessorCount.
A word of caution..
Creating a new TThreadPool come with an overhead. From an initial test where I creating a new TThreadPool for running a small TParallel.For() loop, and then disposing it afterwards it actually decrease performance on your application compared to a traditional for loop. For this reason alone, I would always use a global TThreadPool. When the pool was created globally, the speed performance was immediately backup compared to the create and destroy on demand idea.
Example of using a custom TThreadPool
Below is an example where Pool is a global TThreadPool. When the button is selected to run a TThreadPool with a maximum of 10 WorkerThreads. The only adjustments from the example in the previous tutorials is that we now pass in Pool as a paramater to the For loop, note however that this is not being created and free’ed each time in this code.
var
Pool: TThreadPool;
procedure TForm5.Button1Click(Sender: TObject);
var
Tot: Integer;
SW: TStopwatch;
begin
// counts the prime numbers below a given value
Tot := 0;
SW := TStopWatch.Create;
SW.Start;
if Pool = nil then begin
Pool := TThreadPool.Create;
Pool.SetMaxWorkerThreads(10);
end;
TParallel.For(1, Max, procedure (I: Integer)
begin
if IsPrime (I) then
TInterlocked.Increment (Tot);
end,Pool);
SW.Stop;
Memo1.Lines.Add (Format (
'Parallel (Custom Pool) for loop: %d - %d', [SW.ElapsedMilliseconds, Tot]));
end;
I would say that while this gives me a sense of control, I actually don’t like the fact that I am messing with something that is highly tuned. I would personally conclude that a ThreadPool should be created as the application has initialised and that you use this. Ideally I would say use the default one, as it behaviour is very good already, but if you really want to make more work for yourself, then you can always set the properties of a new pool and use it.
A thank you to Allen Bauer for his input while writing this post.