VeloGraphX
High-performance dynamic graph analytics in C++20
Loading...
Searching...
No Matches
work_stealing_pool.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <atomic>
4#include <chrono>
5#include <condition_variable>
6#include <cstddef>
7#include <deque>
8#include <exception>
9#include <functional>
10#include <memory>
11#include <mutex>
12#include <thread>
13#include <utility>
14#include <vector>
15
16namespace velographx {
17
19 std::size_t submitted{0};
20 std::size_t executed{0};
21 std::size_t steal_attempts{0};
22 std::size_t successful_steals{0};
23 std::size_t local_steals{0};
24 std::size_t remote_steals{0};
25};
26
28 public:
29 using Task = std::function<void()>;
30
31 explicit WorkStealingPool(std::size_t threads = std::thread::hardware_concurrency(),
32 std::vector<std::size_t> queue_groups = {}) {
33 if (threads == 0) threads = 1;
34 if (queue_groups.size() != threads) queue_groups.assign(threads, 0);
35 queue_groups_ = std::move(queue_groups);
36 queues_.reserve(threads);
37 for (std::size_t i = 0; i < threads; ++i) queues_.push_back(std::make_unique<Queue>());
38 workers_.reserve(threads);
39 for (std::size_t i = 0; i < threads; ++i) workers_.emplace_back([this, i] { worker(i); });
40 }
41
44
46 // Destructors must not propagate exceptions from user tasks. Public
47 // wait_idle()/parallel_for() still surface the first task failure.
48 wait_idle_no_throw();
49 stop_.store(true, std::memory_order_release);
50 cv_.notify_all();
51 for (auto& worker : workers_) if (worker.joinable()) worker.join();
52 }
53
54 std::size_t size() const noexcept { return workers_.size(); }
55 std::size_t queue_group(std::size_t index) const noexcept {
56 return index < queue_groups_.size() ? queue_groups_[index] : 0;
57 }
58
59 void submit(Task task, std::size_t locality_hint = 0) {
60 const auto queue = locality_hint % queues_.size();
61 outstanding_.fetch_add(1, std::memory_order_acq_rel);
62 submitted_.fetch_add(1, std::memory_order_relaxed);
63 {
64 std::lock_guard<std::mutex> lock(queues_[queue]->mutex);
65 queues_[queue]->tasks.push_back(std::move(task));
66 }
67 cv_.notify_all();
68 }
69
70 template <class Fn>
71 void parallel_for(std::size_t begin, std::size_t end, Fn&& fn,
72 std::size_t grain = 0) {
73 if (end <= begin) return;
74 if (grain == 0) grain = adaptive_grain(end - begin, size());
75 std::size_t hint = 0;
76 for (std::size_t first = begin; first < end; first += grain, ++hint) {
77 const auto last = (first + grain < end) ? first + grain : end;
78 submit([first, last, &fn] {
79 for (std::size_t i = first; i < last; ++i) fn(i);
80 }, hint);
81 }
82 wait_idle();
83 }
84
85 // Drain all submitted work. If one or more tasks fail, the pool records the
86 // first exception, completes accounting for every task, and rethrows only
87 // after the pool becomes idle. This keeps the pool reusable after failure.
88 void wait_idle() {
89 drain_until_idle();
90 rethrow_pending_exception();
91 }
92
93 WorkStealingStats stats() const noexcept {
94 return {submitted_.load(std::memory_order_relaxed), executed_.load(std::memory_order_relaxed),
95 steal_attempts_.load(std::memory_order_relaxed), successful_steals_.load(std::memory_order_relaxed),
96 local_steals_.load(std::memory_order_relaxed), remote_steals_.load(std::memory_order_relaxed)};
97 }
98
99 static std::size_t adaptive_grain(std::size_t work_items, std::size_t threads) noexcept {
100 if (threads == 0) threads = 1;
101 const std::size_t target_chunks = threads * 8;
102 const std::size_t grain = (work_items + target_chunks - 1) / target_chunks;
103 return grain == 0 ? 1 : grain;
104 }
105
106 private:
107 void record_exception(std::exception_ptr error) noexcept {
108 try {
109 std::lock_guard<std::mutex> lock(exception_mutex_);
110 if (!first_exception_) first_exception_ = std::move(error);
111 } catch (...) {
112 // A task exception must never escape a worker thread merely because the
113 // exception-recording path itself encountered an exceptional runtime
114 // condition.
115 }
116 }
117
118 void complete_task(Task& task) noexcept {
119 try {
120 task();
121 } catch (...) {
122 record_exception(std::current_exception());
123 }
124 executed_.fetch_add(1, std::memory_order_relaxed);
125 if (outstanding_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
126 idle_cv_.notify_all();
127 }
128 }
129
130 void drain_until_idle() {
131 while (outstanding_.load(std::memory_order_acquire) != 0) {
132 Task task;
133 if (pop_any(task)) {
134 complete_task(task);
135 continue;
136 }
137 std::unique_lock<std::mutex> lock(wait_mutex_);
138 idle_cv_.wait_for(lock, std::chrono::milliseconds(1), [this] {
139 return outstanding_.load(std::memory_order_acquire) == 0;
140 });
141 }
142 }
143
144 void wait_idle_no_throw() noexcept {
145 try {
146 drain_until_idle();
147 } catch (...) {
148 // Destruction is a non-reporting boundary. Call wait_idle() explicitly
149 // when task failures need to be observed by the caller.
150 }
151 }
152
153 void rethrow_pending_exception() {
154 std::exception_ptr error;
155 {
156 std::lock_guard<std::mutex> lock(exception_mutex_);
157 error = std::exchange(first_exception_, {});
158 }
159 if (error) std::rethrow_exception(error);
160 }
161
162 struct Queue {
163 std::mutex mutex;
164 std::deque<Task> tasks;
165 };
166
167 bool pop_local(std::size_t index, Task& task) {
168 std::lock_guard<std::mutex> lock(queues_[index]->mutex);
169 if (queues_[index]->tasks.empty()) return false;
170 task = std::move(queues_[index]->tasks.back());
171 queues_[index]->tasks.pop_back();
172 return true;
173 }
174
175 bool pop_any(Task& task) {
176 for (auto& queue : queues_) {
177 std::lock_guard<std::mutex> lock(queue->mutex);
178 if (queue->tasks.empty()) continue;
179 task = std::move(queue->tasks.front());
180 queue->tasks.pop_front();
181 return true;
182 }
183 return false;
184 }
185
186 bool try_steal_from(std::size_t thief, std::size_t victim, Task& task, bool local) {
187 steal_attempts_.fetch_add(1, std::memory_order_relaxed);
188 std::lock_guard<std::mutex> lock(queues_[victim]->mutex);
189 if (queues_[victim]->tasks.empty()) return false;
190 task = std::move(queues_[victim]->tasks.front());
191 queues_[victim]->tasks.pop_front();
192 successful_steals_.fetch_add(1, std::memory_order_relaxed);
193 (local ? local_steals_ : remote_steals_).fetch_add(1, std::memory_order_relaxed);
194 return true;
195 }
196
197 bool steal(std::size_t thief, Task& task) {
198 const auto thief_group = queue_group(thief);
199 for (std::size_t offset = 1; offset < queues_.size(); ++offset) {
200 const std::size_t victim = (thief + offset) % queues_.size();
201 if (queue_group(victim) != thief_group) continue;
202 if (try_steal_from(thief, victim, task, true)) return true;
203 }
204 for (std::size_t offset = 1; offset < queues_.size(); ++offset) {
205 const std::size_t victim = (thief + offset) % queues_.size();
206 if (queue_group(victim) == thief_group) continue;
207 if (try_steal_from(thief, victim, task, false)) return true;
208 }
209 return false;
210 }
211
212 void worker(std::size_t index) {
213 while (!stop_.load(std::memory_order_acquire)) {
214 Task task;
215 if (pop_local(index, task) || steal(index, task)) {
216 complete_task(task);
217 continue;
218 }
219 std::unique_lock<std::mutex> lock(cv_mutex_);
220 cv_.wait_for(lock, std::chrono::milliseconds(1), [this] {
221 return stop_.load(std::memory_order_acquire) || outstanding_.load(std::memory_order_acquire) != 0;
222 });
223 }
224 }
225
226 std::vector<std::unique_ptr<Queue>> queues_;
227 std::vector<std::thread> workers_;
228 std::vector<std::size_t> queue_groups_;
229 std::atomic<bool> stop_{false};
230 std::atomic<std::size_t> outstanding_{0};
231 std::atomic<std::size_t> submitted_{0};
232 std::atomic<std::size_t> executed_{0};
233 std::atomic<std::size_t> steal_attempts_{0};
234 std::atomic<std::size_t> successful_steals_{0};
235 std::atomic<std::size_t> local_steals_{0};
236 std::atomic<std::size_t> remote_steals_{0};
237 std::mutex cv_mutex_;
238 std::condition_variable cv_;
239 std::mutex wait_mutex_;
240 std::condition_variable idle_cv_;
241 std::mutex exception_mutex_;
242 std::exception_ptr first_exception_;
243};
244
245} // namespace velographx
std::size_t size() const noexcept
void parallel_for(std::size_t begin, std::size_t end, Fn &&fn, std::size_t grain=0)
static std::size_t adaptive_grain(std::size_t work_items, std::size_t threads) noexcept
WorkStealingStats stats() const noexcept
WorkStealingPool(std::size_t threads=std::thread::hardware_concurrency(), std::vector< std::size_t > queue_groups={})
WorkStealingPool & operator=(const WorkStealingPool &)=delete
void submit(Task task, std::size_t locality_hint=0)
std::size_t queue_group(std::size_t index) const noexcept
WorkStealingPool(const WorkStealingPool &)=delete