What Is std::pmr::synchronized_pool_resource in C++?
This article provides an overview of
std::pmr::synchronized_pool_resource, a memory resource
introduced in C++17 under the Polymorphic Memory Resources (PMR)
library. It explains the core concepts behind pooled memory allocation,
details the internal mechanisms this class uses to guarantee thread
safety during concurrent allocation and deallocation requests, and
examines when and how developers should use it in multi-threaded C++
applications.
Understanding std::pmr::synchronized_pool_resource
In modern C++, standard allocators often suffer from performance
bottlenecks and memory fragmentation when frequently allocating and
deallocating objects of varying sizes. C++17 addressed this by
introducing polymorphic memory resources (std::pmr), which
decouple memory allocation strategies from container types through
run-time polymorphism.
std::pmr::synchronized_pool_resource is a specialized
implementation derived from std::pmr::memory_resource. It
manages a collection of memory pools, where each pool contains chunks of
a specific, uniform size. When an allocation request is made:
- Pool Allocation: If the requested size matches or fits within one of the pool's configured geometric size classes, the resource dispenses a pre-allocated chunk from that pool.
- Upstream Fallback: If the requested size exceeds
the maximum pool size, or if pool capacities are exhausted and cannot
expand, the resource delegates the allocation directly to an "upstream"
memory resource (such as
std::pmr::new_delete_resource()).
Deallocated memory returns to its respective pool for future reuse rather than immediately returning to the operating system, significantly reducing system-call overhead.
How It Protects Thread Safety
Unlike std::pmr::unsynchronized_pool_resource, which is
intended for single-threaded contexts or usage restricted by external
locking, std::pmr::synchronized_pool_resource provides
built-in thread safety.
1. Concurrent
allocate and deallocate Operations
The C++ standard mandates that calling allocate() and
deallocate() on the same instance of
std::pmr::synchronized_pool_resource concurrently from
multiple threads does not introduce data races. This thread-safety
guarantee is managed completely inside the resource implementation:
- Internal Mutexes and Locking: Standard library implementations typically employ internal mutexes or spinlocks to guard access to the individual pool freelists. When a thread requests or returns a block, access to that specific pool is serialized.
- Fine-Grained Locking: Instead of using a single global lock over the entire resource, implementations frequently utilize per-pool locks or bucket-level locks. This ensures that threads requesting memory of different sizes (such as 32 bytes versus 256 bytes) can allocate concurrently without blocking one another.
- Upstream Synchronization: When a sub-pool needs to
replenish its memory or when an oversized allocation bypasses the pools
entirely, the request goes to the upstream memory resource.
std::pmr::synchronized_pool_resourceensures that calls to the upstream allocator are also thread-safe, even if the upstream resource itself is not natively thread-safe.
2. Thread-Local Caching Optimization
High lock contention can severely degrade multi-threaded performance.
To mitigate this, many implementations of
std::pmr::synchronized_pool_resource incorporate small
thread-specific caches or thread-local storage (TLS) buffers:
- Each thread first attempts to fulfill an allocation or deallocation using its local cache without acquiring a lock.
- Only when a thread-local cache is empty (during allocation) or full (during deallocation) does the thread acquire an internal lock to transfer a batch of blocks between its local cache and the central shared pool.
- This batching strategy minimizes lock acquisition frequency and maximizes throughput across heavily concurrent workloads.
Scope of Thread Safety Guarantees
While member functions like allocate() and
deallocate() are safe for concurrent invocation, other
operations are not:
- Destruction: Destroying the
std::pmr::synchronized_pool_resourceobject while other threads are allocating, deallocating, or accessing it causes undefined behavior. All dependent threads must finish using the resource before it is destroyed. - The
release()Method: Therelease()member function, which deallocates all memory back to the upstream resource, is not thread-safe relative to ongoing allocations or deallocations and requires external synchronization.
When to Use It
std::pmr::synchronized_pool_resource is ideal for
multi-threaded applications where worker threads frequently create and
destroy short-lived nodes or objects of recurring sizes—such as in
std::pmr::map, std::pmr::list, or
std::pmr::vector. By centralizing allocations into
categorized pools and handling mutual exclusion internally, it combines
high allocation performance with clean, race-free concurrency.