Flow 3.0.0
Flow project: Public API.
Public Types | Public Member Functions | List of all members
flow::util::Thread_local_ptr< Thread_local_state_t > Class Template Reference

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...
 

Detailed Description

template<typename Thread_local_state_t>
class flow::util::Thread_local_ptr< Thread_local_state_t >

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.

Rationale: Performance

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.

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:

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.

A corner case

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.

Todo:
Produce a modified version of 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.
Todo:
Once the preceding to-do (w/r/t a more-stable 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.
Template Parameters
Thread_local_state_tSee Thread_local_state.

Member Typedef Documentation

◆ Thread_local_state

template<typename Thread_local_state_t >
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.

Constructor & Destructor Documentation

◆ Thread_local_ptr()

template<typename Thread_local_state_t >
flow::util::Thread_local_ptr< Thread_local_state_t >::Thread_local_ptr ( cleanup_func_t  cleanup_func)
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:

  • In reset(new_value), if get() is not null and does not equal new_value.
  • In dtor, if get() is not null.
Note
In general we recommend perusing the Boost docs when it comes to cleanup algorithms; the above are the basics, but there is more subtlety to it than that. (E.g., cleanup_func() might add more things to cleanup_func()... and so forth.)
Also it can throw (system error); so we can throw.
Parameters
cleanup_funcSee above. Reminder: null means no cleanup.

◆ ~Thread_local_ptr()

template<typename Thread_local_state_t >
flow::util::Thread_local_ptr< Thread_local_state_t >::~Thread_local_ptr ( )
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.

Note
In general we recommend perusing the Boost docs when it comes to the dtor; the above are the basics, but there is more subtlety to it than that. (E.g., what about the main, exit()ing thread?)
Warning
As in 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).

Member Function Documentation

◆ get()

template<typename Thread_local_state_t >
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.

Note
The performance should be significantly better than Boost's, for this operation.
Returns
See above. May well be null.

◆ operator*()

template<typename Thread_local_state_t >
Thread_local_state_t & flow::util::Thread_local_ptr< Thread_local_state_t >::operator*

It's *(get()).

Behavior is undefined if get() is null; assertion may trip.

Returns
See above.

◆ operator->()

template<typename Thread_local_state_t >
Thread_local_state_t * flow::util::Thread_local_ptr< Thread_local_state_t >::operator->

It's get().

Returns
See above.

◆ release()

template<typename Thread_local_state_t >
Thread_local_state_t * flow::util::Thread_local_ptr< Thread_local_state_t >::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().

  • If pre-condition get() is null: no-op.
  • Either way returns pre-condition get().
Returns
Pre-condition get() (possibly null).

◆ reset()

template<typename Thread_local_state_t >
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:
  • If !old_value: get() is made to equal null; then cleanup of old_value executes.
  • If new_value truthy: get() is made to equal new_value.
Note
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.
Parameters
new_valueSee above.

The documentation for this class was generated from the following file: