Flow 2.0.0
Flow project: Full implementation reference.
thread_lcl_ptr.hpp
Go to the documentation of this file.
1/* Flow
2 * Copyright 2023 Akamai Technologies, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the
5 * "License"); you may not use this file except in
6 * compliance with the License. You may obtain a copy
7 * of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in
12 * writing, software distributed under the License is
13 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
14 * CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing
16 * permissions and limitations under the License. */
17
18/// @file
19#pragma once
20
22#include <boost/core/noncopyable.hpp>
23#include <boost/thread/tss.hpp>
24
25namespace flow::util
26{
27// Types.
28
29/**
30 * `boost::thread_specific_ptr` replacement (augmentation) whose fast-path `.get()` touches only `thread_local`s
31 * and an advanced hash-map, avoiding the price of the `thread_specific_ptr`s use of OS threads API and
32 * a relatively slow `std::map` lookup.
33 *
34 * In terms of contract, it is identical to `boost::thread_specific_ptr` -- including, at times crucially, the
35 * cleanup algorithm and *when, for a given thread, it runs for all items*.
36 *
37 * ### Rationale: Performance ###
38 * In terms of performance, we suspect it is superior for most use-cases; but there is a trade-off. Ignoring the
39 * memory cost, the trade-off is as follows.
40 * - get(), outside of rare invocations very near the end of a thread or `exit()`ing program, is faster.
41 * Boost's (as of June 2026, 1.91) involves -- taking Linux as the representative example -- a `pthread_`
42 * API call to access TLS via TLS-key; then a lookup in an old-school `std::map`. `*this` will, instead:
43 * - Access a `thread_local bool`. This is new but very cheap.
44 * - Access a `thread_local` map (replaces the `pthread_` or equivalent OS-call). This will probably
45 * end up a single instruction: much better in absolute terms, all else being equal.
46 * - Perform a `boost::unordered_flat_map` lookup, using a previously generated 64-bit ID as opposed to
47 * `this` (probably somewhat better hashing behavior). Overall: better.
48 * - However, the other basic operation -- reset() -- is strictly slower: in fact it generally performs
49 * internally *both* a `thread_specific_ptr::reset()` *and also* operations to update our new fast-access
50 * stuff.
51 *
52 * For most use-cases this is better. Moreover, in some use-cases we've encountered it is better in a
53 * salient, if unspectacular, way. (A rudimentary benchmark is in our test suite; representative x86-64 Linux result:
54 * get() ~1.4x faster than `boost::thread_specific_ptr`'s with 1 live instance, ~2x with 16 -- ours stays flat
55 * as instance-count grows, theirs degrades -- while reset() is generally ~1.8x slower, as expected per above.)
56 *
57 * However! Let us be fair and precise about the justification, because in absolute terms
58 * `boost::thread_specific_ptr` on a modern Linux stack is *better* than its rep (along the lines of
59 * "syscall is involved, slow") suggests: ~7-11ns per get() in the aforementioned benchmark runs. For the vast majority
60 * of use-cases that is negligible, and developing Thread_local_ptr would be over-engineering. The specific
61 * circumstances in which `*this` could be worthwhile:
62 * - *Hot paths*: `get()`s executed at millions-per-second rates, inside operations whose total budgets are
63 * themselves tens of nanoseconds (in our history: per-deallocation and per-pointer-dereference lookups
64 * in the SHM-heap machinery of our sister project Flow-IPC) -- there a several-ns lookup is a
65 * double-digit percentage of the entire operation.
66 * - *Occupancy scaling*: both impls keep one map per thread shared by all instances process-wide; the
67 * `thread_specific_ptr` lookup cost grows with total instance count (7 -> 11ns going 1 -> 16 in our
68 * runs), while ours stays flat. So the gap widens, as a program grows busier.
69 * - *Cache realism*: a tight benchmark loop flatters the `std::map` (its nodes stay pinned in L1 cache);
70 * interleaved with real, cache-hostile work, a pointer-chasing tree re-misses in ways a single-probe
71 * flat-map does not. We therefore *suspect* the field gap exceeds the naively-benchmarked one (we have
72 * not measured that).
73 *
74 * Generalizing/speculating: if the effort is taken to make something thread-local, as opposed to
75 * using a cross-thread datum with a mutex, then perf is usually already a concern. Sometimes the additional cycles
76 * involved in the `thread_specific_ptr` fast-path are significant. Using `*this` should not hurt, at least,
77 * and provides a perf backstop with no contract differences.
78 *
79 * ### A corner case ###
80 * See the warning in the ~Thread_local_ptr() dtor doc header; in short, as with `thread_specific_ptr`, it is
81 * not formally allowed to have a *relevant thread* (other then the dtor-calling one) at dtor time, where
82 * relevant thread means one with `.get() != nullptr`. That's the official contract. Moreover this contract
83 * affects Thread_local_state_registry (which uses a `*this`); in that thing if there's a different extant
84 * thread at ctor time, for which `.this_thread_state_or_null() != nullptr`, there is also possible instability.
85 *
86 * We now briefly discuss what actually can happen if one disregards this. Firstly, assuming no stability
87 * issue, `cleanup_func()`/`delete ...` will not immediately (at dtor time) run for the relevant threads.
88 * Instead it will occur at thread-exit time for each relevant thread. (That might be acceptable.
89 * Thread_local_state_registry provides cross-thread cleanup in this situation; so it is timelier.) So with that,
90 * we're left with this scary instability.
91 *
92 * The reason is as follows. Firstly: temporarily ignore Thread_local_ptr itself and consider its internal
93 * core: `boost::thread_specific_ptr` (`tsp`). `tsp`, per thread, maintains a map from `this` pointers to the user
94 * TLS pointers. Dtor deletes the entry for its own `this` but only from the calling thread's TL-map; the
95 * others stay around. This in itself is fine; it just means the cleanup now must occur no earlier than
96 * each given thread's exit; it cannot be triggered via `x.reset()` anymore; as `x` is gone via dtor.
97 * The problem is that `this` as a key is not unique across time, and it is possible another `tsp` with the
98 * same address springs into existence: Now things can be unstable. If this new `y` does `y.reset(a)`,
99 * the `.reset()` code will erroneously think there is already a thing in this thread... but that's a different
100 * thing; it might be of a different type for example, and the `cleanup_func()` might crash if it tries to
101 * free it. `y.get()` will also wrongly yield non-null; basically at that point it's just chaos.
102 *
103 * Now... Thread_local_ptr actually, in maintaining its fast-access map/cache, internally avoids this problem
104 * by using a truly unique ID instead. So -- at least up to the start of on-thread-exit `thread_local` deinit --
105 * get() (by itself) is safe. reset(), though, unfortunately is not -- because it maintains (internally) both
106 * the fast-access map/cache *and* the internal `tsp`; the latter (as we know from above) is unsafe. Plus it
107 * interacts with any other `tsp` operating in the same thread, potentially.
108 *
109 * @todo Produce a modified version of `boost::thread_specific_ptr` wherein instead of using `this`
110 * as the TL-map-key internally, it uses a 64-bit unique ID a-la our `Thread_local_ptr`'s fast-map/cache.
111 * It can keep all its other code -- perhaps reduced to target C++17/compiler/OS we want to support -- but otherwise
112 * stay as similar as possible, to retain its cleanup-timing guarantees et al and keep busy-work low.
113 *
114 * @todo Once the preceding to-do (w/r/t a more-stable `boost::thread_specific_ptr`) is done, trivially adjust
115 * Thread_local_ptr to internally use that one instead of the less-stable one; the resulting Thread_local_ptr
116 * will continue to be faster on get() than Boost-guy; and it'll also inherit the lack of the stability corner case.
117 *
118 * @internal
119 * Impl strategy
120 * -------------
121 * First a digression in the form of advice:
122 * In general we note that this particular topic can be akin to a siren's call; things seem not-too-complex;
123 * C++'s `thread_local` support is solid; and the lack of concurrency, by definition, also makes it much
124 * easier to reason about various things. This is probably a misleading impression, we've found, so just
125 * hand-wavily speaking we recommend caution, especially when it comes
126 * to *reimplementing parts of `thread_specific_ptr`*; it is indeed a fairly aged chunk of code, and there
127 * can be a temptation to assume it just hasn't kept up with the times -- but that is probably not exactly the
128 * case. Those guys knew what they were doing, and it is battle-tested. Attempts to simplify/replace things
129 * about it tend to result in wasted time and giving up. One source of trouble is that arguably the motivations
130 * behind this or that decision are not necessarily explained in the source code -- a big one being, e.g.,
131 * "why not just use `thread_local`" (the answer "it wasn't available at the start, and they've decided not to
132 * change a working thing" seems natural -- but, turns out, that's probably not it or at least not *all* of it).
133 * So then one tries to assume answers not in evidence, tries to do it one's own way, and finds out that is
134 * not so simple.
135 *
136 * (Being *slightly* more specific, the trouble tends to begin around cleanup and around thread
137 * exit and/or `std::exit()`.)
138 *
139 * That said let's get into it. As for the scary warning above, we discuss related stuff briefly below
140 * under "How we didn't do it."
141 *
142 * ### How we did it ###
143 * As is probably implied in the public-facing section above, this really wraps a `boost::thread_specific_ptr`;
144 * plus in every reset() we additionally save the same thread-local information in our own data structure -- this
145 * time `static thread_local`. The `boost` impl's data structure is `std::map<>` from `this` itself to
146 * the user's `new_value` (passed to `reset()`) -- it should be noted, merged among all instantiations of
147 * the class template, one map (per thread). We mimic this with two modifications:
148 * - The map type is a faster map type.
149 * - The key is a per-`*this` 64-bit ID from Unique_id_holder; this is likely to have better hashing
150 * properties than pointer values (but at least not worse; the cost for generating these IDs is negligible).
151 *
152 * An internal-use class Thread_local_ptr_cache encapsulates this additional data structure and its updates
153 * and queries; `*this` code and Thread_local_ptr_cache's code are in cooperation. So that is #m_cache; while
154 * the nearby #m_tsp is the canonical `thread_specific_ptr`.
155 *
156 * @note We stress that #m_cache is 100% oblivious of `cleanup_func()` or equivalent. It does not whatsoever
157 * participate in executing that. It is just for storing #Thread_local_state in a fast-`get()`table way.
158 *
159 * There is a corner case, which may seem obscure -- but we assure you ignoring it can be quite fatal -- which
160 * has to do with a certain gap around thread exit. Suppose you're running an `std::thread` (not a `boost::thread`;
161 * similar API, different impl; and note there is no `std::thread_specific_ptr` -- only `boost::`).
162 * The gap: past a `static thread_local`'s deinit but before
163 * `thread_specific_ptr`'s `pthread_`-or-equivalent-arranged per-thread deinit. The cleanup (`delete ...` or
164 * `cleanup_func()` as given to ctor) occurs during the latter phase which creates a potential problem.
165 * Without detailing the various bad things that can happen in practice, the bottom line is simply that
166 * get() or even reset() might be required during that gap -- quite possibly (we've seen empirically
167 * even) *during* `cleanup_func()` and/or #Thread_local_state dtor.
168 *
169 * Our tactic for dealing with this is straightforward: determine when this (for a given thread) gap begins;
170 * and simply do not access #m_cache after that. This should present no problem simply because if
171 * reset() or get() is required, one can simply forward to #m_tsp. The get() is slower, naturally, but at
172 * that point it hardly matters.
173 *
174 * Now: consider a `boost::thread` thread. Things change somewhat. `boost::thread`'s thread-body cooperates
175 * with `boost::thread_specific_ptr`'s cleanup duties; as a result the `cleanup_func()`s-or-`delete`s stage
176 * happens *before* `thread_local` deinit. (The `pthread`-or-equivalent hook is nullified by `boost::thread`
177 * code, so that part won't happen again after `thread_local` deinit.) The danger that cleanup-or-delete
178 * code could rely on some `thread_local` which would be gone already goes away with a `boost::thread` thread.
179 *
180 * Naturally we have to work with both, as `boost::thread_specific_ptr` does work with both. That brings us to
181 * the exact mechanism we use to mark that `m_caches` is *inoperative*. This is all per-thread. The mechanism:
182 * a `thread_local bool`: Thread_local_ptr_cache::s_this_thread_global_tl_state_operative; starts `true`; once
183 * it is `false` -- we only use the `boost::thread_specific_ptr`.
184 *
185 * The question is when to set it. Pleasantly, `boost::this_thread::at_thread_exit(F)` registers `F`, so that
186 * `F()` is called at thread exit (along with other so-registered functions in some order), just before the
187 * `thread_specific_ptr` cleanups execute. (If it is an `exit()`ing thread -- such as `main()` returning -- and
188 * the `boost::thread_specific_ptr` cleanups don't run, then the at-thread-exit functions won't run either.)
189 * So: We register an `F()` that sets the *inoperative* flag. *First* the flag is set. *Second* the cleanups
190 * run.
191 * - If this occurs after `thread_local` deinit (`std::thread`):
192 * Good: by then the Thread_local_ptr_cache's `thread_local` fast-access map is gone, so trying to check
193 * anything in there is bad. Cleanup functions/dtors accessing `.get()` or even `.reset()`-to-non-null
194 * will forward entirely to the `thread_specific_ptr`. In addition: we must remember: either before or
195 * after calling a `cleanup_func()`-or-`delete`, the Boost code will -- a-la `.reset()` -- delete the
196 * entry in its internal TL-map. It won't actually *call* *our* `.reset()` though. If it did the result
197 * would be the same though; the flag indicates *inoperative* by then. Good.
198 * - If this occurs before `thread_local` deinit (`boost::thread`):
199 * Neutral: by then the Thread_local_ptr_cache's `thread_local` fast-access map is still there, so checking
200 * if there would work fine -- but we would have set the flag to *inoperative*, so we won't touch it anyway.
201 * Slight perf hit, but it's only around the cleanup phase -- no problem. Read on though!
202 * Cleanup functions/dtors accessing `.get()` or even `.reset()`-to-non-null
203 * will forward entirely to the `thread_specific_ptr`. Again... neutral, neither good nor bad. BUT!
204 * Again: Either before or after calling a `cleanup_func()`-or-`delete`, the Boost code will -- a-la `.reset()` --
205 * delete the entry in its internal TL-map. Again: It won't actually *call* *our* `.reset()` though.
206 * This is where our having already set the flag to *inoperative* is not neutral but required. Suppose
207 * we had not. A `cleanup_func()` might delete entry A from the map -- without calling
208 * Thread_local_ptr::reset()! So now there is a stale entry A in our cache-map, probably pointing to something
209 * now deleted (by `cleanup_func()`). No problem yet... but suppose entry B's `cleanup_func()` now runs...
210 * and performs a `.get()` for A. The flag is set to *operative* still in this what-if. The `.get()` will
211 * return non-null: use-after-free (stale entry).
212 *
213 * So that is why we use `at_thread_exit()` in Thread_local_ptr_cache. An easy bug would be to do it (set flag
214 * to *inoperative*) only when the TL-state containing our fast-map is being destroyed. Sure, in a sense the
215 * flag would be more accurate: it would reflect whether the fast-map (cache) is usable. However it would
216 * cause the bug described in the 2nd bullet above: Cleanup for A might leave a stale entry for A in the
217 * cache-map, diverging from `thread_specific_ptr`; then cleanup for B might use-after-free by still "seeing"
218 * A as set.
219 *
220 * ### How we did not do it ###
221 * While there are various approaches, the most tempting idea is a full reimpl that entirely bypasses
222 * (1) `pthread_` or equivalent or (2) anything that uses it; and instead relies completely on `thread_local`.
223 * To sketch it out:
224 * - Write a non-template impl-class that type-erases `Thread_local_state`, stores `void*`s, and handles
225 * the type-safe call of `cleanup_func(Thread_local_state*)` using a type-erasure trick.
226 * (`function<>` is probably easiest, and the perf should be pretty good -- and in any case well outside
227 * the fast-path, get().) The impl problem then reduces to designing this impl-class:
228 * - Have a `struct Global_tl_state {}` containing the desired hash-map, just as in
229 * Thread_local_ptr_cache. Can also use save some thread-local sequence numbers per node in this map,
230 * so cleanup can be invoked in reverse-chronological order.
231 * - Keep a simple `thread_local Global_tl_state`. In its destructor -- which should run at thread exit
232 * or `std::exit()` -- perform the cleanup algorithm.
233 * - To mimick the as-of-this-writing algorithm in `thread_specific_ptr` the cleanup algorithm must wrestle
234 * with the fact that a `cleanup_func()` can add still more things to clean-up; so the algorithm must
235 * keep cycling through the list, until it is empty.
236 * - The seq num thing is optional, really, but it might be a good way to keep things more deterministic
237 * and less churn-y.
238 *
239 * All of this requires some care but is eminently doable. In fact we've done it. It is even arguably true
240 * that the resulting this is generally usable. However:
241 *
242 * It may be *usable*, but it is *not* a `thread_specific_ptr` replacement. It's close, but it has a different
243 * behavior. The different behavior, that we know of, is at least as follows.
244 *
245 * Above we mention how `thread_specific_ptr` cleanup runs either before `thread_local` deinit or after it:
246 * `boost::thread` versus `std::thread` at least. In the above sketch, though, we stipulated running
247 * cleanup *as part of* `thread_local` deinit.
248 * - Boost's impl shall run `cleanup_func()` or `delete ...` strictly after all that thread's `thread_local`s
249 * have been destroyed; or strictly before them.
250 * - The hypothetical replacement would, instead, run cleanup *during* that thread's `thread_local` deinit.
251 * - If cleanup touches some `thread_local`: it might work... or it might not work.
252 * - More to the point: it is simply earlier, or later (depending)! That is not the same behavior! One
253 * could even say that's better; or not; doesn't matter. The point is: Boost's just runs "before" or "after"
254 * things, deterministically(ish?), while the alternative runs "during" things, less deterministically.
255 *
256 * Could one deal with it? Probably yes -- though it is no joke actually -- there are techniques one can use
257 * to affect `thread_local` deinit order. With `thread_specific_ptr` (and therefore Thread_local_ptr) there is
258 * just no need. Maybe that's fine; but it is not a replacement; and code that relies on the Boost behavior
259 * might start being unstable, particularly around thread or program exit time. Then it is a can of worms.
260 *
261 * ### Another approach ###
262 * We've mentioned the issue, inherited from `thread_specific_ptr`, wherein having a *relevant thread*
263 * (one with non-null get()) extant -- excluding the calling thread -- at dtor time means undefined behavior,
264 * due to `tsp`'s using `this` as a map-key. There's a to-do about resolving it earlier up; and it is saying
265 * to fix-up `tsp` (our #m_tsp in particular would gain) which would fix it up for Thread_local_ptr too.
266 * A reasonable approach.
267 *
268 * There is a related line of thought we could follow, and it would have the benefit of speeding-up
269 * reset() and not having to worry about `tsp` code anymore. The basic insight is simple: the `thread_local`
270 * Thread_local_ptr_cache::Global_tl_state is perfectly sufficient for most of the lifetime of a `*this`.
271 * That's how the "How we did not do it" prototype was written. It eliminates the key-clash problem too.
272 * All it lacks is the cleanup "prowess" of `tsp`; and even then it is only about timing of when the cleanup is
273 * launched on thread exit. Doing it in `~Global_state()` is simply too early/too late -- but we could probably
274 * rig `pthread_` and/or `at_thread_exit()` calls easily-enough; basically set-up what `tsp` sets up.
275 * The required information is all available -- ultimately it is a list of
276 * cleanup-functions to call -- and `pthread_`/`boost::thread` API can perhaps be rigged to do it. The details
277 * of that are likely the riskiest aspect here, so it'd be best to develop/prototype/sanity-check that part first.
278 * Do recall that a `cleanup_func()` or `~Thread_local_state()` can do `.reset(non_null_value)` -- creating
279 * more cleanup work to do immediately, but more to the point `.reset()` has to still work at that stage,
280 * when there is no more `Global_state` or indeed any more `thread_local` mechanics available.
281 * @endinternal
282 *
283 * @tparam Thread_local_state_t
284 * See #Thread_local_state.
285 * */
286template<typename Thread_local_state_t>
288 private boost::noncopyable
289{
290public:
291 // Types.
292
293 /**
294 * Pointer-to-this-type is stored thread-locally; and #cleanup_func_t takes an argument of pointer-to-this-type.
295 * Requirements: See `boost::thread_specific_ptr`. In short there are almost none.
296 */
297 using Thread_local_state = Thread_local_state_t;
298
299 /// Type for `cleanup_func_t()` to ctor: takes `Thread_local_state*`; must not throw.
301
302 // Constructors/destructor.
303
304 /// Equivalent to `Thread_local_ptr(f)`, where `f(t)` performs `delete t;`.
306
307 /**
308 * Equivalent to `boost::thread_specific_ptr`: memorizes the given function pointer. When cleanup executes
309 * the following occurs: if `get()` is null: no-op; if `cleanup_func` is null: no-op; otherwise
310 * `cleanup_func(get())`. The following are all the cleanup points:
311 * - In `reset(new_value)`, if get() is not null and does not equal `new_value`.
312 * - In dtor, if get() is not null.
313 *
314 * @note In general we recommend perusing the Boost docs when it comes to cleanup algorithms; the above are
315 * the basics, but there is more subtlety to it than that. (E.g., `cleanup_func()` might add more
316 * things to `cleanup_func()`... and so forth.)
317 * @note Also it can throw (system error); so we can throw.
318 *
319 * @param cleanup_func
320 * See above. Reminder: null means no cleanup.
321 */
322 explicit Thread_local_ptr(cleanup_func_t cleanup_func);
323
324 /**
325 * Destructor equivalent to that of `boost::thread_specific_ptr`.
326 * In short it performs reset() (which is a no-op if get() is null) but -- as always -- can only affect the
327 * current thread. For other threads, if get() is not null, behavior is formally undefined -- see
328 * Boost docs -- though at least in Linux what actually happens is that `get()` leaks (is not cleaned-up)
329 * ever, modulo resource reclamation at program exit.
330 *
331 * @note In general we recommend perusing the Boost docs when it comes to the dtor; the above are
332 * the basics, but there is more subtlety to it than that. (E.g., what about the main, `exit()`ing thread?)
333 *
334 * @warning As in `boost::thread_specific_ptr`, the formal contract is that the following results in
335 * undefined behavior: At dtor, there are *relevant threads* (threads for which get() is non-null) other
336 * than the current thread. Hence for full safety either store `*this` as `static` or global; or
337 * join all relevant threads before dtor; or make them all irrelevant (reset()) (same thing really).
338 */
340
341 // Methods.
342
343 /**
344 * Equivalent to `boost::thread_specific_ptr`, potentially forgets/cleans current get(); potentially
345 * records `new_value` instead. To restate the Boost behavior: let `old_value = get()`:
346 * - `old_value == new_value` => no-op (including when non-null). Else:
347 * - If `!old_value`: get() is made to equal null; then cleanup of `old_value` executes.
348 * - If `new_value` truthy: get() is made to equal `new_value`.
349 *
350 * @note `boost::thread_specific_ptr::reset()` can throw (system error); so we can throw.
351 * Generally that means `*this`, at least, is kaput. Probably it's generally chaos in this thread too.
352 *
353 * @param new_value
354 * See above.
355 */
356 void reset(Thread_local_state* new_value = nullptr);
357
358 /**
359 * Equivalent to `boost::thread_specific_ptr`, returns the value last passed in this thread to
360 * reset(); or null if it has not been called.
361 *
362 * @note The performance should be significantly better than Boost's, for this operation.
363 *
364 * @return See above. May well be null.
365 */
367
368 /**
369 * It's get().
370 * @return See above.
371 */
373
374 /**
375 * It's *(get()). Behavior is undefined if get() is null; assertion may trip.
376 * @return See above.
377 */
379
380 /**
381 * Equivalent to `boost::thread_specific_ptr`, potentially forgets -- but does not clean as via `cleanup_func()` --
382 * current get(), so that get() returns null; and returns the pre-condition value of get().
383 * - If pre-condition get() is null: no-op.
384 * - Either way returns pre-condition get().
385 *
386 * @return Pre-condition get() (possibly null).
387 */
389
390private:
391 // Data.
392
393 /**
394 * The canonical `boost::thread_specific_ptr`. It is declared before #m_cache; hence most saliently its
395 * dtor runs before that of #m_cache.
396 *
397 * ### Rationale for order of `m_tsp` versus `m_cache` ###
398 * This should not matter (much), that we can tell anyway, but the order `tsp`-then-`cache`
399 * seems least entropy-laden based on the following reasoning:
400 *
401 * The crux-y op is reset() which in the non-null, `old_value != new_value` case does the following:
402 * -# `cache`: remove pair `this, old_value` from internal map FASTMAP;
403 * -# `tsp`: `.reset(new_value)` which internally is probably:
404 * -# remove pair `this, old_value` from internal map SLOWMAP;
405 * -# invoke cleanup of `old_value`;
406 * -# add pair `this, new_value` to internal map SLOWMAP;
407 * -# `cache`: add pair `this, new_value` to internal map FASTMAP.
408 *
409 * (If `new_value` is null then skip the last two bullets.) Fine; the member order isn't relevant to that per se.
410 * Now consider the `tsp` and `cache` dtors; each does this:
411 * -# (both) remove pair `this, old_value` from internal map ...MAP;
412 * -# (tsp only) invoke cleanup of released thing.
413 * So if we set the order here as `tsp`, `cache`, then *our* dtor will do (referring to the above):
414 * -# `cache`: remove pair `this, old_value` from internal map FASTMAP;
415 * -# `tsp`: remove pair `this, old_value` from internal map SLOWMAP;
416 * -# `tsp`: invoke cleanup of released thing.
417 *
418 * That matches `reset(nullptr)`; and the dtor conceptually is to do reset() by contract. So that's probably best...
419 * or at least not worse.
420 */
421 boost::thread_specific_ptr<Thread_local_state> m_tsp;
422
423 /// The `thread_local`-oriented duplication of information in #m_tsp. See our class doc header.
425}; // class Thread_local_ptr
426
427// Template implementations.
428
429template<typename Thread_local_state_t>
431 m_tsp(cleanup_func)
432{
433 // That's all. Real action starts, potentially, in reset().
434}
435
436template<typename Thread_local_state_t>
438
439template<typename Thread_local_state_t>
440Thread_local_ptr<Thread_local_state_t>::~Thread_local_ptr() = default; // See comment about member order near m_tsp.
441
442template<typename Thread_local_state_t>
444{
445 using Reset_result = Thread_local_ptr_cache::Reset_result;
446
447 /* See comment about member order near m_tsp; it shows the overall order of ops as a result of the following.
448 * The main anti-entropy principles going into that are as follows.
449 * - Let old_value = get(); might be null. Recall that `new_value == old_value` is possible -- means no-op --
450 * and both `*_value = nullptr` is possible. Assume for now `new_value != old_value`.
451 * - Split reset(new_value) into 3 parts:
452 * - release() equivalent (delete old_value)
453 * - cleanup w/r/t old_value
454 * - insert new_value.
455 * - An anti-chaos point is that the cleanup-step occurs with the pre-condition that this->get() is null
456 * already. Like maybe we are reset(null), while cleanup_func() does reset(non_null_state) itself --
457 * that would work; whereas if cleanup happened first, there could be some recursive chaos. No need to
458 * overthink the possibilities there; we are just pointing out this way is more controlled at least.
459 *
460 * Right, so since we need to maintain the canonical (but slow-get()-impl-having) m_tsp *and*
461 * the touchier-around-deinit (but fast-get()-impl-having) m_cache, we can implement the above steps in the
462 * way shown in the aforementioned comment in m_tsp doc header. That's what you see below, modulo having
463 * to also deal with the possibility m_cache is no longer usable (its TLS has been destroyed: thread exiting or
464 * std::exit()ing) and `new_value == old_value` (no-op). */
465
466 // Phase 1: delete old_value from m_cache, m_tsp (assuming m_cache is operative and new_value is not a dupe).
467
468 // Phase 1, part 1/2: delete old_value from m_cache (unless m_cache is inoperative, or new_value is a dupe).
469 const auto cache_result = m_cache.reset_release(new_value);
470 if (cache_result == Reset_result::S_INOPERATIVE)
471 {
472 /* Phase 1.1 no-oped, because m_cache can no longer operate (we must be near thread exit or exit()).
473 * Therefore short-circuit the whole operation (all phases) to simply defer to m_tsp which is canonically
474 * correct. */
475 m_tsp.reset(new_value);
476 return;
477 }
478 /* else if (m_cache is operative; and we can trust what it reported, at least through the current method;
479 * in particular m_cache remains operative through all below): */
480
481 if (cache_result == Reset_result::S_DUPE)
482 {
483 return; // Per contract this means no-op. Since m_cache is accurate, we can skip m_tsp.reset(new_value) too.
484 }
485 // else if (m_cache is operative, and old_value-if-any has been deleted from it; now should be smooth sailing):
486
487 /* Phase 1, part 2/2: delete old_value from m_tsp (if it's there). +
488 * Phase 2: cleanup_func(old_value), if old_value was just there. +
489 * [ Phase 3: insert new_value into m_tsp, m_cache. ]
490 * Phase 3, part 1/2: insert new_value into m_tsp (unless new_value is falsy). */
491 m_tsp.reset(new_value);
492 // ^-- Can throw. As advertised in that case *this should be abandoned. So just let it happen. @todo Revisit?
493
494 // Phase 3, part 2/2: insert new_value into m_cache (unless new_value is falsy). */
495 if (new_value)
496 {
497 m_cache.reset_set(new_value);
498 }
499} // Thread_local_ptr::reset()
500
501template<typename Thread_local_state_t>
503{
504 const auto cache_result_or_inoperative = m_cache.get();
505 return cache_result_or_inoperative
506 // Got the result real-fast. This eventuality is the reason this class template exists.
507 ? static_cast<Thread_local_state*>(*cache_result_or_inoperative)
508 // m_cache inoperative (thread must be exiting or exit()ing), so just fallback to "slow" guy.
509 : m_tsp.get();
510}
511
512template<typename Thread_local_state_t>
514{
515 return get();
516}
517
518template<typename Thread_local_state_t>
520{
521 Thread_local_state* const value = get();
522 assert(value && "Used against contract.");
523 return *value;
524}
525
526template<typename Thread_local_state_t>
528{
529 // Shouldn't matter but mimic order from reset() (phases 1.1, 1.2).
530 m_cache.release();
531 return m_tsp.release();
532}
533
534} // namespace flow::util
Internal-use helper for Thread_local_ptr which encapsulates that guy's thread_locally-accessed TLS pa...
Reset_result
Encodes result of reset_release(), indicating what the calling Thread_local_ptr::reset() should do ne...
boost::thread_specific_ptr replacement (augmentation) whose fast-path .get() touches only thread_loca...
Thread_local_state * operator->() const
It's get().
Thread_local_state * release()
Equivalent to boost::thread_specific_ptr, potentially forgets – but does not clean as via cleanup_fun...
void(*)(Thread_local_state *) cleanup_func_t
Type for cleanup_func_t() to ctor: takes Thread_local_state*; must not throw.
Thread_local_state_t Thread_local_state
Pointer-to-this-type is stored thread-locally; and cleanup_func_t takes an argument of pointer-to-thi...
Thread_local_ptr()
Equivalent to Thread_local_ptr(f), where f(t) performs delete t;.
~Thread_local_ptr()
Destructor equivalent to that of boost::thread_specific_ptr.
boost::thread_specific_ptr< Thread_local_state > m_tsp
The canonical boost::thread_specific_ptr.
void reset(Thread_local_state *new_value=nullptr)
Equivalent to boost::thread_specific_ptr, potentially forgets/cleans current get(); potentially recor...
Thread_local_state * get() const
Equivalent to boost::thread_specific_ptr, returns the value last passed in this thread to reset(); or...
Thread_local_state & operator*() const
It's *(get()).
Thread_local_ptr_cache m_cache
The thread_local-oriented duplication of information in m_tsp. See our class doc header.
Thread_local_ptr(cleanup_func_t cleanup_func)
Equivalent to boost::thread_specific_ptr: memorizes the given function pointer.
Flow module containing miscellaneous general-use facilities that don't fit into any other Flow module...
The actual user Thread_local_state stored per thread as lazily-created in this_thread_state(); plus a...
Definition: thread_lcl.hpp:660