Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ jobs:
./tests/TestRunner -a -p --format=plain
fi

- name: Build benchmark
run: lazbuild --build-mode=Release benchmarks/ThreadPoolBenchmark.lpi

- name: Run benchmark smoke test
shell: bash
run: |
if [ -f benchmarks/ThreadPoolBenchmark.exe ]; then
./benchmarks/ThreadPoolBenchmark.exe
else
./benchmarks/ThreadPoolBenchmark
fi

build-examples:
name: Build examples (${{ matrix.os }})
runs-on: ${{ matrix.os }}
Expand Down
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,53 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [0.8.0] - 2026-07-13

### Added

- Shared lifecycle contract for both pools: `tpsAccepting` → `tpsDraining` →
`tpsStopped`, exposed through `State`
- Idempotent `Shutdown`, which stops admission, drains all accepted work, wakes
and joins workers, and rejects calls made from a pool worker to avoid self-wait
- Timeout-aware `WaitForAll(TimeoutMS): Boolean` on both pools
- Four timeout-aware `TryQueue(..., TimeoutMS): Boolean` overloads on both pools
- `EThreadPoolShutdown` for submissions attempted after shutdown begins
- Event-driven not-empty/not-full signalling for the bounded queue
- Event-driven, dynamically growing O(1) circular FIFO for the Simple pool
- Lifecycle, queue-timeout, callback-failure, concurrent admission/shutdown,
invalid-capacity, and draining regression tests
- Release benchmark covering 20,000-task bursts and idle queue-to-start latency

### Changed

- Existing `Queue` overloads remain source-compatible
- Both destructors now use the same draining shutdown behaviour
- Worker completion accounting runs independently of task and callback failures
- `OnError` exceptions are contained so they cannot terminate a worker or wedge
`WaitForAll`
- Callback execution remains synchronous and is not time-limited; blocking tasks
or `OnError` handlers can delay completion and `Shutdown`
- Producer backpressure waits only when the queue is full; load-threshold sleeps
are removed while `TBackpressureConfig` remains source-compatible
- Producer debug logging is disabled by default
- Test-suite runtime reduced from about 54 seconds to about 4 seconds on the
development machine while growing from 43 to 58 tests

### Performance

- Median of three identical-source Windows/FPC 3.2.2 `-O3` runs, logging disabled:
- Simple 20,000-task burst: 234 ms (v0.7.0) → 109 ms (v0.8.0), about 2.1× faster
- Producer burst: 3,953 ms → 172 ms, about 23.0× faster
- Simple idle latency: 7.9 ms → below the benchmark's 1 ms timer resolution
- Producer idle latency: 81.3 ms → below the 1 ms timer resolution

### Compatibility notes

- Queueing after `Shutdown` now raises `EThreadPoolShutdown`; v0.7.0's Simple
pool silently discarded such work
- Backpressure threshold and low/medium-delay fields no longer introduce sleeps;
new code should use `TryQueue` to state its timeout directly

## [0.7.0] - 2026-07-11

### Added
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ sizes are welcome — bug reports, documentation fixes, new examples, and code.
## Prerequisites

- Free Pascal 3.2.2 or later
- Lazarus 3.6.0 or later (provides `lazbuild`)
- Lazarus 4.0 or later (provides `lazbuild`)
- No external dependencies

## Building
Expand All @@ -40,8 +40,8 @@ lazbuild tests/TestRunner.lpi
./tests/TestRunner -a -p --format=plain
```

All tests should pass before you submit a change. The full suite can take a
few minutes to run because some tests exercise high task volumes.
All tests should pass before you submit a change. The full suite normally takes
only a few seconds, although timing varies by machine.

CI runs the package build, the test suite, and all examples on Linux and
Windows for every pull request — please make sure your branch is green.
Expand Down
108 changes: 78 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🚀 ThreadPool for Free Pascal

[![Version](https://img.shields.io/badge/version-0.7.0-8B5CF6.svg)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-0.8.0-8B5CF6.svg)](CHANGELOG.md)
[![License: MIT](https://img.shields.io/badge/License-MIT-1E3A8A.svg)](https://opensource.org/licenses/MIT)
[![Free Pascal](https://img.shields.io/badge/Free%20Pascal-3.2.2+-3B82F6.svg)](https://www.freepascal.org/)
[![Lazarus](https://img.shields.io/badge/Lazarus-4.0+-60A5FA.svg)](https://www.lazarus-ide.org/)
Expand Down Expand Up @@ -47,6 +47,7 @@ A lightweight, easy-to-use thread pool implementation for Free Pascal. Simplify
- [🏃 Quick Start](#-quick-start)
- [Simple Thread Pool](#simple-thread-pool)
- [Producer-Consumer Thread Pool](#producer-consumer-thread-pool)
- [Timeouts and shutdown (v0.8.0)](#timeouts-and-shutdown-v080)
- [Error Handling Simple Thread Pool](#error-handling-simple-thread-pool)
- [Error Handling Producer-Consumer Thread Pool](#error-handling-producer-consumer-thread-pool)
- [Tips](#tips)
Expand Down Expand Up @@ -76,7 +77,7 @@ This library provides two thread pool implementations, each with its own strengt
uses ThreadPool.Simple;
```
- Global singleton instance for quick use
- Direct task execution
- Event-driven, dynamically growing FIFO work queue
- Automatic thread count management
- Best for simple parallel tasks
- Lower memory overhead
Expand All @@ -86,25 +87,29 @@ uses ThreadPool.Simple;
uses ThreadPool.ProducerConsumer;
```

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

- **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
- Producers wait only while the queue is actually full
- `TryQueue(..., TimeoutMS)` reports saturation without raising
- Existing `Queue(...)` calls retain a bounded default wait and raise
`EQueueFullException` if that wait expires

- **Monitoring & Debug**
- Thread-safe error capture with thread IDs
- Detailed debug logging (can be disabled)
- Thread-safe error capture
- Internal debug logging is disabled by default

> [!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.
> For sustained saturation, prefer `TryQueue(..., TimeoutMS)` and handle a
> `False` result explicitly. Legacy `Queue(...)` raises `EQueueFullException`
> when its compatibility wait expires.

### Shared Features

Expand All @@ -118,16 +123,21 @@ A thread pool with fixed-size circular buffer (1024 items) and built-in backpres
- Object methods: `Pool.Queue(@MyObject.MyMethod)`
- Indexed variants: `Pool.Queue(@MyProc, Index)`

- **Thread Safety**
- **Lifecycle & Thread Safety — v0.8.0**
- Built-in synchronization
- Safe resource sharing
- Protected error handling
- Event-driven workers; no polling sleeps
- `Shutdown` stops admission, drains accepted work, and joins workers
- Queueing after shutdown raises `EThreadPoolShutdown`
- `WaitForAll(TimeoutMS)` and `TryQueue(..., TimeoutMS)` use milliseconds;
zero means do not wait and `THREADPOOL_INFINITE` means no timeout

- **Error Management**
- Worker exceptions are caught automatically; the pool keeps running
- `LastError` for the most recent failure
- `Errors` collection captures **all** failed-task messages (capped, oldest dropped) — *v0.7.0*
- Optional `OnError` callback fired per failed task — *v0.7.0*
- Exceptions raised by `OnError` are contained and cannot terminate workers
- Callback execution remains synchronous; keep callbacks short and bounded

> [!NOTE]
> Thread count is determined by `TThread.ProcessorCount` at startup and remains fixed. See [Thread Management](#-thread-management) for details.
Expand Down Expand Up @@ -198,6 +208,42 @@ begin
end;
```

### Timeouts and shutdown (v0.8.0)

Both pools use the same lifecycle contract and timeout units:

```pascal
// Existing source remains valid and waits indefinitely for completion.
Pool.Queue(@DoWork);
Pool.WaitForAll;

// Timeout-aware wait: False means work is still running after 250 ms.
if not Pool.WaitForAll(250) then
WriteLn('Still working');

// ProducerConsumer is bounded; this waits at most 50 ms for queue space.
if not Pool.TryQueue(@DoWork, 50) then
WriteLn('Queue is saturated');

// Stop accepting work, drain every accepted task, then join all workers.
Pool.Shutdown;
```

Timeouts are milliseconds. `0` performs only an immediate check and
`THREADPOOL_INFINITE` waits without a deadline. `TSimpleThreadPool` is
unbounded, so its `TryQueue` timeout is accepted for API symmetry but queue
capacity cannot time out. After `Shutdown`, all `Queue` and `TryQueue` overloads
raise `EThreadPoolShutdown`.

Exception containment does not impose an execution deadline. A task or
`OnError` handler that blocks indefinitely continues to occupy its worker, and
`Shutdown` waits because it drains all accepted work. Use application-level
timeouts or cancellation inside operations that may block.

`WaitForAll` is not an admission barrier for unrelated producer threads. If
producers may still submit concurrently, coordinate them first or call
`Shutdown`, which closes admission before draining.

### Error Handling Simple Thread Pool

```pascal
Expand Down Expand Up @@ -301,22 +347,23 @@ Prefer to react the moment a task fails (instead of polling after `WaitForAll`)?
Assign an `OnError` callback:

```pascal
// IMPORTANT: OnError is called from a worker thread. Keep the handler short and
// thread-safe; synchronize if it touches the UI or shared state.
// IMPORTANT: OnError is called synchronously from a worker thread. Keep the
// handler short, bounded, and thread-safe; synchronize if it touches the UI or
// shared state.
Pool.OnError := @MyHandler.OnTaskError;
```

### Tips

> [!NOTE]
> **Error Handling**
> - 🛡️ Exceptions are caught and stored with thread IDs
> - 🛡️ Exceptions are caught and stored without terminating workers
> - ⚡ Pool continues operating after exceptions
> - 🔄 Use ClearLastError to reset error state
>
> **Debugging**
> - 🔍 Error messages contain thread identification
> - 📝 Debug logging enabled by default (configurable)
> - 🔍 Task failures are collected without stopping workers
> - 📝 Internal debug logging is disabled by default
> - 📊 Queue capacity monitoring available


Expand All @@ -331,7 +378,7 @@ Need a thread pool?
```

**Use Simple Thread Pool when:**
- Direct task execution without queuing needed
- An unbounded, dynamically growing queue is appropriate
- Task count is predictable and moderate
- Low memory overhead is important
- Global instance (GlobalThreadPool) convenience desired
Expand All @@ -342,7 +389,7 @@ Need a thread pool?
- Backpressure handling required
- Queue overflow protection important
- Need detailed execution monitoring
- Want configurable retry mechanisms
- Want bounded submission with explicit timeouts

### Queue overload reference

Expand Down Expand Up @@ -491,7 +538,7 @@ All tasks completed successfully!
## ⚙️ Requirements

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

## 📚 Documentation
Expand All @@ -500,6 +547,7 @@ All tasks completed successfully!
- [ThreadPool.Simple Technical Details](docs/ThreadPool.Simple-Technical.md)
- [ThreadPool.ProducerConsumer API Documentation](docs/ThreadPool.ProducerConsumer-API.md)
- [ThreadPool.ProducerConsumer Technical Details](docs/ThreadPool.ProducerConsumer-Technical.md)
- [v0.8.0 Release Notes](docs/release-notes-v0.8.0.md)

## 🧪 Testing

Expand All @@ -508,7 +556,8 @@ All tasks completed successfully!
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.
The complete suite normally finishes in a few seconds. Release benchmarks live
in [`benchmarks/`](benchmarks/).

## 🧵 Thread Management

Expand All @@ -521,14 +570,14 @@ May take up to 5 mins to run all tests.
### Implementation Characteristics

**Simple Thread Pool**
- Direct task execution without queuing
- Continuous task processing
- Clean shutdown handling
- Dynamically growing O(1) FIFO queue
- Event-driven worker wakeups
- Draining shutdown

**Producer-Consumer Thread Pool**
- Fixed-size circular queue (1024 items by default, configurable)
- Backpressure handling with adaptive delays
- Graceful overflow management
- Event-driven not-empty/not-full signalling
- Timeout-aware bounded submission and draining shutdown


## ⚠️ Common Mistakes
Expand Down Expand Up @@ -603,10 +652,9 @@ GlobalThreadPool.WaitForAll;

## 🚧 Planned/In Progress

- Performance & robustness pass — event-driven idle workers (remove the Simple pool's poll loop), shutdown review, stress/soak tests (planned for 0.8.0)
- Support for `procedure Queue(AMethod: TProc; AArgs: array of Const);`
- More comprehensive tests
- More examples
- Result-bearing task handles/futures
- Chunked `ParallelFor` helpers
- Additional platform and long-running soak coverage

## 🤝 Contributing

Expand Down
17 changes: 17 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# ThreadPool benchmark

Build and run the release-mode benchmark:

```bash
lazbuild --build-mode=Release benchmarks/ThreadPoolBenchmark.lpi
./benchmarks/ThreadPoolBenchmark
```

The same source compiles against v0.7.0 and v0.8.0. It reports:

- completion time for a 20,000-task burst through each pool;
- average queue-to-start latency after workers have been idle.

Run each version several times on the same otherwise-idle machine and compare
medians. Debug logging must be disabled in both versions so console I/O is not
included in the scheduler measurement.
Loading
Loading