Flow 3.0.0
Flow project: Full implementation reference.
thread_lcl_obj.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 <type_traits>
23
24namespace flow::util
25{
26// Types.
27
28/**
29 * A `thread_local` object of type #Obj, in effect, but with death-aware access: this_thread_obj_or_null()
30 * returns the calling thread's instance -- or null, once that instance has been destroyed during the thread's
31 * `thread_local` deinit. Hence -- unlike a plain `thread_local Obj` at namespace/class/function scope -- it is
32 * safe to access from *any* code in the thread at *any* time: including from `thread_local` destructors during
33 * deinit (close to thread exit); and from `boost::thread_specific_ptr`-style cleanup (see Thread_local_ptr,
34 * Thread_local_state_registry), which -- depending on the thread type (to wit: non-`boost::thread` threads) --
35 * can run after deinit.
36 *
37 * @note It is only useful for #Obj types wherein `Obj` is *not trivially destructible*. If it is trivially
38 * destructible, then a simple `thread_local Obj` would already be usable at all times from its thread
39 * (including during deinit).
40 *
41 * To put it in terms of the problem being solved: a plain non-trivially-destructible `thread_local` dies at
42 * some hard-to-predict point during deinit; code running near thread death (usually in destructor(s) of
43 * thread-local object(s)) cannot know whether the object is still valid; and a wrong guess means undefined
44 * behavior. (It might also work with an `std::thread`-thread but then later explode when someone uses a
45 * `boost::thread` or otherwise; or vice versa; or who knows?) This class provides the missing knowledge, in the
46 * form of the null return.
47 *
48 * One does not instantiate a Thread_local_obj_deinit_safe itself; it is a static-access facility, singleton-like.
49 * The identity of the underlying per-thread object is the pair <#Obj, #Tag> (one object per thread per such
50 * combo). Use #Tag to obtain distinct storage, when the same #Obj type is needed for 2+ independent purposes.
51 *
52 * @note That semantic should not be too onerous, as a naked `thread_local` object can only be `static`
53 * or global in the first place. So, essentially, instead of identifying a `thread_local` by name, one
54 * identifies a Thread_local_obj_deinit_safe by the aforementioned `Obj`+`Tag` pair.
55 *
56 * ### How to use ###
57 * The semantics:
58 * - The first this_thread_obj_or_null() call in a thread lazily default-constructs that thread's #Obj.
59 * This matches what likely happens in practice for all `thread_local`s (for function-local ones: that is
60 * formally what happens, period).
61 * - Subsequent calls in that thread return the same pointer. This matches `thread_local` behavior too.
62 * - Once the object is destroyed -- during that thread's `thread_local` deinit, in reverse construction
63 * order versus other `thread_local`s, as usual -- calls return null; and shall keep doing so
64 * (no resurrection). A `thread_local` access would instead be undefined behavior.
65 * - Therefore: check for null; on null degrade/skip/fall-back to some (presumably slower or lesser)
66 * alternative as appropriate for your use-case.
67 *
68 * ### Corner case: the *first* this_thread_obj_or_null() occurring near thread death ###
69 * Suppose the first-ever this_thread_obj_or_null() call in a given thread occurs when that thread is already
70 * dying. Then:
71 * - If it occurs during `thread_local` deinit (so from some other `thread_local`'s dtor): the #Obj is
72 * constructed normally; and the ongoing deinit pass shall destroy it too, in due course (at least in the
73 * Itanium-ABI/glibc regime -- e.g., Linux -- where destructions registered mid-pass are processed by that
74 * same pass). So: the full normal lifecycle, compressed into the deinit phase; callers before its
75 * destruction get the object; after it -- null, as usual.
76 * - If it occurs even later -- e.g., in `thread_specific_ptr`-style cleanup of a non-`boost::thread`
77 * thread, which runs after deinit: the #Obj is constructed normally and is fully usable for the rest of
78 * the teardown; however its dtor, though registered, shall never run (the deinit pass is over).
79 * Consequence: the #Obj -- heap parts and all -- *leaks*. We consider this acceptable: it is rare, at
80 * worst once per dying thread, and the alternative would be having no object at all; but be aware.
81 *
82 * (The preceding is anchored in how mainstream implementations behave -- the standard is largely silent on
83 * construction-during-teardown. On our target platforms *as of this writing* it holds; a `static_assert()`
84 * will warn about looking into this in ports to other platforms.)
85 *
86 * @internal
87 *
88 * ### How it works / why it is formally OK ###
89 * Internally, next to the #Obj -- which presumably needs a destructor, or you would not bother with any of
90 * this -- lives a separate `thread_local bool`. The #Obj's destruction flips it. Reading the `bool` at any
91 * subsequent time is formally fine: it is trivially destructible and constant-initialized, so its lifetime
92 * never ends before the thread's very existence does. Two subtleties are essential:
93 * - The `bool` is a separate complete object -- *not* a member of some wrapper around #Obj: accessing a
94 * member of a destroyed object is formally undefined behavior, storage-persistence notwithstanding.
95 * - There is no destruction-order dependency of any kind: the `bool` has no destructor, so no order of
96 * `thread_local` deinit can invalidate it. Incidentally... that is why there's no infinite recursion,
97 * as we try to ourselves use `Thread_local_obj_deinit_safe<bool>`. (Joking. It's true though.)
98 *
99 * @endinternal
100 *
101 * @tparam Obj_t
102 * The per-thread object type. Must be default-constructible; and must *not* be trivially
103 * destructible: otherwise a plain `thread_local Obj_t` is already safe
104 * in the above sense, and `*this` class adds nothing (cf. this_thread_unique_token() internals which
105 * use that plain-and-already-safe technique).
106 * @tparam Tag_t
107 * Any type; used purely at compile time for disambiguation: distinct #Tag => distinct per-thread
108 * object, given equal #Obj. Suggested conventions: the class in which the use site lives; or an
109 * ad-hoc `struct X_tag;` declared just for this. Defaults to `void`.
110 */
111template<typename Obj_t, typename Tag_t>
113{
114public:
115 // Types.
116
117 /// Short-hand for template parameter type: the per-thread object type.
118 using Obj = Obj_t;
119 /// Short-hand for template parameter type: the disambiguating tag type.
120 using Tag = Tag_t;
121
122 static_assert(!std::is_trivially_destructible_v<Obj_t>,
123 "There is no point in using Thread_local_obj_deinit_safe with a trivially-destructible Obj: "
124 "a plain `thread_local Obj` is already accessible at all times from its thread, deinit "
125 "included (cf. this_thread_unique_token() internals which use that technique).");
126
127 // Constructors/destructor.
128
129 /// No instances: `*this` class is a static-access facility.
131
132 // Methods.
133
134 /**
135 * Returns pointer to the calling thread's #Obj, lazily default-constructing it if this is the first call
136 * in that thread; or null if it has already been destroyed (which occurs during the thread's `thread_local`
137 * deinit). Once null, always null (in that thread).
138 *
139 * @return See above.
140 */
142
143private:
144 // Types.
145
146 /// Holds the actual per-thread #Obj; its destruction records itself in the adjacent dead-flag.
148 {
149 // Constructors/destructor.
150
151 /**
152 * Constructs #m_obj via its default ctor.
153 *
154 * @param dead_flag
155 * Pointer to the flag to flip at destruction.
156 */
157 explicit Guarded_obj(bool* dead_flag);
158
159 /// Flips the dead-flag; then #m_obj dies (in that order: members are destroyed after the dtor body).
160 ~Guarded_obj();
161
162 // Data.
163
164 /// The payload.
166
167 /// See ctor.
168 bool* const m_dead_flag;
169 }; // struct Guarded_obj
170}; // class Thread_local_obj_deinit_safe
171
172// Template implementations.
173
174template<typename Obj_t, typename Tag_t>
176 m_dead_flag(dead_flag)
177{
178 // Yep.
179}
180
181template<typename Obj_t, typename Tag_t>
183{
184 *m_dead_flag = true;
185}
186
187template<typename Obj_t, typename Tag_t>
190{
191#ifndef FLOW_OS_LINUX
192static_assert(false,
193 "All or most of this should work in other OS, with which we have not tested, but upon "
194 "porting to such an OS be sure you've handled any corner cases mentioned throughout "
195 "the comments in this impl, starting with class doc header. At least one corner case "
196 "concerns behavior if this_thread_obj_or_null() is called for the first time near thread exit.");
197#endif
198
199 /* s_this_thread_obj_dead: constant-initialized + trivially destructible => its lifetime spans the entire
200 * thread lifetime: readable before s_this_thread_obj exists and after it is gone -- the whole point.
201 * (See class doc header for the formal-correctness discussion.) */
202 thread_local bool s_this_thread_obj_dead = false;
203 if (s_this_thread_obj_dead)
204 {
205 /* Do not even pass through s_this_thread_obj's definition below: formally, control passing through the
206 * definition of a destroyed block-scope object (static or thread storage duration) is undefined behavior
207 * (per standard). It might still be okay in practice but zero reason to risk it. ("It" would be
208 * something like doing `return s_this_thread_obj_dead ? nullptr : &s_this_thread_obj.m_obj` after
209 * the `thread_local` seen below (the 2nd one overall).) */
210 return nullptr;
211 }
212 // else:
213
214 thread_local Guarded_obj s_this_thread_obj{&s_this_thread_obj_dead};
215 return &s_this_thread_obj.m_obj;
216}
217
218} // namespace flow::util
A thread_local object of type Obj, in effect, but with death-aware access: this_thread_obj_or_null() ...
Tag_t Tag
Short-hand for template parameter type: the disambiguating tag type.
Thread_local_obj_deinit_safe()=delete
No instances: *this class is a static-access facility.
static Obj * this_thread_obj_or_null()
Returns pointer to the calling thread's Obj, lazily default-constructing it if this is the first call...
Obj_t Obj
Short-hand for template parameter type: the per-thread object type.
Flow module containing miscellaneous general-use facilities that don't fit into any other Flow module...
Holds the actual per-thread Obj; its destruction records itself in the adjacent dead-flag.
Guarded_obj(bool *dead_flag)
Constructs m_obj via its default ctor.
~Guarded_obj()
Flips the dead-flag; then m_obj dies (in that order: members are destroyed after the dtor body).