Skip to content

ikelaiah/threadpool-fp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

77 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🚀 ThreadPool for Free Pascal

Version License: MIT Free Pascal Lazarus Supports Windows Supports Linux No Dependencies Tests Documentation Status

A lightweight, easy-to-use thread pool implementation for Free Pascal. Simplify parallel processing for simple tasks! ⚡

Important

Parallel processing can improve performance for CPU-intensive tasks that can be executed independently. However, not all tasks benefit from parallelization. See Thread Management for important considerations.

Note

This library was originally written to explore the concept of thread pools in Free Pascal. It has since grown into a stable, tested implementation suitable for simple parallel processing tasks.

It is not designed for high-load or production-scale applications. For those use cases, see the alternatives below.

Tip

If you are looking for performant and battle-tested threading libraries, please check out these alternatives:

📑 Table of Contents

✨ Features

This library provides two thread pool implementations, each with its own strengths:

ThreadPool Implementations

1. Simple Thread Pool (ThreadPool.Simple)

uses ThreadPool.Simple;
  • Global singleton instance for quick use
  • Direct task execution
  • Automatic thread count management
  • Best for simple parallel tasks
  • Lower memory overhead

2. Producer-Consumer Thread Pool (ThreadPool.ProducerConsumer)

uses ThreadPool.ProducerConsumer;

A thread pool with fixed-size circular buffer (1024 items) and built-in backpressure handling:

  • Queue Management

    • Fixed-size circular buffer for predictable memory usage
    • Efficient space reuse without dynamic resizing
    • Configurable capacity (default: 1024 items)
  • Backpressure Handling

    • Load-based adaptive delays (10ms to 100ms)
    • Automatic retry mechanism (up to 5 attempts)
    • Throws EQueueFullException when retries exhausted
  • Monitoring & Debug

    • Thread-safe error capture with thread IDs
    • Detailed debug logging (can be disabled)

Warning

While the system includes automatic retry mechanisms, it's recommended that users implement their own error handling strategies for scenarios where the queue remains full after all retry attempts.

Shared Features

  • Thread Count Management

    • Minimum 4 threads for optimal parallelism
    • Maximum 2× ProcessorCount to prevent overload
    • Fixed count after initialization
  • Task Types Support

    • Simple procedures: Pool.Queue(@MyProc)
    • Object methods: Pool.Queue(@MyObject.MyMethod)
    • Indexed variants: Pool.Queue(@MyProc, Index)
  • Thread Safety

    • Built-in synchronization
    • Safe resource sharing
    • Protected error handling
  • Error Management

    • Thread-specific error capture
    • Error messages with thread IDs
    • Continuous operation after exceptions

Note

Thread count is determined by TThread.ProcessorCount at startup and remains fixed. See Thread Management for details.

🏃 Quick Start

Simple Thread Pool

uses ThreadPool.Simple;

// Simple parallel processing
procedure ProcessItem(index: Integer);
begin
  WriteLn('Processing item: ', index);
end;

begin
  // Queue multiple items
  for i := 1 to 5 do
    GlobalThreadPool.Queue(@ProcessItem, i);
    
  GlobalThreadPool.WaitForAll;
end;

Producer-Consumer Thread Pool

uses ThreadPool.ProducerConsumer;

procedure DoWork;
begin
  WriteLn('Working in thread: ', GetCurrentThreadId);
end;

var
  Pool: TProducerConsumerThreadPool;
begin
  Pool := TProducerConsumerThreadPool.Create;
  try
    Pool.Queue(@DoWork);
    Pool.WaitForAll;
  finally
    Pool.Free;
  end;
end;

Error Handling Simple Thread Pool

program ErrorHandling;

{$mode objfpc}{$H+}{$J-}

uses
  Classes, SysUtils, ThreadPool.Simple;

procedure RiskyProcedure;
begin
  raise Exception.Create('Something went wrong!');
end;

var
  Pool: TSimpleThreadPool;
begin
  Pool := TSimpleThreadPool.Create(4); // Create with 4 threads
  try
    Pool.Queue(@RiskyProcedure);
    Pool.WaitForAll;
    
    // Check for errors after completion
    if Pool.LastError <> '' then
    begin
      WriteLn('An error occurred: ', Pool.LastError);
      Pool.ClearLastError;  // Clear for reuse if needed
    end;
  finally
    Pool.Free;
  end;
end.

Error Handling Producer-Consumer Thread Pool

program ErrorHandling;

{$mode objfpc}{$H+}{$J-}

uses
  Classes, SysUtils, ThreadPool.ProducerConsumer;

procedure RiskyProcedure;
begin
  raise Exception.Create('Something went wrong!');
end;

var
  Pool: TProducerConsumerThreadPool;
begin
  Pool := TProducerConsumerThreadPool.Create;
  try
    try
      Pool.Queue(@RiskyProcedure);
    except
      on E: EQueueFullException do
        WriteLn('Queue is full after retries: ', E.Message);
    end;
    
    Pool.WaitForAll;
    
    // Check for errors after completion
    if Pool.LastError <> '' then
    begin
      WriteLn('An error occurred: ', Pool.LastError);
      Pool.ClearLastError;  // Clear for reuse if needed
    end;
  finally
    Pool.Free;
  end;
end.

Tips

Note

Error Handling

  • 🛡️ Exceptions are caught and stored with thread IDs
  • ⚡ Pool continues operating after exceptions
  • 🔄 Use ClearLastError to reset error state

Debugging

  • 🔍 Error messages contain thread identification
  • 📝 Debug logging enabled by default (configurable)
  • 📊 Queue capacity monitoring available

Which implementation should I use?

Need a thread pool?
├─ Tasks are fire-and-forget, count is predictable, low overhead wanted?
│  └─ → Use ThreadPool.Simple (or the GlobalThreadPool singleton)
└─ Producer can outpace consumers, or you need queue overflow control?
   └─ → Use ThreadPool.ProducerConsumer

Use Simple Thread Pool when:

  • Direct task execution without queuing needed
  • Task count is predictable and moderate
  • Low memory overhead is important
  • Global instance (GlobalThreadPool) convenience desired
  • Simple error handling is sufficient

Use Producer-Consumer Pool when:

  • High volume of tasks with rate control needed
  • Backpressure handling required
  • Queue overflow protection important
  • Need detailed execution monitoring
  • Want configurable retry mechanisms

Queue overload reference

All four Queue overloads share the same pattern — pick the one that fits your task:

Overload Signature Use when Example
Plain procedure Queue(@MyProc) Standalone procedure, no shared state needed File I/O, independent calculations
Object method Queue(@MyObj.MyMethod) Task needs access to object fields/state Counter objects, result accumulators
Indexed procedure Queue(@MyProc, i) Loop parallelism over an array/range for i := 0 to N-1 do Queue(@Proc, i)
Indexed method Queue(@MyObj.MyMethod, i) Loop parallelism + object state Parallel array transform on an object

Note

LastError is overwritten (not appended) each time a task raises an exception. If you queue multiple tasks, only the last error is stored. Check LastError immediately after WaitForAll and call ClearLastError before reusing the pool.

📚 Examples

Getting Started

  1. 👋 Starter (examples/Starter/Starter.lpr)
    • The absolute minimum to compile and run
    • Heavily commented — every line explained
    • Best first file to read before the other examples

Simple Thread Pool Examples

  1. 🎓 Simple Demo (examples/SimpleDemo/SimpleDemo.lpr)

    • Basic usage with GlobalThreadPool
    • Demonstrates procedures and methods
    • Shows proper object lifetime
  2. 🔢 Thread Pool Demo (examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpr)

    • Custom thread pool management
    • Thread-safe operations
    • Error handling patterns
  3. 📝 Word Counter (examples/SimpleWordCounter/SimpleWordCounter.lpr)

    • Queue-based task processing
    • Thread-safe counters
    • File I/O with queue management
  4. 🔢 Square Numbers (examples/SimpleSquareNumbers/SimpleSquareNumbers.lpr)

    • High volume task processing
    • Queue full handling
    • Performance comparison

Producer-Consumer Examples

  1. 🎓 Simple Demo (examples/ProdConSimpleDemo/ProdConSimpleDemo.lpr)

    • Basic usage with ProducerConsumerThreadPool
    • Demonstrates procedures and methods
    • Shows proper object lifetime
  2. 🔢 Square Numbers (examples/ProdConSquareNumbers/ProdConSquareNumbers.lpr)

    • High volume task processing
    • Queue full handling
    • Backpressure demonstration
    • Performance monitoring
  3. 📝 Message Processor (examples/ProdConMessageProcessor/ProdConMessageProcessor.lpr)

    • Queue-based task processing
    • Thread-safe message handling
    • Graceful shutdown
    • Error handling patterns

🛠️ Installation

  1. Add the src directory to your project's search path

  2. Choose your implementation:

    For Simple Thread Pool:

    uses ThreadPool.Simple;

    For Producer-Consumer Thread Pool:

    uses ThreadPool.ProducerConsumer;
  3. Start using:

    • Simple: Use GlobalThreadPool or create TSimpleThreadPool
    • Producer-Consumer: Create TProducerConsumerThreadPool

Verify your setup

Compile and run the simplest demo from the command line to confirm everything is wired up correctly:

# Using the Free Pascal compiler directly
fpc -Fu./src examples/SimpleDemo/SimpleDemo.lpr && ./SimpleDemo

# Or build with Lazarus from the command line
lazbuild examples/SimpleDemo/SimpleDemo.lpi && ./SimpleDemo

Expected output (order may vary — tasks run in parallel):

Demo of ThreadPool functionality:
--------------------------------
1. Queueing simple procedure
2. Queueing method of a class
3. Queueing indexed procedure
4. Queueing method with index of a class
--------------------------------
Waiting for all tasks to complete...
Simple procedure executed
Method executed
Indexed procedure executed with index: 1
Method with index executed: 2
--------------------------------
All tasks completed successfully!

Tip

Make sure your source file starts with {$mode objfpc}{$H+}. Without this, Free Pascal defaults to TP/Delphi-7 mode and some syntax will not compile.

⚙️ Requirements

  • 💻 Free Pascal 3.2.2 or later
  • 📦 Lazarus 3.6.0 or later
  • 🆓 No external dependencies

📚 Documentation

🧪 Testing

  1. Go to the tests/ directory
  2. Open TestRunner.lpi in Lazarus IDE and compile
  3. Run ./TestRunner.exe -a -p --format=plain to see the test results.
  4. Ensure all tests pass to verify the library's functionality

May take up to 5 mins to run all tests.

🧵 Thread Management

Thread Count Rules

  • Default: Uses ProcessorCount when thread count ≤ 0
  • Minimum: 4 threads enforced
  • Maximum: 2× ProcessorCount
  • Fixed after creation (no dynamic scaling)

Implementation Characteristics

Simple Thread Pool

  • Direct task execution without queuing
  • Continuous task processing
  • Clean shutdown handling

Producer-Consumer Thread Pool

  • Fixed-size circular queue (1024 items by default, configurable)
  • Backpressure handling with adaptive delays
  • Graceful overflow management

⚠️ Common Mistakes

1. Freeing an object before WaitForAll

// WRONG — MyObject may be freed while worker threads are still calling its methods
MyObject := TMyClass.Create;
GlobalThreadPool.Queue(@MyObject.DoWork);
MyObject.Free;          // freed too early!
GlobalThreadPool.WaitForAll;

// CORRECT — always wait before freeing
MyObject := TMyClass.Create;
try
  GlobalThreadPool.Queue(@MyObject.DoWork);
  GlobalThreadPool.WaitForAll;  // wait first
finally
  MyObject.Free;        // safe to free now
end;

2. Forgetting WaitForAll

Without WaitForAll, your program may exit (and destroy the pool) while tasks are still running, causing access violations or silent data loss.

// WRONG
for i := 0 to 99 do
  GlobalThreadPool.Queue(@ProcessItem, i);
// program exits here, tasks may never finish

// CORRECT
for i := 0 to 99 do
  GlobalThreadPool.Queue(@ProcessItem, i);
GlobalThreadPool.WaitForAll;

3. Only the last error is kept

LastError is overwritten on every exception — not appended. If multiple tasks fail, you only see the last one.

// Queue several tasks that might fail
for i := 0 to 9 do
  Pool.Queue(@RiskyProc, i);
Pool.WaitForAll;

// Only the LAST exception is in LastError
if Pool.LastError <> '' then
  WriteLn('At least one task failed: ', Pool.LastError);
Pool.ClearLastError;

4. Freeing the global pool manually

GlobalThreadPool is managed by the unit's initialization/finalization blocks. Do not call GlobalThreadPool.Free — let the runtime clean it up.

// WRONG
GlobalThreadPool.Free;  // double-free at program exit!

// CORRECT — just use it; finalization handles cleanup
GlobalThreadPool.Queue(@MyProc);
GlobalThreadPool.WaitForAll;

🚧 Planned/In Progress

  • Adaptive thread adjustment based on a load factor
  • Support for procedure Queue(AMethod: TProc; AArgs: array of Const);
  • More comprehensive tests
  • More examples

👏 Acknowledgments

Special thanks to the Free Pascal and Lazarus communities and the creators of the threading libraries mentioned above for inspiration!

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

📋 Changelog

See CHANGELOG.md for the full version history.


💡 More Tip: Check out the examples directory for more usage patterns!

About

A lightweight, easy-to-use thread pool implementation for Free Pascal. Simplify parallel processing for simple tasks! ⚡

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages