Last active
April 27, 2026 22:24
-
-
Save TerensTare/38c462dae0920c3640b203b966286751 to your computer and use it in GitHub Desktop.
You only do things once, with C++20.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <atomic> | |
| #include <concepts> | |
| #define fwd(x) static_cast<decltype(x) &&>(x) | |
| template <auto = []{}> | |
| inline auto once(std::invocable<> auto &&fn) { | |
| static constinit bool init = false; | |
| if (!init) { | |
| fwd(fn)(); | |
| init = true; | |
| } | |
| } | |
| template <auto = []{}> | |
| [[nodiscard]] inline auto once() { | |
| static constinit bool init = false; | |
| bool old = init; | |
| return (init = true), !old; | |
| } | |
| template <auto = []{}> | |
| inline auto once_per_thread(std::invocable<> auto &&fn) { | |
| static thread_local constinit bool init = false; | |
| if (!init) { | |
| fwd(fn)(); | |
| init = true; | |
| } | |
| } | |
| template <auto = []{}> | |
| [[nodiscard]] inline auto once_per_thread() { | |
| static thread_local constinit bool init = false; | |
| bool old = init; | |
| return (init = true), !old; | |
| } | |
| template <auto = []{}> | |
| inline auto atomic_once(std::invocable<> auto &&fn) { | |
| static constinit std::atomic_flag init = false; | |
| if (!init.test_and_set(std::memory_order::acquire)) | |
| { | |
| fwd(fn)(); | |
| std::atomic_thread_fence(std::memory_order::release); | |
| } | |
| } | |
| template <auto = []{}> | |
| inline auto blocking_atomic_once(std::invocable<> auto &&fn) { | |
| // legend: -1 means not init yet, 0 means "is initializing now", 1 means initialized | |
| static constinit std::atomic_int status = -1; | |
| auto tmp = -1; | |
| if (status.compare_exchange_strong( | |
| tmp, 0, | |
| std::memory_order::acq_rel, | |
| std::memory_order::acquire)) | |
| { | |
| fwd(fn)(); | |
| status.store(1, std::memory_order::release); | |
| status.notify_all(); | |
| } | |
| else | |
| status.wait(0, std::memory_order::acquire); | |
| } | |
| #include <cstdio> | |
| #include <thread> | |
| #include <vector> | |
| // For once and once_per_thread, you can also use the alternative overload: | |
| // ```cpp | |
| // if (once()) | |
| // { | |
| // } | |
| // ``` | |
| int main() | |
| { | |
| int i = 0; | |
| for (; i < 10; ++i) | |
| { | |
| // `once` will run something just once, no matter how many times you go over it. | |
| once([&] { | |
| printf("%d\n", i); | |
| }); | |
| } | |
| std::vector<std::jthread> threads; | |
| int state = 0; | |
| for (; i < 20; ++i) { | |
| threads.emplace_back([&, i] { | |
| // `atomic_once` is like `once`, but will only run once in only one thread. | |
| // ^ ie. only one thread calls the function here and everybody else makes progress. | |
| // This means you don't need to protect your data access with a mutex/etc. as long as no other thread is reading/writing the same data by some other means (try replacing `blocking_atomic_once` below with `atomic_once`). | |
| // Note that using `once` here would cause a data race when setting the inner flag. | |
| // This is useful for doing lazy initialization from a single thread. | |
| atomic_once([&] { | |
| printf("i from thread=%d\n", i); | |
| }); | |
| // `blocking_atomic_once` offers more safety for certain initialization patterns, by making all threads wait here for `blocking_atomic_once` to finish before making any progress. | |
| // This is useful for cases where | |
| blocking_atomic_once([&] { | |
| for (int j{}; j < 100; ++j) { | |
| state = j * j; | |
| std::this_thread::yield(); | |
| } | |
| printf("blocking_atomic_once was done\n"); | |
| }); | |
| // If we let this run on all threads, we will have a race | |
| // So only the thread with i==15 can do this. | |
| // Notice how it doesn't cause a race with the `blocking_atomic_once` | |
| if (i == 15) { | |
| printf("Modifying `state` after init"); | |
| state = i * 100; | |
| } | |
| for (int j{}; j < 10; ++j) | |
| { | |
| // Finally, there is `once_per_thread`, which behaves like `once`, but keeps a separate flag per thread, so each thread runs the callback once. | |
| // This is useful to get `once` behavior, but running on each thread. | |
| // `once` here would cause a data race but also would be ran only once no matter how many threads pass here (assuming no data race happens somehow). | |
| once_per_thread([&] { | |
| auto const tid = std::this_thread::get_id(); | |
| auto const hash = std::hash<std::thread::id>{}(tid); | |
| printf("Thread id=%u j=%d\n", (uint32_t)hash, j); | |
| }); | |
| } | |
| }); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment