|
Flow 3.0.0
Flow project: Public API.
|
boost::thread_specific_ptr replacement (augmentation) whose fast-path .get() touches only thread_locals and an advanced hash-map, avoiding the price of the thread_specific_ptrs use of OS threads API and a relatively slow std::map lookup.
More...
#include <thread_lcl_ptr.hpp>
Inherits boost::noncopyable.
Public Types | |
| using | Thread_local_state = Thread_local_state_t |
| Pointer-to-this-type is stored thread-locally; and cleanup_func_t takes an argument of pointer-to-this-type. More... | |
| using | cleanup_func_t = void(*)(Thread_local_state *) |
Type for cleanup_func_t() to ctor: takes Thread_local_state*; must not throw. | |
Public Member Functions | |
| Thread_local_ptr () | |
Equivalent to Thread_local_ptr(f), where f(t) performs delete t;. | |
| Thread_local_ptr (cleanup_func_t cleanup_func) | |
Equivalent to boost::thread_specific_ptr: memorizes the given function pointer. More... | |
| ~Thread_local_ptr () | |
Destructor equivalent to that of boost::thread_specific_ptr. More... | |
| void | reset (Thread_local_state *new_value=nullptr) |
Equivalent to boost::thread_specific_ptr, potentially forgets/cleans current get(); potentially records new_value instead. More... | |
| Thread_local_state * | get () const |
Equivalent to boost::thread_specific_ptr, returns the value last passed in this thread to reset(); or null if it has not been called. More... | |
| Thread_local_state * | operator-> () const |
| It's get(). More... | |
| Thread_local_state & | operator* () const |
| It's *(get()). More... | |
| Thread_local_state * | release () |
Equivalent to boost::thread_specific_ptr, potentially forgets – but does not clean as via cleanup_func() – current get(), so that get() returns null; and returns the pre-condition value of get(). More... | |
boost::thread_specific_ptr replacement (augmentation) whose fast-path .get() touches only thread_locals and an advanced hash-map, avoiding the price of the thread_specific_ptrs use of OS threads API and a relatively slow std::map lookup.
In terms of contract, it is identical to boost::thread_specific_ptr – including, at times crucially, the cleanup algorithm and when, for a given thread, it runs for all items.
In terms of performance, we suspect it is superior for most use-cases; but there is a trade-off. Ignoring the memory cost, the trade-off is as follows.
exit()ing program, is faster. Boost's (as of June 2026, 1.91) involves – taking Linux as the representative example – a pthread_ API call to access TLS via TLS-key; then a lookup in an old-school std::map. *this will, instead:thread_local bool. This is new but very cheap.thread_local map (replaces the pthread_ or equivalent OS-call). This will probably end up a single instruction: much better in absolute terms, all else being equal.boost::unordered_flat_map lookup, using a previously generated 64-bit ID as opposed to this (probably somewhat better hashing behavior). Overall: better.thread_specific_ptr::reset() and also operations to update our new fast-access stuff.For most use-cases this is better. Moreover, in some use-cases we've encountered it is better in a salient, if unspectacular, way. (A rudimentary benchmark is in our test suite; representative x86-64 Linux result: get() ~1.4x faster than boost::thread_specific_ptr's with 1 live instance, ~2x with 16 – ours stays flat as instance-count grows, theirs degrades – while reset() is generally ~1.8x slower, as expected per above.)
However! Let us be fair and precise about the justification, because in absolute terms boost::thread_specific_ptr on a modern Linux stack is better than its rep (along the lines of "syscall is involved, slow") suggests: ~7-11ns per get() in the aforementioned benchmark runs. For the vast majority of use-cases that is negligible, and developing Thread_local_ptr would be over-engineering. The specific circumstances in which *this could be worthwhile:
get()s executed at millions-per-second rates, inside operations whose total budgets are themselves tens of nanoseconds (in our history: per-deallocation and per-pointer-dereference lookups in the SHM-heap machinery of our sister project Flow-IPC) – there a several-ns lookup is a double-digit percentage of the entire operation.thread_specific_ptr lookup cost grows with total instance count (7 -> 11ns going 1 -> 16 in our runs), while ours stays flat. So the gap widens, as a program grows busier.std::map (its nodes stay pinned in L1 cache); interleaved with real, cache-hostile work, a pointer-chasing tree re-misses in ways a single-probe flat-map does not. We therefore suspect the field gap exceeds the naively-benchmarked one (we have not measured that).Generalizing/speculating: if the effort is taken to make something thread-local, as opposed to using a cross-thread datum with a mutex, then perf is usually already a concern. Sometimes the additional cycles involved in the thread_specific_ptr fast-path are significant. Using *this should not hurt, at least, and provides a perf backstop with no contract differences.
See the warning in the ~Thread_local_ptr() dtor doc header; in short, as with thread_specific_ptr, it is not formally allowed to have a relevant thread (other then the dtor-calling one) at dtor time, where relevant thread means one with .get() != nullptr. That's the official contract. Moreover this contract affects Thread_local_state_registry (which uses a *this); in that thing if there's a different extant thread at ctor time, for which .this_thread_state_or_null() != nullptr, there is also possible instability.
We now briefly discuss what actually can happen if one disregards this. Firstly, assuming no stability issue, cleanup_func()/delete ... will not immediately (at dtor time) run for the relevant threads. Instead it will occur at thread-exit time for each relevant thread. (That might be acceptable. Thread_local_state_registry provides cross-thread cleanup in this situation; so it is timelier.) So with that, we're left with this scary instability.
The reason is as follows. Firstly: temporarily ignore Thread_local_ptr itself and consider its internal core: boost::thread_specific_ptr (tsp). tsp, per thread, maintains a map from this pointers to the user TLS pointers. Dtor deletes the entry for its own this but only from the calling thread's TL-map; the others stay around. This in itself is fine; it just means the cleanup now must occur no earlier than each given thread's exit; it cannot be triggered via x.reset() anymore; as x is gone via dtor. The problem is that this as a key is not unique across time, and it is possible another tsp with the same address springs into existence: Now things can be unstable. If this new y does y.reset(a), the .reset() code will erroneously think there is already a thing in this thread... but that's a different thing; it might be of a different type for example, and the cleanup_func() might crash if it tries to free it. y.get() will also wrongly yield non-null; basically at that point it's just chaos.
Now... Thread_local_ptr actually, in maintaining its fast-access map/cache, internally avoids this problem by using a truly unique ID instead. So – at least up to the start of on-thread-exit thread_local deinit – get() (by itself) is safe. reset(), though, unfortunately is not – because it maintains (internally) both the fast-access map/cache and the internal tsp; the latter (as we know from above) is unsafe. Plus it interacts with any other tsp operating in the same thread, potentially.
boost::thread_specific_ptr wherein instead of using this as the TL-map-key internally, it uses a 64-bit unique ID a-la our Thread_local_ptr's fast-map/cache. It can keep all its other code – perhaps reduced to target C++17/compiler/OS we want to support – but otherwise stay as similar as possible, to retain its cleanup-timing guarantees et al and keep busy-work low.boost::thread_specific_ptr) is done, trivially adjust Thread_local_ptr to internally use that one instead of the less-stable one; the resulting Thread_local_ptr will continue to be faster on get() than Boost-guy; and it'll also inherit the lack of the stability corner case.| Thread_local_state_t | See Thread_local_state. |
| using flow::util::Thread_local_ptr< Thread_local_state_t >::Thread_local_state = Thread_local_state_t |
Pointer-to-this-type is stored thread-locally; and cleanup_func_t takes an argument of pointer-to-this-type.
Requirements: See boost::thread_specific_ptr. In short there are almost none.
|
explicit |
Equivalent to boost::thread_specific_ptr: memorizes the given function pointer.
When cleanup executes the following occurs: if get() is null: no-op; if cleanup_func is null: no-op; otherwise cleanup_func(get()). The following are all the cleanup points:
reset(new_value), if get() is not null and does not equal new_value.cleanup_func() might add more things to cleanup_func()... and so forth.) | cleanup_func | See above. Reminder: null means no cleanup. |
|
default |
Destructor equivalent to that of boost::thread_specific_ptr.
In short it performs reset() (which is a no-op if get() is null) but – as always – can only affect the current thread. For other threads, if get() is not null, behavior is formally undefined – see Boost docs – though at least in Linux what actually happens is that get() leaks (is not cleaned-up) ever, modulo resource reclamation at program exit.
exit()ing thread?)boost::thread_specific_ptr, the formal contract is that the following results in undefined behavior: At dtor, there are relevant threads (threads for which get() is non-null) other than the current thread. Hence for full safety either store *this as static or global; or join all relevant threads before dtor; or make them all irrelevant (reset()) (same thing really). | Thread_local_state_t * flow::util::Thread_local_ptr< Thread_local_state_t >::get |
Equivalent to boost::thread_specific_ptr, returns the value last passed in this thread to reset(); or null if it has not been called.
| Thread_local_state_t & flow::util::Thread_local_ptr< Thread_local_state_t >::operator* |
| Thread_local_state_t * flow::util::Thread_local_ptr< Thread_local_state_t >::operator-> |
It's get().
| Thread_local_state_t * flow::util::Thread_local_ptr< Thread_local_state_t >::release |
| void flow::util::Thread_local_ptr< Thread_local_state_t >::reset | ( | Thread_local_state * | new_value = nullptr | ) |
Equivalent to boost::thread_specific_ptr, potentially forgets/cleans current get(); potentially records new_value instead.
To restate the Boost behavior: let old_value = get():
old_value == new_value => no-op (including when non-null). Else:!old_value: get() is made to equal null; then cleanup of old_value executes.new_value truthy: get() is made to equal new_value.boost::thread_specific_ptr::reset() can throw (system error); so we can throw. Generally that means *this, at least, is kaput. Probably it's generally chaos in this thread too.| new_value | See above. |