-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtreiber_hazard.cpp
More file actions
215 lines (176 loc) · 5.86 KB
/
Copy pathtreiber_hazard.cpp
File metadata and controls
215 lines (176 loc) · 5.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
// treiber_hazard.cpp
// Treiber stack with hazard pointers (safe memory reclamation).
//
// Build: g++ -std=c++17 -O2 -pthread treiber_hazard.cpp -o treiber_hazard
// Run: ./treiber_hazard
//
// Notes:
// - This solves *use-after-free* by deferring deletes until no thread hazards
// the node.
// - This does NOT implement tagged pointers; it focuses on reclamation safety.
// - Works best when you have a small fixed number of threads (typical for
// lock-free HP).
#include <algorithm>
#include <atomic>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <thread>
#include <vector>
struct Node {
int value;
Node *next;
};
// ---------------- Hazard Pointers ----------------
constexpr int MAX_THREADS = 8; // demo limit
std::atomic<void *> g_hazard[MAX_THREADS]; // one hazard slot per thread
// Retired nodes per thread (simple vector). In real systems: linked list +
// batching.
thread_local std::vector<Node *> t_retired;
thread_local int t_tid = -1;
// Set hazard pointer for this thread.
inline void set_hazard(int tid, void *p) {
g_hazard[tid].store(p, std::memory_order_release);
}
// Clear hazard pointer for this thread.
inline void clear_hazard(int tid) {
g_hazard[tid].store(nullptr, std::memory_order_release);
}
// Collect all hazard pointers currently published.
static std::vector<void *> collect_hazards() {
std::vector<void *> hz;
hz.reserve(MAX_THREADS);
for (int i = 0; i < MAX_THREADS; ++i) {
void *p = g_hazard[i].load(std::memory_order_acquire);
if (p)
hz.push_back(p);
}
return hz;
}
// Scan retired list and free nodes that are not protected.
static void scan_and_reclaim() {
auto hazards = collect_hazards();
std::sort(hazards.begin(), hazards.end());
auto &r = t_retired;
std::vector<Node *> keep;
keep.reserve(r.size());
for (Node *n : r) {
// if n is in hazard set => keep it (someone might still read it)
if (std::binary_search(hazards.begin(), hazards.end(),
static_cast<void *>(n))) {
keep.push_back(n);
} else {
delete n; // safe to reclaim now
}
}
r.swap(keep);
}
// Retire a node (defer delete) and occasionally scan.
static void retire_node(Node *n) {
t_retired.push_back(n);
// Tune threshold for batching. Lower threshold = more scanning overhead.
if (t_retired.size() >= 64) {
scan_and_reclaim();
}
}
// ---------------- Treiber Stack (HP) ----------------
class TreiberHP {
public:
TreiberHP() : head_(nullptr) {}
void push(Node *n) {
Node *old = head_.load(std::memory_order_relaxed);
do {
n->next = old;
} while (!head_.compare_exchange_weak(old, n, std::memory_order_release,
std::memory_order_relaxed));
}
Node *pop() {
assert(t_tid >= 0 && t_tid < MAX_THREADS &&
"Set t_tid for this thread before using pop()");
for (;;) {
Node *old = head_.load(std::memory_order_acquire);
if (!old) {
clear_hazard(t_tid);
return nullptr;
}
// Publish hazard: "I am about to dereference old"
set_hazard(t_tid, old);
// Re-check that head is still old after publishing hazard.
// This avoids the window: head changes -> old freed -> we hazard too
// late.
if (head_.load(std::memory_order_acquire) != old) {
continue; // retry: publish hazard for new head
}
Node *next = old->next;
if (head_.compare_exchange_weak(old, next, std::memory_order_acquire,
std::memory_order_relaxed)) {
// Successfully removed 'old' from stack.
clear_hazard(t_tid);
return old; // caller should retire_node(old) after done
}
// CAS failed; retry
}
}
private:
std::atomic<Node *> head_;
};
// ---------------- Demo / Stress ----------------
int main() {
// init hazard slots
for (int i = 0; i < MAX_THREADS; ++i)
g_hazard[i].store(nullptr, std::memory_order_relaxed);
TreiberHP st;
constexpr int PRODUCERS = 2;
constexpr int CONSUMERS = 2;
static_assert(PRODUCERS + CONSUMERS <= MAX_THREADS, "Increase MAX_THREADS");
constexpr int PER_PRODUCER = 200000;
std::atomic<int> produced{0};
std::atomic<int> consumed{0};
// Producers allocate nodes and push them.
std::vector<std::thread> threads;
for (int p = 0; p < PRODUCERS; ++p) {
threads.emplace_back([&, p] {
t_tid = p; // 0..PRODUCERS-1
for (int i = 0; i < PER_PRODUCER; ++i) {
Node *n = new Node{p * PER_PRODUCER + i, nullptr};
st.push(n);
produced.fetch_add(1, std::memory_order_relaxed);
}
// No reclamation work needed for producer-only thread
});
}
// Consumers pop nodes and retire them safely.
for (int c = 0; c < CONSUMERS; ++c) {
threads.emplace_back([&, c] {
t_tid = PRODUCERS + c; // next ids
// Keep consuming until we've consumed everything produced
// (This is a demo; real systems have shutdown signals.)
while (consumed.load(std::memory_order_acquire) <
PRODUCERS * PER_PRODUCER) {
Node *n = st.pop();
if (!n)
continue;
// Use node data (safe because hazard protected it during pop)
// After CAS success, no one else can reach 'n' from the stack.
(void)n->value;
retire_node(n);
consumed.fetch_add(1, std::memory_order_relaxed);
}
// Final cleanup: reclaim remaining retired nodes.
scan_and_reclaim();
// Clear hazard slot already cleared in pop(); just in case:
clear_hazard(t_tid);
});
}
for (auto &th : threads)
th.join();
std::cout << "Produced: " << produced.load() << "\n";
std::cout << "Consumed: " << consumed.load() << "\n";
// Reclaim any leftovers in this main thread (if it had any; usually none)
if (t_tid == -1)
t_tid = MAX_THREADS - 1;
scan_and_reclaim();
std::cout << "OK\n";
return 0;
}