Wildmeshing Toolkit
Loading...
Searching...
No Matches
enumerable_thread_specific.hpp
1#pragma once
2
3#include <algorithm>
4#include <atomic>
5#include <functional>
6#include <memory>
7#include <thread>
8#include <vector>
9
10namespace wmtk::threading {
11
12namespace detail {
13inline std::atomic<std::uint64_t>& ets_id_counter()
14{
15 static std::atomic<std::uint64_t> counter{1};
16 return counter;
17}
18} // namespace detail
19
20// ---------------------------------------------------------------------------
21// enumerable_thread_specific: replaces tbb::enumerable_thread_specific.
22// Only `.local()` (and construction with an optional initial value) is used.
23// Lock-free lookup: each thread owns a thread_local vector of slots.
24//
25// Slots outlive their object on other threads. The task_group workers persist for the life of
26// the process (task_group.hpp), so a thread that used an instance keeps that instance's slot
27// after the instance is destroyed; nothing else could remove it, since the slot vector belongs
28// to that thread alone. Each slot therefore carries a weak reference to its instance, and a
29// thread drops its dead slots -- destroying their values on the thread that created them --
30// the next time it creates a slot. That keeps the lookup loop free of any liveness test, bounds
31// what a dead instance leaves behind to one value per thread until that thread next meets a
32// new instance (every new mesh is one), and ids are never reused, so a dead slot can never be
33// mistaken for a live one in the meantime.
34// ---------------------------------------------------------------------------
35template <typename T>
37{
38 struct Slot
39 {
40 std::uint64_t id;
41 std::weak_ptr<const void> owner;
42 std::unique_ptr<T> value;
43 };
44
45 static std::vector<Slot>& thread_slots()
46 {
47 static thread_local std::vector<Slot> slots;
48 return slots;
49 }
50
51 std::uint64_t m_id = detail::ets_id_counter().fetch_add(1, std::memory_order_relaxed);
52 std::function<T()> m_factory;
54 std::shared_ptr<const void> m_alive = std::make_shared<char>(0);
55
56public:
58 : m_factory([]() { return T(); })
59 {}
60
61 template <
62 typename U,
63 typename = std::enable_if_t<!std::is_same_v<std::decay_t<U>, enumerable_thread_specific>>>
64 explicit enumerable_thread_specific(U&& init)
65 : m_factory([captured = T(std::forward<U>(init))]() { return captured; })
66 {}
67
68 enumerable_thread_specific(const enumerable_thread_specific&) = delete;
69 enumerable_thread_specific& operator=(const enumerable_thread_specific&) = delete;
70 enumerable_thread_specific(enumerable_thread_specific&&) = delete;
71 enumerable_thread_specific& operator=(enumerable_thread_specific&&) = delete;
72
73 ~enumerable_thread_specific()
74 {
75 // Clear this (usually the main) thread's slot for this instance. Other threads' slots
76 // expire with m_alive and are dropped by those threads (see the class comment).
77 auto& slots = thread_slots();
78 slots.erase(
79 std::remove_if(
80 slots.begin(),
81 slots.end(),
82 [this](const Slot& s) { return s.id == m_id; }),
83 slots.end());
84 }
85
86 T& local()
87 {
88 auto& slots = thread_slots();
89 for (auto& s : slots) {
90 if (s.id == m_id) {
91 return *s.value;
92 }
93 }
94 // No slot for this thread yet. Drop the slots of instances that no longer exist, then
95 // create one.
96 slots.erase(
97 std::remove_if(
98 slots.begin(),
99 slots.end(),
100 [](const Slot& s) { return s.owner.expired(); }),
101 slots.end());
102 slots.push_back(Slot{m_id, m_alive, std::make_unique<T>(m_factory())});
103 return *slots.back().value;
104 }
105};
106
107} // namespace wmtk::threading
Definition enumerable_thread_specific.hpp:37
std::shared_ptr< const void > m_alive
Expires when this instance is destroyed, which is how other threads learn of it.
Definition enumerable_thread_specific.hpp:54
Definition enumerable_thread_specific.hpp:39