diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 707722f..12853d2 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -1,7 +1,15 @@
{
"permissions": {
"allow": [
- "Bash(wc -l /c/Users/iwank/Documents/github/threadpool-fp/src/*.pas)"
+ "Bash(wc -l /c/Users/iwank/Documents/github/threadpool-fp/src/*.pas)",
+ "Bash(grep -iE '\\\\.\\(o|ppu|a|compiled|bak|lps|res\\)$|/lib/|/backup/')",
+ "Bash(grep -iE 'threadpool.*\\\\.pas$|\\\\.lpk$|\\\\.lpi$|\\\\.lpr$')",
+ "Bash(grep -iE '\\\\.\\(o|ppu|bak\\)$|/lib/|/backup/')",
+ "Bash(fpc -iV)",
+ "Bash(grep '\\\\.lpi$')",
+ "Bash(lazbuild tests/TestRunner.lpi)",
+ "Bash(git remote *)",
+ "Bash(git status *)"
]
}
}
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..756f2e1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,64 @@
+name: π Bug Report
+description: Report something that isn't working as expected
+labels: ["bug"]
+body:
+ - type: markdown
+ attributes:
+ value: Thanks for taking the time to file a bug report!
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: What happened?
+ description: A clear description of the bug, including what you expected instead.
+ validations:
+ required: true
+ - type: textarea
+ id: repro
+ attributes:
+ label: Steps to reproduce
+ description: A minimal code snippet or the steps that trigger the problem.
+ placeholder: |
+ 1. Create a TSimpleThreadPool with ...
+ 2. Queue ...
+ 3. Call WaitForAll ...
+ render: pascal
+ validations:
+ required: true
+ - type: dropdown
+ id: pool
+ attributes:
+ label: Which implementation?
+ options:
+ - ThreadPool.Simple
+ - ThreadPool.ProducerConsumer
+ - Both / not sure
+ validations:
+ required: true
+ - type: input
+ id: fpc-version
+ attributes:
+ label: Free Pascal version
+ placeholder: "3.2.2"
+ validations:
+ required: true
+ - type: input
+ id: lazarus-version
+ attributes:
+ label: Lazarus version (if used)
+ placeholder: "3.6.0"
+ - type: dropdown
+ id: os
+ attributes:
+ label: Operating system
+ options:
+ - Windows
+ - Linux
+ - macOS
+ - Other
+ validations:
+ required: true
+ - type: textarea
+ id: extra
+ attributes:
+ label: Anything else?
+ description: Logs, error messages, or context that might help.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..5abb3cb
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: π¬ Questions & Discussion
+ url: https://github.com/ikelaiah/threadpool-fp/discussions
+ about: Ask usage questions or share what you built.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..20e6aae
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,22 @@
+name: π‘ Feature Request
+description: Suggest an idea or improvement
+labels: ["enhancement"]
+body:
+ - type: textarea
+ id: problem
+ attributes:
+ label: What problem would this solve?
+ description: Describe the use case or pain point motivating the request.
+ validations:
+ required: true
+ - type: textarea
+ id: proposal
+ attributes:
+ label: Proposed solution
+ description: What would you like to see? An API sketch is welcome.
+ render: pascal
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: Alternatives considered
+ description: Other approaches or workarounds you've thought about.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..c6358aa
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,24 @@
+
+
+## What does this PR do?
+
+
+
+## Related issue
+
+
+
+## Type of change
+
+- [ ] π Bug fix
+- [ ] β¨ New feature
+- [ ] π Documentation / examples
+- [ ] π§ Refactor / internal change
+
+## Checklist
+
+- [ ] Tests pass locally (`lazbuild tests/TestRunner.lpi` then run `TestRunner`)
+- [ ] Added/updated tests where it makes sense
+- [ ] Updated `CHANGELOG.md`
+- [ ] Updated docs in `docs/` if behavior or API changed
+- [ ] New units use `{$mode objfpc}{$H+}{$J-}` and filenames match unit casing
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..4165494
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,67 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
+
+jobs:
+ build-and-test:
+ name: Build & Test (${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install Lazarus + FPC
+ uses: gcarreno/setup-lazarus@v3
+ with:
+ lazarus-version: stable
+
+ - name: Build package
+ run: lazbuild package/lazarus/threadpool_fp.lpk
+
+ - name: Build test runner
+ run: lazbuild tests/TestRunner.lpi
+
+ - name: Run tests
+ shell: bash
+ run: |
+ if [ -f tests/TestRunner.exe ]; then
+ ./tests/TestRunner.exe -a -p --format=plain
+ else
+ ./tests/TestRunner -a -p --format=plain
+ fi
+
+ build-examples:
+ name: Build examples (${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install Lazarus + FPC
+ uses: gcarreno/setup-lazarus@v3
+ with:
+ lazarus-version: stable
+
+ - name: Build all examples
+ shell: bash
+ run: |
+ set -e
+ for lpi in examples/*/*.lpi; do
+ echo "==> Building $lpi"
+ lazbuild "$lpi"
+ done
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2b94ca..503ae42 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,32 @@ 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.6.0] - 2026-06-11
+
+### Added
+
+- GitHub Actions CI (`.github/workflows/ci.yml`) β builds the package, runs the FPCUnit test suite, and builds all examples on both Linux and Windows for every push and pull request
+- `CONTRIBUTING.md` β build/test instructions, code style guide (including the unit-filename casing rule), and pull-request workflow
+- Issue templates (`.github/ISSUE_TEMPLATE/`) for bug reports and feature requests, plus a discussions contact link
+- Pull request template (`.github/PULL_REQUEST_TEMPLATE.md`)
+- README: Contributing section linking to `CONTRIBUTING.md`
+
+### Changed
+
+- Renamed source files to match their `unit` identifier casing so the library compiles on case-sensitive filesystems (Linux): `threadpool.simple.pas` β `ThreadPool.Simple.pas`, `threadpool.producerconsumer.pas` β `ThreadPool.ProducerConsumer.pas`, and `tests/threadpool.producerconsumer.tests.pas` β `tests/ThreadPool.ProducerConsumer.Tests.pas`
+- Normalized the unit reference in `tests/TestRunner.lpr` (`Threadpool.` β `ThreadPool.`)
+- README: replaced the hardcoded test-count badge with a live CI status badge
+
+### Fixed
+
+- **Access violation on Linux at runtime (exit code 217).** The test runner and example programs were missing the `cthreads` unit, which Unix/Linux requires (as the first unit) for any program that creates threads. Without it, constructing the thread pool crashed in the worker/sync-object setup. Added `{$IFDEF UNIX}cthreads{$ENDIF}` to `tests/TestRunner.lpr` and all 8 example programs. Windows was unaffected and did not need it
+- `ThreadPool.Simple` now compiles on non-Windows targets: the `IWorkerThread` methods (`QueryInterface`/`_AddRef`/`_Release`) hardcoded the `stdcall` calling convention, which only matches `IInterface` on Windows β FPC uses `cdecl` elsewhere. Guarded with `{$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF}`
+- `TSimpleWorkerThread` is now explicitly non-ref-counted (`_AddRef`/`_Release` return `-1`, the `TComponent` contract), so an `IWorkerThread` interface assignment can never free a worker the pool still owns via `ClearThreads`. Removed the now-unused `FRefCount` field
+- `TSimpleThreadPool.Destroy` is now safe to call on a partially constructed pool: it skips `WaitForAll` and the queue/thread cleanup when the relevant fields are `nil`, instead of dereferencing them
+- Example projects (all except `Starter`) only had the `src` search path in their `Release` build mode, so `lazbuild` (which builds the `Default` mode) could not find the units. Added the search path to each project's global compiler options
+- Tests `Test04_CreateDestroy` and `Test11_ThreadCount` hardcoded thread-count expectations that only held on multi-core machines; on low-core CI runners the enforced minimum of 4 threads made them fail. They now assert against the actual `Max(Min(requested, 2ΓProcessorCount), 4)` formula
+- `Test08_QueueFullBehavior` assumed a single worker drains the queue, but the pool enforces a 4-thread minimum, so on Linux the queue drained before it could overflow. It now bursts `ThreadCount + QueueSize + 1` long-running tasks, which overflows a fixed queue deterministically regardless of core count or scheduling
+
## [0.5.0] - 2026-04-13
### Added
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..d08bcbe
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,73 @@
+# Contributing to ThreadPool for Free Pascal
+
+Thanks for your interest in improving this library! Contributions of all
+sizes are welcome β bug reports, documentation fixes, new examples, and code.
+
+## Ways to contribute
+
+- π **Report a bug** β open an [issue](https://github.com/ikelaiah/threadpool-fp/issues) using the Bug Report template.
+- π‘ **Request a feature** β open an issue using the Feature Request template.
+- π **Improve docs or examples** β these are especially appreciated and easy to start with.
+- π§ **Fix a bug or add a feature** β see the workflow below.
+
+## Prerequisites
+
+- Free Pascal 3.2.2 or later
+- Lazarus 3.6.0 or later (provides `lazbuild`)
+- No external dependencies
+
+## Building
+
+Build the package:
+
+```bash
+lazbuild package/lazarus/threadpool_fp.lpk
+```
+
+Or compile a single example directly with FPC:
+
+```bash
+fpc -Fu./src examples/SimpleDemo/SimpleDemo.lpr
+```
+
+## Running the tests
+
+```bash
+lazbuild tests/TestRunner.lpi
+# Windows
+./tests/TestRunner.exe -a -p --format=plain
+# Linux / macOS
+./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.
+
+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.
+
+## Code style
+
+- Use `{$mode objfpc}{$H+}{$J-}` at the top of new units and programs.
+- Match the existing formatting: two-space indentation, `PascalCase` for
+ types/methods, and a descriptive comment for non-obvious logic.
+- **Filenames must match the `unit` identifier's casing exactly**
+ (e.g. unit `ThreadPool.Simple` lives in `ThreadPool.Simple.pas`). This keeps
+ the library compiling on case-sensitive filesystems like Linux.
+- Keep the library dependency-free (FCL only).
+
+## Pull request workflow
+
+1. Fork the repo and create a branch off `main`
+ (e.g. `feat/adaptive-threads` or `fix/queue-overflow`).
+2. Make your change, including tests and/or an example where it makes sense.
+3. Run the test suite locally and make sure it passes.
+4. Update `CHANGELOG.md` and any relevant docs in `docs/`.
+5. Open a pull request describing **what** changed and **why**.
+
+## Reporting security issues
+
+For anything sensitive, please email the maintainer rather than opening a
+public issue.
+
+Thank you for helping make this library better! π
diff --git a/README.md b/README.md
index c8726e3..fe1e8bd 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
# π ThreadPool for Free Pascal
-[](CHANGELOG.md)
+[](CHANGELOG.md)
[](https://opensource.org/licenses/MIT)
[](https://www.freepascal.org/)
[](https://www.lazarus-ide.org/)



-
+[](https://github.com/ikelaiah/threadpool-fp/actions/workflows/ci.yml)
[](docs/)
[](README.md)
@@ -522,6 +522,11 @@ GlobalThreadPool.WaitForAll;
- More comprehensive tests
- More examples
+## π€ Contributing
+
+Contributions are welcome β bug reports, docs, examples, and code. See
+[CONTRIBUTING.md](CONTRIBUTING.md) for how to build, test, and submit changes.
+
## π Acknowledgments
Special thanks to the Free Pascal and Lazarus communities and the creators of the threading libraries mentioned above for inspiration!
diff --git a/docs/release-notes-v0.6.0.md b/docs/release-notes-v0.6.0.md
new file mode 100644
index 0000000..d3294f8
--- /dev/null
+++ b/docs/release-notes-v0.6.0.md
@@ -0,0 +1,92 @@
+# ThreadPool for Free Pascal β v0.6.0
+
+This release focuses on **developer experience and cross-platform reliability**.
+There are no public API changes β existing code continues to compile and run
+unchanged.
+
+## What's new
+
+### Continuous Integration
+
+- **GitHub Actions CI** (`.github/workflows/ci.yml`) β every push and pull
+ request now builds the package, runs the full FPCUnit test suite, and builds
+ all examples on both **Linux and Windows**.
+- **Live CI badge** β the README's hardcoded test-count badge has been replaced
+ with a real build-status badge, so the README always reflects the current
+ state of the `main` branch.
+
+### Easier contributing
+
+- **`CONTRIBUTING.md`** β build and test instructions, code style guide
+ (including the unit-filename casing rule), and the pull-request workflow.
+- **Issue templates** (`.github/ISSUE_TEMPLATE/`) β structured forms for bug
+ reports and feature requests, plus a link to Discussions for questions.
+- **Pull request template** (`.github/PULL_REQUEST_TEMPLATE.md`) β a checklist
+ covering tests, changelog, docs, and the unit-casing convention.
+- **README: Contributing section** linking to `CONTRIBUTING.md`.
+
+## Cross-platform fix
+
+Unit filenames now match their `unit` identifier casing exactly, so the library
+compiles on case-sensitive filesystems such as Linux. On Windows this was
+masked by the case-insensitive filesystem.
+
+| Before | After |
+| --- | --- |
+| `src/threadpool.simple.pas` | `src/ThreadPool.Simple.pas` |
+| `src/threadpool.producerconsumer.pas` | `src/ThreadPool.ProducerConsumer.pas` |
+| `tests/threadpool.producerconsumer.tests.pas` | `tests/ThreadPool.ProducerConsumer.Tests.pas` |
+
+The unit reference in `tests/TestRunner.lpr` was also normalized
+(`Threadpool.` β `ThreadPool.`).
+
+## Bug fixes
+
+Setting up CI surfaced several issues that previously only affected Linux (and
+example builds) β all were masked on Windows:
+
+- **Access violation on Linux at runtime (exit code 217).** The test runner and
+ example programs were missing the `cthreads` unit. On Unix/Linux, any program
+ that creates threads must include `cthreads` as its first unit; without it,
+ constructing the thread pool crashed while setting up its worker threads and
+ synchronization objects. `{$IFDEF UNIX}cthreads{$ENDIF}` was added to the test
+ runner and all 8 examples. Windows does not need it and is unaffected.
+- **Compilation on non-Windows targets.** `TSimpleWorkerThread` hardcoded the
+ `stdcall` calling convention on its `IWorkerThread` methods
+ (`QueryInterface`/`_AddRef`/`_Release`). FPC's `IInterface` only uses
+ `stdcall` on Windows and `cdecl` elsewhere, so the implementations didn't
+ match on Linux. Now guarded with `{$IFDEF WINDOWS}`.
+- **Hardened worker lifetime.** `IWorkerThread` is now explicitly
+ non-ref-counted (`_AddRef`/`_Release` return `-1`), so an interface
+ assignment can never free a worker the pool still owns via `ClearThreads`.
+ `TSimpleThreadPool.Destroy` is also now safe to run on a partially
+ constructed pool.
+- **Example projects couldn't find the library.** Every example except
+ `Starter` had the `src` search path only in its `Release` build mode, so
+ `lazbuild` (which builds the `Default` mode) failed with "Can't find unit".
+ The search path is now in each project's global compiler options.
+- **Core-count-dependent tests.** A few tests hardcoded thread-count
+ expectations and queue-draining timing that only held on the developer's
+ multi-core machine; on low-core CI runners the enforced 4-thread minimum
+ made them fail. They now assert against the actual thread-count formula and
+ overflow the queue with a burst sized off the real thread count, so they're
+ deterministic on any number of cores.
+
+## Upgrade notes
+
+- **No code changes required.** `uses ThreadPool.Simple;`,
+ `uses ThreadPool.ProducerConsumer;`, etc. are unchanged.
+- If you reference the source files by literal path (rare), update to the new
+ capitalized filenames listed above.
+
+## Verification
+
+CI builds the package, runs the test suite, and builds all 8 examples on both
+**Linux and Windows**:
+
+- Test suite: **35 tests, 0 errors, 0 failures, 0 unfreed memory blocks**.
+- All 8 example projects build cleanly on both platforms.
+
+## Full changelog
+
+See [CHANGELOG.md](../CHANGELOG.md) for the complete version history.
diff --git a/examples/ProdConMessageProcessor/ProdConMessageProcessor.lpi b/examples/ProdConMessageProcessor/ProdConMessageProcessor.lpi
index d7639f3..c9ae83f 100644
--- a/examples/ProdConMessageProcessor/ProdConMessageProcessor.lpi
+++ b/examples/ProdConMessageProcessor/ProdConMessageProcessor.lpi
@@ -101,6 +101,7 @@
+
diff --git a/examples/ProdConMessageProcessor/ProdConMessageProcessor.lpr b/examples/ProdConMessageProcessor/ProdConMessageProcessor.lpr
index fb3b235..57e1747 100644
--- a/examples/ProdConMessageProcessor/ProdConMessageProcessor.lpr
+++ b/examples/ProdConMessageProcessor/ProdConMessageProcessor.lpr
@@ -3,6 +3,9 @@
{$mode objfpc}{$H+}{$J-}
uses
+ {$IFDEF UNIX}
+ cthreads, // MUST be first: enables threading support on Unix/Linux
+ {$ENDIF}
Classes, SysUtils, ThreadPool.ProducerConsumer;
type
diff --git a/examples/ProdConSimpleDemo/ProdConSimpleDemo.lpi b/examples/ProdConSimpleDemo/ProdConSimpleDemo.lpi
index ccbbecd..a0146dd 100644
--- a/examples/ProdConSimpleDemo/ProdConSimpleDemo.lpi
+++ b/examples/ProdConSimpleDemo/ProdConSimpleDemo.lpi
@@ -101,6 +101,7 @@
+
diff --git a/examples/ProdConSimpleDemo/ProdConSimpleDemo.lpr b/examples/ProdConSimpleDemo/ProdConSimpleDemo.lpr
index 966dc67..3042a4e 100644
--- a/examples/ProdConSimpleDemo/ProdConSimpleDemo.lpr
+++ b/examples/ProdConSimpleDemo/ProdConSimpleDemo.lpr
@@ -3,6 +3,9 @@
{$mode objfpc}{$H+}{$J-}
uses
+ {$IFDEF UNIX}
+ cthreads, // MUST be first: enables threading support on Unix/Linux
+ {$ENDIF}
Classes, SysUtils, ThreadPool.ProducerConsumer;
type
diff --git a/examples/ProdConSquareNumbers/ProdConSquareNumbers.lpi b/examples/ProdConSquareNumbers/ProdConSquareNumbers.lpi
index 93b1b91..708e892 100644
--- a/examples/ProdConSquareNumbers/ProdConSquareNumbers.lpi
+++ b/examples/ProdConSquareNumbers/ProdConSquareNumbers.lpi
@@ -101,6 +101,7 @@
+
diff --git a/examples/ProdConSquareNumbers/ProdConSquareNumbers.lpr b/examples/ProdConSquareNumbers/ProdConSquareNumbers.lpr
index 5919d17..fb3b7b6 100644
--- a/examples/ProdConSquareNumbers/ProdConSquareNumbers.lpr
+++ b/examples/ProdConSquareNumbers/ProdConSquareNumbers.lpr
@@ -3,6 +3,9 @@
{$mode objfpc}{$H+}{$J-}
uses
+ {$IFDEF UNIX}
+ cthreads, // MUST be first: enables threading support on Unix/Linux
+ {$ENDIF}
Classes, SysUtils, ThreadPool.ProducerConsumer, SyncObjs;
const
diff --git a/examples/SimpleDemo/SimpleDemo.lpi b/examples/SimpleDemo/SimpleDemo.lpi
index 7694efa..e208d83 100644
--- a/examples/SimpleDemo/SimpleDemo.lpi
+++ b/examples/SimpleDemo/SimpleDemo.lpi
@@ -101,6 +101,7 @@
+
diff --git a/examples/SimpleDemo/SimpleDemo.lpr b/examples/SimpleDemo/SimpleDemo.lpr
index 16cb206..3d126e9 100644
--- a/examples/SimpleDemo/SimpleDemo.lpr
+++ b/examples/SimpleDemo/SimpleDemo.lpr
@@ -3,6 +3,9 @@
{$mode objfpc}{$H+}{$J-}
uses
+ {$IFDEF UNIX}
+ cthreads, // MUST be first: enables threading support on Unix/Linux
+ {$ENDIF}
Classes, SysUtils, ThreadPool.Simple;
type
diff --git a/examples/SimpleSquareNumbers/SimpleSquareNumbers.lpi b/examples/SimpleSquareNumbers/SimpleSquareNumbers.lpi
index 07ab534..25746f4 100644
--- a/examples/SimpleSquareNumbers/SimpleSquareNumbers.lpi
+++ b/examples/SimpleSquareNumbers/SimpleSquareNumbers.lpi
@@ -101,6 +101,7 @@
+
diff --git a/examples/SimpleSquareNumbers/SimpleSquareNumbers.lpr b/examples/SimpleSquareNumbers/SimpleSquareNumbers.lpr
index a82f971..fc5e743 100644
--- a/examples/SimpleSquareNumbers/SimpleSquareNumbers.lpr
+++ b/examples/SimpleSquareNumbers/SimpleSquareNumbers.lpr
@@ -3,6 +3,9 @@
{$mode objfpc}{$H+}{$J-}
uses
+ {$IFDEF UNIX}
+ cthreads, // MUST be first: enables threading support on Unix/Linux
+ {$ENDIF}
Classes, SysUtils, ThreadPool.Simple;
const
diff --git a/examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpi b/examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpi
index 80a10c9..9372804 100644
--- a/examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpi
+++ b/examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpi
@@ -101,6 +101,7 @@
+
diff --git a/examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpr b/examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpr
index fa2ee57..95e540b 100644
--- a/examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpr
+++ b/examples/SimpleThreadpoolDemo/SimpleThreadpoolDemo.lpr
@@ -3,6 +3,9 @@
{$mode objfpc}{$H+}{$J-} // Standard FPC mode directives
uses
+ {$IFDEF UNIX}
+ cthreads, // MUST be first: enables threading support on Unix/Linux
+ {$ENDIF}
Classes, // For TThread and other basic classes
SysUtils, // For string handling and formatting
ThreadPool.Simple, // Our thread pool implementation
diff --git a/examples/SimpleWordCounter/SimpleWordCounter.lpi b/examples/SimpleWordCounter/SimpleWordCounter.lpi
index 9319f99..1c59692 100644
--- a/examples/SimpleWordCounter/SimpleWordCounter.lpi
+++ b/examples/SimpleWordCounter/SimpleWordCounter.lpi
@@ -101,6 +101,7 @@
+
diff --git a/examples/SimpleWordCounter/SimpleWordCounter.lpr b/examples/SimpleWordCounter/SimpleWordCounter.lpr
index 733e841..397d083 100644
--- a/examples/SimpleWordCounter/SimpleWordCounter.lpr
+++ b/examples/SimpleWordCounter/SimpleWordCounter.lpr
@@ -3,6 +3,9 @@
{$mode objfpc}{$H+}{$J-}
uses
+ {$IFDEF UNIX}
+ cthreads, // MUST be first: enables threading support on Unix/Linux
+ {$ENDIF}
Classes, SysUtils, ThreadPool.Simple, syncobjs;
const
diff --git a/examples/Starter/Starter.lpr b/examples/Starter/Starter.lpr
index cc6cad3..2aa0830 100644
--- a/examples/Starter/Starter.lpr
+++ b/examples/Starter/Starter.lpr
@@ -32,6 +32,9 @@
{$mode objfpc}{$H+}{$J-}
uses
+ {$IFDEF UNIX}
+ cthreads, // MUST be first: enables threading support on Unix/Linux
+ {$ENDIF}
Classes, SysUtils, ThreadPool.Simple;
// ---------------------------------------------------------------------------
diff --git a/package/lazarus/threadpool_fp.lpk b/package/lazarus/threadpool_fp.lpk
index 47bfc2f..247293c 100644
--- a/package/lazarus/threadpool_fp.lpk
+++ b/package/lazarus/threadpool_fp.lpk
@@ -81,7 +81,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. "/>
-
+
-
diff --git a/src/threadpool.producerconsumer.pas b/src/ThreadPool.ProducerConsumer.pas
similarity index 100%
rename from src/threadpool.producerconsumer.pas
rename to src/ThreadPool.ProducerConsumer.pas
diff --git a/src/threadpool.simple.pas b/src/ThreadPool.Simple.pas
similarity index 87%
rename from src/threadpool.simple.pas
rename to src/ThreadPool.Simple.pas
index 271b931..5781d51 100644
--- a/src/threadpool.simple.pas
+++ b/src/ThreadPool.Simple.pas
@@ -33,7 +33,6 @@ TSimpleWorkItem = class(TInterfacedObject, IWorkItem)
{ Simple worker thread implementation }
TSimpleWorkerThread = class(TThread, IWorkerThread)
private
- FRefCount: Integer;
FThreadPool: TObject;
protected
procedure Execute; override;
@@ -45,9 +44,9 @@ TSimpleWorkerThread = class(TThread, IWorkerThread)
procedure Terminate;
procedure WaitFor;
function GetThreadID: TThreadID;
- function QueryInterface(constref IID: TGUID; out Obj): HResult; stdcall;
- function _AddRef: Integer; stdcall;
- function _Release: Integer; stdcall;
+ function QueryInterface(constref IID: TGUID; out Obj): HResult; {$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF};
+ function _AddRef: Integer; {$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF};
+ function _Release: Integer; {$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF};
end;
{$ENDREGION}
@@ -100,7 +99,6 @@ implementation
constructor TSimpleWorkerThread.Create(AThreadPool: TObject);
begin
inherited Create(True); // Create suspended
- FRefCount := 0;
FThreadPool := AThreadPool;
FreeOnTerminate := False;
end;
@@ -130,7 +128,7 @@ function TSimpleWorkerThread.GetThreadID: TThreadID;
Result := ThreadID;
end;
-function TSimpleWorkerThread.QueryInterface(constref IID: TGUID; out Obj): HResult; stdcall;
+function TSimpleWorkerThread.QueryInterface(constref IID: TGUID; out Obj): HResult; {$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF};
begin
if GetInterface(IID, Obj) then
Result := S_OK
@@ -138,16 +136,19 @@ function TSimpleWorkerThread.QueryInterface(constref IID: TGUID; out Obj): HResu
Result := E_NOINTERFACE;
end;
-function TSimpleWorkerThread._AddRef: Integer; stdcall;
+function TSimpleWorkerThread._AddRef: Integer; {$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF};
begin
- Result := InterlockedIncrement(FRefCount);
+ // The pool owns this thread's lifetime via FThreads/ClearThreads, so the
+ // IWorkerThread interface must NOT reference-count. Returning -1 marks this
+ // as a non-ref-counted interface (the same contract TComponent uses), which
+ // prevents an interface assignment from freeing the still-live worker.
+ Result := -1;
end;
-function TSimpleWorkerThread._Release: Integer; stdcall;
+function TSimpleWorkerThread._Release: Integer; {$IFDEF WINDOWS}stdcall{$ELSE}cdecl{$ENDIF};
begin
- Result := InterlockedDecrement(FRefCount);
- if Result = 0 then
- Destroy;
+ // See _AddRef: lifetime is owned by the pool, never by interface refcount.
+ Result := -1;
end;
procedure TSimpleWorkerThread.Execute;
@@ -282,19 +283,28 @@ constructor TSimpleThreadPool.Create(AThreadCount: Integer = 0);
destructor TSimpleThreadPool.Destroy;
begin
- WaitForAll; // Ensure all tasks complete before destroying
+ // Only drain outstanding work if the pool was fully constructed. If the
+ // constructor failed partway, FPC still calls this destructor on the
+ // half-built object, so guard against the sync objects being nil.
+ if Assigned(FWorkItemEvent) and Assigned(FErrorEvent) then
+ WaitForAll; // Ensure all tasks complete before destroying
+
FShutdown := True;
- ClearWorkItems;
- ClearThreads;
-
- // Clean up synchronization objects
+
+ if Assigned(FWorkItems) then
+ ClearWorkItems;
+ if Assigned(FThreads) then
+ ClearThreads;
+
+ // Clean up synchronization objects (each may be nil after a partial
+ // construction, so Free β which is nil-safe β is used throughout).
FWorkItemLock.Free;
FThreads.Free;
FWorkItems.Free;
FWorkItemEvent.Free;
FErrorEvent.Free;
FErrorLock.Free;
-
+
inherited Destroy;
end;
diff --git a/tests/TestRunner.lpr b/tests/TestRunner.lpr
index 6eb89cb..377faa4 100644
--- a/tests/TestRunner.lpr
+++ b/tests/TestRunner.lpr
@@ -3,9 +3,12 @@
{$mode objfpc}{$H+}
uses
+ {$IFDEF UNIX}
+ cthreads, // MUST be first: enables threading support on Unix/Linux
+ {$ENDIF}
Classes, consoletestrunner,
ThreadPool.Simple.Tests,
- Threadpool.ProducerConsumer.Tests;
+ ThreadPool.ProducerConsumer.Tests;
type
diff --git a/tests/threadpool.producerconsumer.tests.pas b/tests/ThreadPool.ProducerConsumer.Tests.pas
similarity index 93%
rename from tests/threadpool.producerconsumer.tests.pas
rename to tests/ThreadPool.ProducerConsumer.Tests.pas
index 1a2c8b0..4f32044 100644
--- a/tests/threadpool.producerconsumer.tests.pas
+++ b/tests/ThreadPool.ProducerConsumer.Tests.pas
@@ -280,41 +280,42 @@ procedure TTestProducerConsumerThreadPool.Test07_ParallelExecution;
procedure TTestProducerConsumerThreadPool.Test08_QueueFullBehavior;
const
QUEUE_SIZE = 2;
- THREAD_COUNT = 1;
var
TestPool: TProducerConsumerThreadPool;
- I: Integer;
+ I, BurstSize: Integer;
ExceptionRaised: Boolean;
ExceptionMessage: string;
Config: TBackpressureConfig;
begin
LogTest('Test08_QueueFullBehavior starting...');
-
- // Create a separate pool for this test
- TestPool := TProducerConsumerThreadPool.Create(THREAD_COUNT, QUEUE_SIZE);
+
+ // The base pool enforces a minimum of 4 worker threads regardless of the
+ // requested count, so the test cannot rely on a "single slow worker". With
+ // N workers and a queue of QUEUE_SIZE, the system can hold at most
+ // N + QUEUE_SIZE long-running tasks before the queue is full. Queue one more
+ // than that, fast, with MaxAttempts=1 and no backpressure delay, and the
+ // overflow MUST raise EQueueFullException β independent of scheduling or the
+ // host's core count.
+ TestPool := TProducerConsumerThreadPool.Create(1, QUEUE_SIZE);
try
- // Configure aggressive backpressure
Config := TestPool.WorkQueue.BackpressureConfig;
Config.MaxAttempts := 1;
Config.LowLoadDelay := 0;
Config.MediumLoadDelay := 0;
Config.HighLoadDelay := 0;
TestPool.WorkQueue.BackpressureConfig := Config;
-
- ExceptionRaised := False;
- // Fill the queue with long-running tasks so the worker cannot drain the
- // queue between these enqueues and the third attempt below.
- // SlowTask (250ms) is too short β the single worker may dequeue item 1
- // before item 3 is attempted, defeating the "full queue" condition.
- LogTest('test08: Queueing first task');
- TestPool.Queue(@LongTask);
- LogTest('test08: Queueing second task');
- TestPool.Queue(@LongTask);
+ ExceptionRaised := False;
+ // ThreadCount reflects the enforced minimum; +QUEUE_SIZE in-flight slots,
+ // +1 to guarantee overflow.
+ BurstSize := TestPool.ThreadCount + QUEUE_SIZE + 1;
+ LogTest(Format('test08: bursting %d LongTasks (threads=%d, queue=%d)',
+ [BurstSize, TestPool.ThreadCount, QUEUE_SIZE]));
try
- TestPool.Queue(@LongTask);
- LogTest('ERROR: Queue succeeded when it should have been full');
+ for I := 1 to BurstSize do
+ TestPool.Queue(@LongTask);
+ LogTest('ERROR: Queue never reported full');
except
on E: EQueueFullException do
begin
@@ -326,17 +327,14 @@ procedure TTestProducerConsumerThreadPool.Test08_QueueFullBehavior;
LogTest('Caught unexpected exception type: ' + E.ClassName + ': ' + E.Message);
end;
- // Wait for queued tasks to complete
- TestPool.WaitForAll;
-
AssertTrue('Should have raised an exception', ExceptionRaised);
- AssertTrue('Exception should be EQueueFullException',
+ AssertTrue('Exception should be EQueueFullException',
ExceptionMessage.Contains('Queue is full'));
finally
TestPool.WaitForAll;
TestPool.Free;
end;
-
+
LogTest('Test08_QueueFullBehavior finished');
end;
diff --git a/tests/threadpool.simple.tests.pas b/tests/threadpool.simple.tests.pas
index 7353875..4b5be15 100644
--- a/tests/threadpool.simple.tests.pas
+++ b/tests/threadpool.simple.tests.pas
@@ -5,8 +5,8 @@
interface
uses
- Classes, SysUtils, fpcunit, testregistry,
- ThreadPool.Types, ThreadPool.Simple, syncobjs, DateUtils;
+ Classes, SysUtils, fpcunit, testregistry,
+ ThreadPool.Types, ThreadPool.Simple, syncobjs, DateUtils, Math;
type
TTestException = class(Exception);
@@ -186,10 +186,16 @@ procedure TSimpleThreadPoolTests.Test03_WorkerThreadInterface;
Pool := TSimpleThreadPool.Create(4);
try
WorkerThread := TSimpleWorkerThread.Create(Pool);
- Thread := WorkerThread; // Explicit interface conversion
- AssertTrue('TSimpleWorkerThread should implement IWorkerThread', Thread <> nil);
+ try
+ Thread := WorkerThread; // Explicit interface conversion
+ AssertTrue('TSimpleWorkerThread should implement IWorkerThread', Thread <> nil);
+ finally
+ // IWorkerThread is non-ref-counted (the pool owns worker lifetime), so
+ // the worker must be freed explicitly rather than via the interface.
+ Thread := nil;
+ WorkerThread.Free;
+ end;
finally
- Thread := nil;
Pool.Free;
end;
end;
@@ -199,19 +205,22 @@ procedure TSimpleThreadPoolTests.Test04_CreateDestroy;
Pool: TSimpleThreadPool;
const
MinThreads = 4;
- TestThreads = 6;
+ // Request well above both the minimum and any plausible 2x-core cap so the
+ // result is governed by the cap, independent of the host's core count.
+ TestThreads = 64;
begin
Pool := TSimpleThreadPool.Create(TestThreads);
try
- AssertEquals('Thread count should match when above minimum',
- TestThreads, Pool.ThreadCount);
+ AssertEquals('Thread count should be capped at 2x processor count',
+ Max(Min(TestThreads, TThread.ProcessorCount * 2), MinThreads),
+ Pool.ThreadCount);
finally
Pool.Free;
end;
Pool := TSimpleThreadPool.Create(2);
try
- AssertEquals('Thread count should be adjusted to minimum',
+ AssertEquals('Thread count should be adjusted to minimum',
MinThreads, Pool.ThreadCount);
finally
Pool.Free;
@@ -279,23 +288,26 @@ procedure TSimpleThreadPoolTests.Test10_StressTest;
end;
procedure TSimpleThreadPoolTests.Test11_ThreadCount;
+const
+ MinThreads = 4;
var
Pool: TSimpleThreadPool;
begin
- // Test maximum thread count
+ // Test maximum thread count. The result is the 2x-core cap, but never below
+ // the enforced minimum of 4 (which matters on low-core CI runners).
Pool := TSimpleThreadPool.Create(TThread.ProcessorCount * 3);
try
- AssertEquals('Thread count should be limited to 2x processor count',
- TThread.ProcessorCount * 2, Pool.ThreadCount);
+ AssertEquals('Thread count should be limited to 2x processor count (min 4)',
+ Max(TThread.ProcessorCount * 2, MinThreads), Pool.ThreadCount);
finally
Pool.Free;
end;
-
- // Test zero thread count
+
+ // Test zero thread count: defaults to processor count, again floored at 4.
Pool := TSimpleThreadPool.Create(0);
try
- AssertEquals('Thread count should default to processor count',
- TThread.ProcessorCount, Pool.ThreadCount);
+ AssertEquals('Thread count should default to processor count (min 4)',
+ Max(TThread.ProcessorCount, MinThreads), Pool.ThreadCount);
finally
Pool.Free;
end;