PERF: Guard the ThreadPool singleton with an atomic instead of std::once_flag#6705
Open
hjmjohnson wants to merge 2 commits into
Open
Conversation
Two dynamically loaded libraries, each statically linking its own copy of ITKCommon and sharing a single SingletonIndex, must observe the same ThreadPool. That is the configuration of every wrapped Python module when ITK is built static, and nothing covered it.
Replaces the 4-byte std::once_flag with a 1-byte atomic guard plus the mutex ThreadPoolGlobals already owns. The guard stays inside those globals, which the SingletonIndex shares, so every ITK instance still agrees on one singleton and registers pthread_atfork once. Selects std::atomic_flag when C++20 makes test() available and std::atomic<bool> otherwise. The choice keys off the library feature-test macro rather than __cplusplus, which MSVC misreports. Addresses the goal of InsightSoftwareConsortium#4171, which moved the guard to a function-local static and so gave each loaded module its own.
hjmjohnson
marked this pull request as ready for review
July 25, 2026 18:53
Contributor
|
| Filename | Overview |
|---|---|
| Modules/Core/Common/src/itkThreadPool.cxx | Replaces call_once with double-checked atomic publication while retaining serialized initialization in shared singleton globals. |
| Modules/Core/Common/test/CMakeLists.txt | Builds two loadable modules with statically linked ITKCommon copies and registers the singleton-sharing regression test. |
| Modules/Core/Common/test/itkThreadPoolSingletonSharingTest.cxx | Loads both test modules, shares the driver's SingletonIndex, and verifies that both expose the same ThreadPool address. |
| Modules/Core/Common/test/ThreadPoolSingletonLibraryA.cxx | Provides exported test entry points for adopting the shared SingletonIndex and retrieving a pinned ThreadPool. |
| Modules/Core/Common/test/ThreadPoolSingletonLibraryB.cxx | Provides the second independently linked ITKCommon copy used to verify process-wide singleton sharing. |
Sequence Diagram
sequenceDiagram
participant Caller
participant Globals as Shared ThreadPoolGlobals
participant Guard as Atomic guard
participant Mutex as Initialization mutex
participant Pool as ThreadPool
Caller->>Guard: acquire-load
alt guard is clear
Caller->>Mutex: lock
Caller->>Guard: relaxed-load
alt still clear
Caller->>Pool: create singleton
Caller->>Guard: release-store set
end
Caller->>Mutex: unlock
end
Caller-->>Caller: return shared SmartPointer
Reviews (1): Last reviewed commit: "PERF: Guard the ThreadPool singleton wit..." | Re-trigger Greptile
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Keeps the ThreadPool singleton guard inside the
SingletonIndex-sharedThreadPoolGlobalswhile replacingstd::once_flagwith a smaller atomic guard, and adds the regression test that was missing. Supersedes #4171, which pursued the same goal by moving the guard to a function-local static — per-binary storage, so each statically linked copy of ITKCommon re-runs the initializer.Why the guard must live in ThreadPoolGlobals
ThreadPoolGlobalsis registered throughitkGetGlobalSimpleMacro→SingletonIndex. In a static build every wrapped Python module links its own ITKCommon and adopts one shared index (Wrapping/macro_files/itk_end_wrap_module.cmake). A guard with per-binary storage therefore cannot serialize creation of a per-process object: the second module re-enters the initializer, replaces the sharedSmartPointer, and registerspthread_atforka second time.Measured behaviour of the superseded approach
Built from
origin/mainwithBUILD_SHARED_LIBS=OFF,ITK_WRAP_PYTHON=ON(96 wrapped modules). Realimport itk, two filters from two modules, thenos.fork():The failing prefork handler belongs to a different module's ITKCommon copy.
mainforks cleanly. With a pinned reference, two distinct pools and 5→9 worker threads are observed.C++17 / C++20
std::atomic_flag::test()is C++20, so C++17 usesstd::atomic<bool>with astatic_assertonis_always_lock_free. Selection keys off__cpp_lib_atomic_flag_testrather than__cplusplus, which MSVC reports as199711Lunless/Zc:__cplusplusis passed. Verified both branches compile and pass (-std=c++17and-std=c++20).Local verification
Fresh isolated builds from
origin/main, ccache disabled.import itk+fork()Threading models,
Module_ITKTBB=ONand OFF, all of Platform/Pool/TBB/Single: identical to main. Under a breakpoint onThreadPool::ThreadPool, the TBB threader never constructs a pool and the Pool threader constructs exactly one — lazy creation is preserved, so non-Pool builds pay nothing.Warm-path
GetInstance()is at parity withcall_once(~100–150 ns/call at 4 and 16 contending threads).Not built here: macOS and Windows. The change adds no platform-specific code; the only conditional is the pre-existing
ITK_USE_PTHREADSguard aroundpthread_atfork.