Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
thread_cache.hpp
Go to the documentation of this file.
1/* Flow-IPC: SHM-jemalloc
2 * Copyright (c) 2023 Akamai Technologies, Inc.; and other contributors.
3 * Each commit is copyright by its respective author or author's employer.
4 *
5 * Licensed under the MIT License:
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 * THE SOFTWARE. */
24
25/// @file
26#pragma once
27
31#include "ipc/common.hpp"
32#include <flow/util/action_registry.hpp>
33#include <flow/util/thread_lcl.hpp>
34#include <flow/log/log.hpp>
35#include <boost/unordered_map.hpp>
36#include <boost/unordered_set.hpp>
37#include <boost/unordered/unordered_flat_map.hpp>
38#include <string>
39#include <atomic>
40
42{
43
44/**
45 * jemalloc central thread cache manager, maintaining an explicitly identified tcache per allocating thread per
46 * arena. While a public-API part of SHM-jemalloc, the following is true:
47 * - When using SHM-jemalloc, Thread_cache will be used automatically without need for the user to mention it
48 * or even enable it. Perf should be maximized in this context automatically.
49 * - You actually can use it directly for your own needs, and in fact it is not limited to supporting allocation
50 * in shared memory (SHM); it will work equally well with the regular heap as is the jemalloc default.
51 * - However, in that case, it is probably best to either:
52 * - never use #S_NO_ARENA_ID as argument to the key method id(); or
53 * - use it explicitly but not use destroy_arena_safely(). The reason will likely be apparent on perusing
54 * doc headers for id() and destroy_arena_safely() and/or the below Background section.
55 *
56 * ### Terminology ###
57 * This might be a bit confusing; our apologies for that in advance.
58 * - The term "thread cache object" or "thread cache" refers to a Thread_cache object. However, such an object
59 * shall maintain one actual jemalloc *tcache* per arena (as specified to id() method) per thread.
60 * - The term "tcache" shall refer to a single jemalloc tcache (a/k/a tcache ID a/k/a #tcache_id_t value).
61 *
62 * Thus one *Thread_cache object* or *thread cache* shall hold handles to 0 or more jemalloc *tcaches*
63 * (to be of any use, for a given thread, 1 or more). Naturally there shall be one cache object per thread
64 * (or none, if you never call id() in a given thread).
65 *
66 * ### Background: arena versus tcache relationship in jemalloc ###
67 * The following information is surprisingly obscure, in that the main jemalloc docs do not state it clearly,
68 * and it does not appear to be common knowledge. However it can be found in links like
69 * https://github.com/jemalloc/jemalloc/issues/460. To wit:
70 *
71 * Consider one given thread T. If you use at least two arenas A and B, and *one* jemalloc tcache (ID) X,
72 * for your jemalloc `mallocx()` and `dallocx()` calls, then beware that it is entirely possible that
73 * the call `p = mallocx(..., MALLOCX_ARENA(B) | MALLOCX_TCACHE(X))` will yield vaddr `p` belonging to
74 * an extent (vaddr range) in arena A!
75 *
76 * This is (1) surprising to at least some; (2) possibly okay if the arenas are in heap, and/or it does not
77 * matter where something is allocated other than maybe perf; (3) definitely not okay in at least some contexts
78 * such as SHM-jemalloc's needs (and arguably SHM in general).
79 *
80 * That is why, unless one only uses #S_NO_ARENA_ID, Thread_cache maintains a stringent one-to-one (in a given thread)
81 * relationship between #arena_id_t values passed to id() + id_or_none() and the resulting returned
82 * #tcache_id_t values. Thus a given `id()` shall be used with only 1 jemalloc-arena, and there is no danger
83 * of seeing the aforementioned arguably-surprising behavior.
84 *
85 * ### Background: jemalloc tcache mechanism ###
86 * For brevity let's pretend there are just the jemalloc alloc-function `p = a(tc)`,
87 * which returns a `void*` to a buffer of some constant size, w/r/t tcache with ID `tc`; and `d(p, tc)` which
88 * deallocates; all from some agreed-upon single arena. Also let `a()` and `d(p)` be the no-tcache versions.
89 * (In reality these are jemalloc's `mallocx()` and `dallocx()` with the APIs shown in the next section.) Let
90 * us assume also 2 threads T1 and T2 with respectively already-created tcache-IDs `tc1` and `tc2`.
91 *
92 * First consider the no-tcache scenario. `p = a()` might be called in T1 and, perhaps, `p2 = a()` or `d(p2)`
93 * simultaneously in T2. jemalloc's internal what's-available-in-arena data structure -- call this `ARENA` --
94 * basically knows the various values `a()` is allowed to return -- some are still available, others are taken,
95 * and the structure is perhaps a bitmap, but no matter: Basically, every possible `p` value it could ever
96 * return is initially *owned* by `ARENA`. Essentially this `ARENA` then has 2 ops: `p = ARENA.a()` and
97 * `ARENA.d(p)`; these un-own that `p` and give them to the invoker-entity and, respectively, the opposite
98 * (re-own it). So then `a()` is simply `ARENA.a()` and `d(p2)` is `ARENA.d(p2)`. However, such things can
99 * happen concurrently from T1 and T2, so `ARENA.*()` both have to bracket themselves with a lock-section on
100 * mutex `ARENA.mtx`. Simple -- but there can be `ARENA.mtx` contention.
101 *
102 * Now with tcache: At its core, which is what we describe here, a given tcache `tc` is more-or-less a
103 * `Set<void*>`: you can `tc.add(p)` and `tc.take(p)`. In reality in jemalloc it's a stack, but we don't care here;
104 * let's just say `tc.take()` grabs a random `void*` and returns it, unless empty.
105 *
106 * Initially `tc1` and `tc2` are empty. Say we now stop dealing directly with `a()` and `d()` and instead
107 * now will do only `a(tc...)` and `d(..., tc...)`, meaning we will now go through tcaches. To wit: say we
108 * start over and do in thread T1: `p = a(tc1)`. This goes through `tc1`; but we cannot do `p = tc1.take()` yet:
109 * `tc1` is empty. So jemalloc *seeds* `tc1`: it performs many -- let's say 100 -- `ARENA.a()`s and places
110 * the resulting pointers into `tc1`. (Since these all happen at one time, `ARENA.mtx` lock can be across all
111 * 100 of these ops.) We aren't done yet though: `ARENA` no longer owns these pointers, but the user (=we) does
112 * not own them either yet. However, now `tc1` is non-empty, so `a(tc1)` can proceed and `tc1.take()` 1 of the
113 * 100 ptrs -- now `tc1` un-owns it while owning the other 99 still -- but we (the user) get it and own it:
114 * `a(tc1)` returns that ptr. If we were to do 50 more `a(tc1)`s right now, each one sees a non-empty
115 * (and used by T1 alone!) `Set tc1`, performs a lock-free `tc1.take()`; done: no mutex touched. (Had this been
116 * done without tcache involvement, there would've been 51 locks/unlocks total so far -- instead of 1. Thus
117 * the perf win.) Further, `d(p, tc1)` simply does `tc1.add(p)` -- in our minimalistic model this cannot fail.
118 * If we do that now, we then now own 50 ptrs, `tc1` now owns 100-1-50+1=50; and `ARENA` owns the rest.
119 * The key point: the 4 *own-sets* (us, `tc1`, `tc2`, `ARENA`) are always *disjoint*. If generalized, the
120 * disjoint *own-sets* are (us, disjoint `tc*` tcaches, `ARENA`).
121 *
122 * Naturally, similar stuff can be applied to `tc2` -- so far we've kept it empty, but we could do similar
123 * ops on it.
124 *
125 * With no loss of generality, we can kick around a given `p` between us, `tc1`, `tc2`. We don't interact
126 * directly with `ARENA` anymore -- it just feeds the `tc`s as-needed, when we want to do `tc.take()` on
127 * an empty `tc` (as part of `a(tc)`) -- but `p` "travels" (ownership-wise) between us, `tc1`, `tc2`, `ARENA`.
128 * (There is also a `flush(tc)` op which is the opposite of the *seed* op: lock `ARENA.mtx`, do N `p = tc.take()`s +
129 * N `ARENA.d(p)`s, unlock.)
130 *
131 * @note Reminder: The only restriction: A `tc` must not be used by 2+ threads concurrently (a jemalloc requirement;
132 * violating it => likely crash). Thread_cache satisfies this by giving each thread its own tcaches, used
133 * only in that thread. Again, though, that aside -- a pointer can be allocated from any `tc` and
134 * deallocated into any `tc`.
135 *
136 * Crucially, the only ops that actually touch `ARENA` -- therefore lock `ARENA.mtx` -- are *seeding*
137 * and `flush()`ing. Seeding occurs automatically within jemalloc, at least roughly as described above.
138 * Flushing is intentional -- we provide flush_tcache() API -- but typically is not necessary to do explicitly.
139 * jemalloc also flushes part of a tcache's per-size-class store automatically when a deallocation finds it full.
140 * (jemalloc's periodic tcache garbage collection does *not* apply to these explicit tcaches; only to each thread's
141 * automatic tcache. So an explicit tcache keeps its cached pointers until such an overflow, an explicit flush,
142 * or its destruction.)
143 *
144 * So: that is how it works. `a(tc)` and `d(p, tc)` -- really
145 * `mallocx(sz, MALLOCX_ARENA(arena_id) | MALLOCX_TCACHE(tc))` and
146 * `dallocx(p, MALLOCX_TCACHE(tc))` respectively -- use the `tc`-identified tcache as a
147 * thread-local intermediary pointer-store, allocating from/deallocating into which is faster since
148 * - deallocating rarely requires mutex-locking (only on the aforementioned overflow-flush);
149 * - allocating rarely requires mutex-locking, batching many pointer-grabs-from-`arena_id` under a single
150 * mutex-lock (as needed).
151 *
152 * @note Already you might note the reality is more complex than our described model; `sz` alone -- as opposed
153 * to our model's single constant size -- complicates things. However, this is not at its core that important.
154 * (Spoiler alert: It's a per-size-class setup.) Other details can also be omitted here (but may well be
155 * important when tuning for perf).
156 *
157 * Incidentally it may now be clear(er) why intermixing 2 arenas and 1 tcache can (will) lead to trouble, at least
158 * if one, black-boxily, expects `mallocx(..., MALLOCX_ARENA(arena_id))` to obtain a ptr from within
159 * `arena_id`-arena and not another one. For example: say we did some allocs from arena A1 and A2, thus seeding
160 * the cache with 100 ptrs from A1 and 100 ptrs from A2. Now say we do an alloc, through the given tcache,
161 * from A2. The tcache just stores pointers; does not know or care from which arena each one originated. So
162 * jemalloc looks in the tcache... it is not empty... it does `.take()`... and that returns a random ptr from the
163 * 100+100=200 in there... but that random one may have come from A1: we said A2 though. Thus the issue.
164 * Hence in Thread_cache (reiterating) it is (outside of #S_NO_ARENA_ID) strictly 1-to-1 between arena IDs and
165 * tcache IDs (per thread).
166 *
167 * ### How to use Thread_cache directly ###
168 * Suppose you are in thread T and would like to allocate (jemalloc `mallocx()`) and deallocate (`dallocx()`)
169 * while using an explicit jemalloc tcache. We support only the use case wherein one passes to `mallocx()`
170 * a `flags` argument that includes `MALLOCX_ARENA(arena_id)` for some user-created `arena_id`. Per jemalloc API,
171 * to use thread caching:
172 * - With `mallocx()`: In addition to `MALLOCX_ARENA(arena_id)`, add-in the mask `MALLOCX_TCACHE(tcache_id)`, where
173 * you shall obtain `tcache_id_t tcache_id` from a `*this`.
174 * - With `dallocx()` for the address returned by `mallocx()`: pass `MALLOCX_TCACHE(tcache_id)`. (`dallocx()`
175 * auto-determines the arena from the address.) That said:
176 * - If you're in the same thread as where you did the `mallocx()`: Simple as it gets; use the
177 * same `tcache_id`.
178 * - If you're in a different thread:
179 * - Do *not* try to reuse the memorized `tcache_id` from the original thread.
180 * - Obtain the `tcache_id_t` for this current/different thread and use that instead.
181 * - Alternatively: forego tcache use: `MALLOCX_TCACHE_NONE`.
182 *
183 * As should be clear from the preceding section, there is nothing wrong with `TCACHE_NONE` (forgoing tcache)
184 * when deallocating, even if it came from a tcache when allocating. It would just mean doing the full
185 * arena-mutex-lock/return-pointer/arena-mutex-unlock op. Naturally, all else being equal, that is slower;
186 * so why do it?
187 *
188 * Answer: Suppose there is no tcache in the current thread when deallocating. `this_thread_cache()->id(arena_id)`
189 * creates it: no problem. However, it is this creation that, conceivably, could itself be dangerous. That we
190 * know of, there is at least one danger-zone: around thread exit; namely `*(this_thread_cache())` might have
191 * been cleaned-up already. It should be safe in some cases; the `flow::util::Thread_local_state_registry`
192 * machinery we use to implement cleanup-at-thread-exit will do stubborn-cleanups, so if you're using
193 * the same facility for *your* per-thread cleanup (wherein you attempt deallocation)
194 * (`Thread_local_ptr` or `boost::thread_specific_ptr` = also fine) it should interoperate properly, briefly
195 * re-creating Thread_cache + tcache and quickly destroying them again. Otherwise it depends on the per-thread
196 * deinit order; get it wrong, and it'll probably crash. Even the "it should be safe" situation feels
197 * dicey/entropy-laden, so informally we recommend against that too. So: at least during thread-deinit, it is
198 * prudent to avoid deallocating via this_thread_cache() and id(); use this_thread_cache_or_null() and id_or_none()
199 * instead (see just below).
200 *
201 * So that leaves the question: what #tcache_id_t value to use? Answer:
202 * -# Call `static` Thread_cache::this_thread_cache() which returns a pointer `x` to a thread-local Thread_cache*
203 * (cache object). It shall be created the first time it is called (in a given thread T).
204 * -# Call `x->id(arena_id)`, where `arena_id` is the arena ID you intend to pass to `mallocx()`
205 * (and probably `dallocx()`). This shall return the `id` to use. It shall be generated the first time it
206 * is called (in a given thread T) for that particular `arena_id`.
207 * - Internally this means `mallctl("tcache.create")` is invoked that first time, and the result is stored
208 * internally in thread-local storage, in a map by arena ID.
209 * -# Pass the resulting `tcache_id_t id` to `mallocx()` and (ideally) `dallocx()` in thread T.
210 *
211 * In the case where you want to use the tcache, but only if it exists (we recommend this above during
212 * danger-zones: around thread exit): this_thread_cache_or_null() to `x`; if not null: `x->id_or_none()`.
213 * If that returns an ID (not #S_NO_TCACHE_ID): use that ID. In the other 2 cases: use `MALLOCX_TCACHE_NONE`.
214 *
215 * You may also obtain the thread unique-token via thread_token() -- in a sense a replacement for
216 * `flow::util::this_thread_unique_token()` -- as well as thread_nickname() (useful for logging and reporting).
217 * We use `Thread_token` instead of `Thread_id` (<=> `this_thread::get_id()`) because of its uniqueness over time
218 * which `Thread_id` lacks (OS thread IDs are recycled after thread exit). This can be useful algorithmically,
219 * whereas outside logging/monitoring `Thread_id` can lead to subtle trouble.
220 *
221 * ### Important: Destroying arenas safely ###
222 * The following is non-obvious to new jemalloc users (though the main doc page does say it explicitly).
223 * If you have ever used tcache X with arena A, and you want to destroy arena A (presumably to return RAM/etc.),
224 * then you *must* first flush (and if your tcaches and arena are 1-to-1, as Thread_cache assumes, probably
225 * destroy -- which also auto-flushes) tcache X. If you do not:
226 * - Trying to flush or destroy X *after* arena A is destroyed will probably crash inside jemalloc.
227 * - Trying to use X for further allocations/deallocations (especially deallocations, it appears empirically)
228 * after A is destroyed -- even with some other arena B -- will probably crash inside jemalloc.
229 *
230 * This is a surprisingly stringent requirement. It is also, normally, potentially difficult to accommodate
231 * because:
232 * - The call to destroy an arena A (`mallctl()`, etc.) will of course have to happen in some specific thread Y...
233 * - ...but there may be tcaches associated with A in threads other than Y -- possibly many threads -- depending
234 * on where you were allocating stuff until then.
235 * - Therefore you'll need to do tcache-destroying for tcache ID X in 1+ threads... and only then is it safe
236 * to destroy the arena A.
237 * - (If you just let everything live until after `main()`, then you're fine.)
238 *
239 * However we have provided a facility to make this straightforward. When you need to destroy arena A, and
240 * you have used Thread_cache to allocate/deallocate objects, across various threads, in A, simply call
241 * `static` Thread_cache::destroy_arena_safely(), passing to it the #arena_id_t A. We shall take care of the
242 * needed tcache-destroying as soon as possible (but without starting any background threads, much like jemalloc
243 * does things -- opportunistic piggy-backing); and upon destroying the last relevant tcache, Thread_cache
244 * will internally actually destroy arena A.
245 *
246 * ### Flushing cache manually ###
247 * You may call `x->flush_tcache(id)`, from the thread to which `*x` belongs (where you called
248 * Thread_cache::this_thread_cache()), to synchronously flush the tcache `id`.
249 */
252 private boost::noncopyable
253{
254public:
255 // Constants.
256
257 /**
258 * Value to always pass to id() or id_or_none(), if you do not wish for Thread_cache to worry about 1-to-1
259 * mapping of tcaches and arenas (in a given thread). That is e.g. use it if you're keeping 1 tcache per thread,
260 * period, and either
261 * - are willing to deal with subtleties arising from how a tcache works when used with 2+ arenas, or
262 * - you only use one arena, period.
263 *
264 * @see Thread_cache doc header "Background: arena versus tcache relationship in jemalloc."
265 */
266 static constexpr arena_id_t S_NO_ARENA_ID = 0;
267
268 /// Special value returned by id_or_none() indicating no tcache yet exists for the given #arena_id_t.
269 static constexpr tcache_id_t S_NO_TCACHE_ID = tcache_id_t(-1);
270
271 // Constructors/destructor.
272
273 /**
274 * Constructs cache for this thread as invoked internally by this_thread_cache().
275 *
276 * @warning Do not directly construct a `*this`. It is public only as a requirement for
277 * `flow::util::Thread_local_state_registry` which we use internally. Behavior is undefined
278 * if you break this prohibition. Always use `static this_thread_cache()` instead (it performs
279 * lazy construction per thread).
280 *
281 * @param logger_ptr
282 * Logger to use for logging subsequently.
283 *
284 * @internal
285 *
286 * `logger_ptr` is taken from Static_state::m_caches.
287 *
288 * @todo The doc-header warning for the Thread_cache::Thread_cache() ctor could be removed, if
289 * (ideally) `flow::util::Thread_local_state_registry` could compile-time-check for existence of
290 * a public such ctor and avoid compiling (optional) code that would use it -- and in
291 * static-init of Static_state::m_caches we supplied a `new Thread_cache` optional-arg in that `private`
292 * context; or (less ideally) if we made a facade sub-class of Thread_cache that would expose this ctor but only
293 * to `Thread_local_state_registry`. For now the ctor is public but with that `@warning` and this to-do.
294 */
295 explicit Thread_cache(flow::log::Logger* logger_ptr);
296
297 /**
298 * Destructor that runs in each thread (potentially even after `main()` -- the thread had not been joined by then);
299 * it does its best to flush and destroy all tcaches created via id() in that thread.
300 */
302
303 // Methods.
304
305 /**
306 * Returns pointer to the one cache-object in the calling thread, first creating it if it is the first
307 * such call. This does not necessarily create (nor return) a tcache -- only a Thread_cache.
308 * However the next step is usually to in fact return/create-on-demand an actual tcache: call
309 * id() (or id_or_none()) on the returned Thread_cache.
310 *
311 * @return See above.
312 */
314
315 /**
316 * Returns pointer to the one cache-object in the calling thread, or null if none exists (this_thread_cache()
317 * has not yet been invoked).
318 *
319 * @see this_thread_cache() for the lazy-init variation. See class doc header for some suggestion(s) as
320 * to how this_thread_cache_or_null() and id_or_none() are useful.
321 *
322 * @return See above.
323 */
325
326 /**
327 * Returns tcache ID of the tcache in this thread with a 1-1 relationship to the given arena,
328 * creating the tcache on the first such call for a given distinct `arena_id` in this thread.
329 *
330 * @see class doc header for explanation as to how to use that with jemalloc alloc and dealloc APIs.
331 *
332 * If you prefer to maintain 1 tcache per thread, so that mapping (if any) between arena and tcache (in
333 * this thread) is left to you, use `arena_id` set to #S_NO_ARENA_ID.
334 *
335 * @param arena_id
336 * See above. Either a value as from `mallctl("arena.create")` or #S_NO_ARENA_ID.
337 * @return ID of tcache usable in this thread (and this thread only).
338 */
339 tcache_id_t id(arena_id_t arena_id);
340
341 /**
342 * Returns tcache ID of the tcache in this thread with a 1-1 relationship to the given arena,
343 * or special value #S_NO_TCACHE_ID, if no such tcache has yet been created via id().
344 *
345 * @see id() for the lazy-init variation. See class doc header for some suggestion(s) as
346 * to how this_thread_cache_or_null() and id_or_none() are useful.
347 *
348 * @param arena_id
349 * See above. Either a value as from `mallctl("arena.create")` or #S_NO_ARENA_ID.
350 * @return ID of tcache usable in this thread (and this thread only); or #S_NO_TCACHE_ID.
351 */
352 tcache_id_t id_or_none(arena_id_t arena_id) const;
353
354 /**
355 * Unique-token of thread in which `*this` was originally created via this_thread_cache() call.
356 * Can be used as a replacement for `flow::util::this_thread_unique_token()`.
357 *
358 * @return See above.
359 */
360 flow::util::Thread_token thread_token() const;
361
362 /**
363 * Thread nickname, as from `flow::log::Logger::set_thread_info()`, corresponding to thread in which `*this`
364 * was originally created via this_thread_cache() call.
365 *
366 * @return See above. Recall that this can be the empty string, if the thread nickname was never set
367 * (which occurs automatically for flow.async-created threads; or if one explicitly calls
368 * `flow::log::Logger::this_thread_set_logged_nickname()`). Can be cross-referenced with
369 * thread nicknames logged by flow.log.
370 */
371 const std::string& thread_nickname() const;
372
373 /**
374 * Flushes the given tcache as specified by its ID. You may obtain said ID, among other ways, by
375 * invoking `id_or_none(arena_id)` for some `arena_id`.
376 *
377 * @param id
378 * The tcache to flush. Behavior is undefined if `id` is invalid.
379 *
380 * @todo jemalloc::Thread_cache internals are as of this writing intentionally intolerant of
381 * failed jemalloc calls, such as due to getting a bad ID, essentially because in practice it was not
382 * important to contend with such scenarios; but it'd be nice to be more resilient about it nevertheless.
383 * As of this writing jemalloc errors within Thread_cache cause a logged message, assertion-trip if enabled,
384 * and finally a hard `std::abort()` if the assertion-trip attempt did not do it.
385 */
386 void flush_tcache(tcache_id_t id);
387
388 /**
389 * Sets the `Logger` to use (or null to skip logging) subsequently for logging in `static` calls;
390 * non-`static` Thread_cache calls in existing objects in all threads; and same for future-spawned such
391 * objects in all threads.
392 *
393 * This is safe against concurrent logging in other threads.
394 *
395 * @see It participates in arena_lend::set_logger(), so you may not need to call it directly.
396 *
397 * @note Since Thread_cache may log after `main()`, even before `main()`,
398 * and in parts of `main()` when no `Logger` is likely available typically, this may be more subtle
399 * than dealing with setting the logger usually is. E.g., one might
400 * bracket much of `main()` with a `caches_set_logger(X)` and `caches_set_logger(nullptr)`, where
401 * `X` points to a `Logger` that is constructed near the top and destroyed near the bottom of `main()`.
402 *
403 * @param logger_ptr
404 * Logger to use for logging subsequently. As usual null is allowed and turns off logging.
405 */
406 static void caches_set_logger(flow::log::Logger* logger_ptr);
407
408 /**
409 * Destroys (as by `mallctl("arena.X.destroy")`) the given jemalloc arena, as soon as it is safe without
410 * causing stability problems w/r/t tcaches currently tracked by Thread_cache in *all* threads.
411 * If it is safe (e.g., there are no relevant thread caches; or there is 1, but it belongs to the calling thread
412 * and can therefore be flushed/destroyed immediately) then the arena is destroyed immediately (synchronously).
413 *
414 * Either way -- including the synchronous case -- immediately after destroying the arena, invokes
415 * `on_done_func(log_ctx, F)`.
416 *
417 * @warning There is a jemalloc-endemic (at least in 5.2.1 through 5.4.0, the latter being the newest version as
418 * of 9/2026) problem wherein invoking a jemalloc stats-dump (jemalloc API `malloc_stats_print()`)
419 * concurrently to arena-destruction (as sooner-or-later invoked by the present method) or
420 * arena-creation can trigger a jemalloc abort (`<jemalloc>: Failure in xmallctlbymib()` or similar).
421 * The present method guards against it via tight global-mutex at actual-arena-destruction time;
422 * but this is only effective if the other ops -- especially stat-dumping itself -- do the same.
423 * Therefore we recommend using Memory_manager::create_arena() and `Memory_manager::...stats_dump...()`.
424 * (Incidentally Memory_manager::destroy_arena() simply forwards to us, so for cosmetic reasons that one
425 * might be good to call.) Save yourself some pain.
426 *
427 * `log_ctx` to `on_done_func()` shall be as follows:
428 * - If invoked synchronously: it will equal the `log_ctx` from the caller to this method.
429 * Since it was given to us as safe to use synchronously, it should continue to be safe in synchronously-called
430 * handler.
431 * - If invoked asynchronously (other thread): it will be one consistent with caches_set_logger().
432 * By then the `log_ctx` given to this method may be invalid; so we give it our valid one.
433 *
434 * More importantly: `F()` shall destroy the arena in question, so you should synchronously invoke it. Do note that
435 * - `on_done_func()` can be called synchronously in the calling thread, or in any thread within which
436 * this_thread_cache() has been called;
437 * - but only once total;
438 * - possibly even while the program is exiting;
439 * - never in parallel with *another* `on_done_func()` from *another* destroy_arena_safely() call.
440 * - We shall arrange synchronization on your behalf among the `on_done_func()` calls.
441 *
442 * ### Failure ###
443 * If, whether synchronously or otherwise, the arena destruction fails -- in practice, we only know of this
444 * happening on bogus `arena_id` -- we will log and `std::abort()`.
445 *
446 * `on_done_func()` must not throw.
447 *
448 * ### Mechanism ###
449 * If the destruction must be deferred due to other threads' tcaches, it shall occur at the earliest possible
450 * future this_thread_cache() or this_thread_cache_or_null() call (or thread exit). Each such call shall
451 * (opportunistically) flush/destroy as many relevant tcaches as are accessible in that thread; and the last such
452 * needed procedure will then finalize by destroying the arena `arena_id`.
453 *
454 * Since the arena and all relevant tcaches apply only to `arena_id`, none of it should affect the rest of
455 * your work. In terms of perf, any slowdown shall be only due to the destroying/flushing itself -- which
456 * presumably will be quite rare -- while the fast-path (wherein no such work is required) is a single
457 * atomic-check per `this_thread_cache*()`.
458 *
459 * ### Suggestion for `on_done_func()` ###
460 * Destroying the jemalloc arena is not a drop-everything-suddenly type of operation. It will potentially write
461 * to currently mapped vaddr-segments a/k/a extents a/k/a pools; it will certainly try to operate on extents
462 * such as to close/unmap them; and in particular this will invoke various extent hooks, if you've registered such
463 * on the arena `arena_id`. Bottom line: until the arena is actually destroyed, jemalloc may officially
464 * require that certain resources that *you* potentially registered with it using the extent-hook system --
465 * SHM-pools, extent hooks themselves -- still be live. Therefore it would then make sense to capture
466 * such items in `on_done_func` functor object (perhaps lambda) and only let them expire once the functor
467 * is invoked.
468 *
469 * As for the `const Log_context_mt*` arg passed to `on_done_func()`: It is possible that by the time the
470 * arena can be safely destroyed, the `Logger` (if any) that your `on_done_func()` is capturing would use is no
471 * longer valid; this gives you the opportunity for `on_done_func()` to log-through a known-safe
472 * `Log_context_mt` (recommend `FLOW_LOG_SET_LOCKED_CONTEXT()`). (It's of course optional; just an informal
473 * point.) Having done that one would typically invoke `F()` which would destroy the arena (and perhaps lead
474 * to synchronous extent-hook-driven behavior inside `F()`).
475 *
476 * ### Design rationale: Why `Log_context_mt*`? Why not just `Logger*`? ###
477 * In practice destroy_arena_safely() can be called at touchy times: past `main()`, during thread exit -- that
478 * kind of thing -- and even supposing not, `on_done_func()` might be asynchronously called at some unpredictable
479 * time. We've found by then -- even right at this call, but equally or more so in async-`on_done_func()` --
480 * it is quite difficult to have the user ensure their `Logger` is still around. Even communicating this
481 * requirement is hard, but assuming the documentation aspect is no issue, it just becomes an onerous/impossible
482 * thing. However `Log_context_mt` does support user's concurrently changing the `Logger*`, and:
483 * - Synchronously logging in this method, we can then support that use-case. If it is not required, it is
484 * trivial for the caller to pass-in a not-really-concurrently-mutable `Log_context_mt` containing
485 * a synchronously-valid `Logger*`.
486 * - Asynchronously logging from another thread, we cannot count on the user-given `Log_context_mt` anymore;
487 * but we can offer our own.
488 *
489 * So it allows for the maximally required flexibility; and if no flexibility is required, then just make a
490 * nominal `Log_context_mt` with whatever logger you've got (null is fine too) and pass that in as `log_ctx`.
491 *
492 * @see class doc header section about this topic for key background.
493 *
494 * @tparam On_done_func
495 * Function object that matches the signature: `void (const Log_context_mt*, auto&& destroy_arena_func)`; and
496 * `destroy_arena_func` is to be invoked as: `destroy_arena_func()`.
497 * @param arena_id
498 * Arena to destroy. Do not pass #S_NO_ARENA_ID.
499 * @param log_ctx
500 * Potentially-concurrently-mutable `Log_context_mt` to use for synchronous logging.
501 * @param on_done_func
502 * See above. Note `on_done_func` is potentially consumed.
503 */
504 template<typename On_done_func>
505 static void destroy_arena_safely(arena_id_t arena_id, const flow::log::Log_context_mt* log_ctx,
506 On_done_func&& on_done_func);
507
508private:
509 // Types.
510
511 /**
512 * For each arena currently being destroyed via destroy_arena_safely(), the state tracking the progress
513 * of that operation.
514 */
516 {
517 // Data.
518
519 /**
520 * Which Thread_cache extant objects have yet to invoke
521 * if_requested_destroy_tcaches_and_possibly_finish_arena_kills() before the arena can be destroyed safely.
522 */
523 boost::unordered_set<Thread_cache*> m_caches_left_to_destroy;
524
525 /**
526 * Functor to invoke, and then immediately destroy, once #m_caches_left_to_destroy is `.empty()`,
527 * and therefore can actually destroy the arena.
528 */
530 }; // struct Arena_death_progress
531
532 /// Bundles `static` state used among the instances of this class; this allows us to ensure order of init/de-init.
534 {
535 // Constructors/destructor.
536
537 /// Constructor.
538 Static_state();
539
540 // Data.
541
542 /// Protects against concurrent execution of Arena_death_progress::m_on_done_func.
543 flow::util::Mutex_non_recursive m_arenas_death_row_map_on_done_func_mutex;
544
545 /**
546 * State shared -- and acted upon using the `Polled_shared_state` pattern -- between extant Thread_cache
547 * per-thread objects, consisting of which of them must destroy relevant tcaches in order to finish
548 * 1+ destroy_arena_safely() ops.
549 *
550 * Each key is an arena being destroyed; that key's value is the progress state of what is yet to be done
551 * before completing the destruction of that arena fully. The key item is
552 * Arena_death_progress::m_caches_left_to_destroy: set of Thread_cache objects that
553 * must, at the next opportunity (this_thread_cache(), this_thread_cache_or_null(), dtor calls in the respective
554 * threads) destroy the tcache (if any) corresponding to that arena ID; and if doing so
555 * makes the set empty finally destroy_arena() for good.
556 *
557 * That operation is if_requested_destroy_tcaches_and_possibly_finish_arena_kills() and is gated
558 * on `Thread_cache::m_destroy_tcaches_requested_poll_flag.poll_armed()` returning `true`; the fast-path is it
559 * returns `false`, and thus there is nothing for that method to do.
560 */
561 flow::util::Polled_shared_state<boost::unordered_map<arena_id_t, Arena_death_progress>> m_arenas_death_row_map;
562
563 /// The work-horse: registry of per-thread Thread_cache objects, each holding 0+ jemalloc tcaches.
564 flow::util::Thread_local_state_registry<Thread_cache> m_caches;
565 }; // struct Static_state
566
567 // Friends.
568 friend std::ostream& operator<<(std::ostream&, const Thread_cache&);
569
570 // Methods.
571
572 /**
573 * Perform deferred cross-thread tcache and arena-destruction as triggered by destroy_arena_safely() call(s)
574 * made earlier. This is piggy-backed onto this_thread_cache() and this_thread_cache_or_null(); and invoked
575 * from the dtor.
576 *
577 * @note The fast-path -- wherein no deferred tcache/arena destruction processing is in fact required -- is
578 * intentionally ultra-minimal computationally. See #m_destroy_tcaches_requested_poll_flag and such.
579 *
580 * @param assume_requested
581 * If and only if `true` act as-if the poll-flag is armed; do not check it or attempt to disarm it.
582 * This is to be used if and only if invoked from dtor. (Thread exit is infrequent, and the scan
583 * is cheap when nothing is pending; so it is simpler to just do it than reason about flag state.
584 * Worst-case, and this is fairly likely, it'll merely lead to a no-op.)
585 */
586 void if_requested_destroy_tcaches_and_possibly_finish_arena_kills(bool assume_requested = false);
587
588 /**
589 * Flushes/destroys the given tcache.
590 * @param id
591 * See id().
592 */
594
595 /**
596 * Destroys the given arena now. Protects against detail::jemalloc_arena_list_mutex() issue (see its doc header).
597 * @param arena_id
598 * See destroy_arena_safely().
599 * @param log_ctx
600 * See destroy_arena_safely().
601 */
602 static void destroy_arena(arena_id_t arena_id, const flow::log::Log_context_mt* log_ctx);
603
604 // Data.
605
606 /// See doc header and especially member doc headers for Static_state.
608
609 /// Poll-flag corresponding to Static_state::m_arenas_death_row_map `Polled_shared_state` pattern.
611
612 /// See thread_token().
613 flow::util::Thread_token m_thread_token;
614
615 /// See thread_nickname().
616 std::string m_thread_nickname;
617
618 /**
619 * See id(): for this thread, maps arena ID (that has been passed to
620 * id() at least once) to the jemalloc tcache dedicated to that arena ID that was created by `*this`
621 * as a result of that first `id(arena_id)` call.
622 */
623 boost::unordered_flat_map<arena_id_t, tcache_id_t> m_tcaches_by_arena;
624}; // class Thread_cache
625
626// Free functions: in *_fwd.hpp.
627
628// Template implementations.
629
630template<typename On_done_func>
632 On_done_func&& on_done_func_arg)
633{
634 using flow::util::this_thread_unique_token;
635
636 FLOW_LOG_SET_LOCKED_CONTEXT(log_ctx);
637
638
639 decltype(Arena_death_progress::m_on_done_func) on_done_func
640 = [on_done_func_arg = std::move(on_done_func_arg), arena_id]
641 (auto log_ctx) mutable
642 {
643 // As promised protect against concurrent on_done_func()s from different destroy_arena_safely()s.
644 flow::util::Lock_guard<decltype(s_state.m_arenas_death_row_map_on_done_func_mutex)>
646
647 on_done_func_arg(log_ctx, [&]()
648 {
649 destroy_arena(arena_id, log_ctx);
650 });
651 };
652 assert(!on_done_func.empty());
653 /* If by the end of the function on_done_func is still not .empty(), we'll execute it synchronously to destroy arena!
654 * Otherwise we've saved it, to invoke/delete once this can be done safely. */
655
656 s_state.m_caches.while_locked([&](const auto& state_per_thread)
657 {
658 if (state_per_thread.empty())
659 {
660 assert(!on_done_func.empty()); // Will run it immediately!
661 return;
662 }
663 // else
664
665 s_state.m_arenas_death_row_map.while_locked([&](auto* arenas_death_row_map)
666 {
667 Arena_death_progress& arena_death_progress = (*arenas_death_row_map)[arena_id];
668
669 arena_death_progress.m_on_done_func = std::move(on_done_func);
670 on_done_func.clear(); // Just in case the move() didn't do it.
671
672 for (const auto& [cache, nil] : state_per_thread)
673 {
674 arena_death_progress.m_caches_left_to_destroy.insert(cache);
675 /* It means: "Yo, `cache`, if you've got a tcache for `arena_id`, destroy it; but either way you-dun-good,
676 * so remove yourself from s_state.m_arenas_death_row_map[arena_id].
677 * And if that makes s_state.m_arenas_death_row_map[arena_id] empty (you're the last one to have dun-good), then
678 * finish what we started: execute, and forget, m_on_done_func() (this will destroy_arena(arena_id))." */
679 }
680 }); // s_state.m_arenas_death_row_map.while_locked()
681
682 for (const auto& cache_and_nil : state_per_thread)
683 {
684 /* Alias: FLOW_LOG_..._LOCKED() messages below and other code may not name structured bindings (C++17).
685 * @todo Move to C++20 => Use bindings after all for pithiness. (Some C++17-mode compilers already OK; not all.)
686 * Remove the @warning about this from doc header of FLOW_LOG_WARNING_LOCKED() (in Flow, not Flow-IPC). */
687 auto* const cache = cache_and_nil.first;
688
689 if (this_thread_unique_token() == cache->thread_token())
690 {
691 FLOW_LOG_INFO_LOCKED("Jem_tcache[" << *cache << "]: "
692 "There was a typically-cross-thread tcache-destruction request "
693 "(arena [" << arena_id << "] being destroyed); "
694 "but we are in the relevant thread *now*, so we shall simply tcache-destroy the "
695 "tcache for that arena (if it exists) and skip the "
696 "flag mechanism; possibly even finish destroying arena.");
697 s_state.m_arenas_death_row_map.while_locked([&](auto* arenas_death_row_map_ptr)
698 {
699 auto& arenas_death_row_map = *arenas_death_row_map_ptr;
700
701 const auto it_arena_and_progress = arenas_death_row_map.find(arena_id);
702 assert((it_arena_and_progress != arenas_death_row_map.end())
703 && "What could have possibly already handled this thread's entry that we just inserted? Bug?");
704
705 /* This next part is kinda similar to the slow-path of
706 * if_requested_destroy_tcaches_and_possibly_finish_arena_kills(), but code reuse is not really
707 * that obvious. Let's just do this.... (@todo Maybe break it out into a non-static method though?) */
708
709 auto& arena_death_progress = it_arena_and_progress->second;
710 auto& caches = arena_death_progress.m_caches_left_to_destroy;
711#ifndef NDEBUG
712 const bool ok = 1 ==
713#endif
714 caches.erase(cache);
715 assert(ok && "What could have possibly already handled this thread's entry that we just inserted? Bug?");
716
717 assert(on_done_func.empty() && "Sanity check of nearby algorithm.");
718 if (caches.empty())
719 {
720 on_done_func = std::move(arena_death_progress.m_on_done_func); // Get it back!
721 arenas_death_row_map.erase(it_arena_and_progress); // It is now empty through-and-through.
722 }
723 // else {}
724
725 const auto it_arena_and_tcache = cache->m_tcaches_by_arena.find(arena_id);
726 if (it_arena_and_tcache != cache->m_tcaches_by_arena.end())
727 {
728 const auto id = it_arena_and_tcache->second;
729 FLOW_LOG_INFO_LOCKED
730 ("Jem_tcache[" << *cache << "]: Indeed we do hold tcache [" << id << "] for that arena. "
731 "Proceeding with destruction to cross ourselves off the list. If that makes "
732 "the list empty (does it? = [" << (!on_done_func.empty()) << "]), "
733 "we shall imminently destroy arena.");
734 cache->destroy_tcache(id); // @todo Technically this should be using log_ctx to fully follow contract.
735 cache->m_tcaches_by_arena.erase(it_arena_and_tcache);
736 }
737 }); // s_state.m_arenas_death_row_map.while_locked()
738 } // if (this_thread_unique_token() == cache->thread_token())
739 else // if (this thread unique-token != cache->thread_token())
740 {
741 FLOW_LOG_INFO_LOCKED("Jem_tcache[" << *cache << "]: In outside thread (unique-token "
742 "[" << this_thread_unique_token() << "]) setting tcache-destruction-requested "
743 "flag on (arena [" << arena_id << "] being destroyed) for per-thread for thread with "
744 "unique-token [" << cache->thread_token() << "], " // *cache includes these but why not....
745 "nickname [" << cache->thread_nickname() << "]. "
746 "Next time that thread polls for that flag being true, it will tcache-destroy the "
747 "relevant tcache if any and possibly finish destroying arena for real.");
748 cache->m_destroy_tcaches_requested_poll_flag.arm_next_poll();
749 } // else if (this thread unique-token != cache->thread_token())
750 } // for (cache : state_per_thread)
751 }); // s_state.m_caches.while_locked()
752
753 if (!on_done_func.empty())
754 {
755 /* destroy_arena_safely(X) equals on_done_func() (which inside will also destroy_arena(X)) in our case.
756 * (To summarize the above as of this writing: no tcaching threads exist yet => no problem; or:
757 * one tcaching thread exists... and it is us, so we destroyed the tcache synchronously => no problem.)
758 *
759 * As promised: Have them -- since this is synchronous -- use the Log_context_mt they gave us
760 * to (synchronously) use for various logging seen above. */
761 on_done_func(log_ctx);
762 }
763 // else { It has been deferred, until it is safe. }
764} // Thread_cache::destroy_arena_safely()
765
766} // namespace ipc::shm::arena_lend::jemalloc
jemalloc central thread cache manager, maintaining an explicitly identified tcache per allocating thr...
flow::util::Thread_token thread_token() const
Unique-token of thread in which *this was originally created via this_thread_cache() call.
void if_requested_destroy_tcaches_and_possibly_finish_arena_kills(bool assume_requested=false)
Perform deferred cross-thread tcache and arena-destruction as triggered by destroy_arena_safely() cal...
static constexpr arena_id_t S_NO_ARENA_ID
Value to always pass to id() or id_or_none(), if you do not wish for Thread_cache to worry about 1-to...
void flush_tcache(tcache_id_t id)
Flushes the given tcache as specified by its ID.
friend std::ostream & operator<<(std::ostream &, const Thread_cache &)
Prints string representation of the given Thread_cache to the given ostream.
static Static_state s_state
See doc header and especially member doc headers for Static_state.
flow::util::Thread_token m_thread_token
See thread_token().
std::string m_thread_nickname
See thread_nickname().
boost::unordered_flat_map< arena_id_t, tcache_id_t > m_tcaches_by_arena
See id(): for this thread, maps arena ID (that has been passed to id() at least once) to the jemalloc...
static constexpr tcache_id_t S_NO_TCACHE_ID
Special value returned by id_or_none() indicating no tcache yet exists for the given arena_id_t.
static void destroy_arena_safely(arena_id_t arena_id, const flow::log::Log_context_mt *log_ctx, On_done_func &&on_done_func)
Destroys (as by mallctl("arena.X.destroy")) the given jemalloc arena, as soon as it is safe without c...
~Thread_cache()
Destructor that runs in each thread (potentially even after main() – the thread had not been joined b...
Thread_cache(flow::log::Logger *logger_ptr)
Constructs cache for this thread as invoked internally by this_thread_cache().
static Thread_cache * this_thread_cache_or_null()
Returns pointer to the one cache-object in the calling thread, or null if none exists (this_thread_ca...
static Thread_cache * this_thread_cache()
Returns pointer to the one cache-object in the calling thread, first creating it if it is the first s...
flow::util::Poll_flag m_destroy_tcaches_requested_poll_flag
Poll-flag corresponding to Static_state::m_arenas_death_row_map Polled_shared_state pattern.
tcache_id_t id(arena_id_t arena_id)
Returns tcache ID of the tcache in this thread with a 1-1 relationship to the given arena,...
const std::string & thread_nickname() const
Thread nickname, as from flow::log::Logger::set_thread_info(), corresponding to thread in which *this...
void destroy_tcache(tcache_id_t id)
Flushes/destroys the given tcache.
tcache_id_t id_or_none(arena_id_t arena_id) const
Returns tcache ID of the tcache in this thread with a 1-1 relationship to the given arena,...
static void destroy_arena(arena_id_t arena_id, const flow::log::Log_context_mt *log_ctx)
Destroys the given arena now.
static void caches_set_logger(flow::log::Logger *logger_ptr)
Sets the Logger to use (or null to skip logging) subsequently for logging in static calls; non-static...
ipc::shm sub-module with the SHM-jemalloc SHM-provider. See ipc::shm doc header for introduction.
unsigned int arena_id_t
Arena ID as dictated by jemalloc.
unsigned int tcache_id_t
Thread cache ID type as dictated by jemalloc.
flow::Function< Signature > Function
Short-hand for polymorphic functor holder which is very common. This is essentially std::function.
Definition: common.hpp:317
For each arena currently being destroyed via destroy_arena_safely(), the state tracking the progress ...
boost::unordered_set< Thread_cache * > m_caches_left_to_destroy
Which Thread_cache extant objects have yet to invoke if_requested_destroy_tcaches_and_possibly_finish...
Function< void(const flow::log::Log_context_mt *)> m_on_done_func
Functor to invoke, and then immediately destroy, once m_caches_left_to_destroy is ....
Bundles static state used among the instances of this class; this allows us to ensure order of init/d...
flow::util::Polled_shared_state< boost::unordered_map< arena_id_t, Arena_death_progress > > m_arenas_death_row_map
State shared – and acted upon using the Polled_shared_state pattern – between extant Thread_cache per...
flow::util::Thread_local_state_registry< Thread_cache > m_caches
The work-horse: registry of per-thread Thread_cache objects, each holding 0+ jemalloc tcaches.
flow::util::Mutex_non_recursive m_arenas_death_row_map_on_done_func_mutex
Protects against concurrent execution of Arena_death_progress::m_on_done_func.