29 using Task = std::function<void()>;
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); });
49 stop_.store(
true, std::memory_order_release);
51 for (
auto& worker : workers_)
if (worker.joinable()) worker.join();
54 std::size_t
size() const noexcept {
return workers_.size(); }
56 return index < queue_groups_.size() ? queue_groups_[index] : 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);
64 std::lock_guard<std::mutex> lock(queues_[queue]->mutex);
65 queues_[queue]->tasks.push_back(std::move(task));
72 std::size_t grain = 0) {
73 if (end <= begin)
return;
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);
90 rethrow_pending_exception();
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)};
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;
107 void record_exception(std::exception_ptr error)
noexcept {
109 std::lock_guard<std::mutex> lock(exception_mutex_);
110 if (!first_exception_) first_exception_ = std::move(error);
118 void complete_task(
Task& task)
noexcept {
122 record_exception(std::current_exception());
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();
130 void drain_until_idle() {
131 while (outstanding_.load(std::memory_order_acquire) != 0) {
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;
144 void wait_idle_no_throw() noexcept {
153 void rethrow_pending_exception() {
154 std::exception_ptr error;
156 std::lock_guard<std::mutex> lock(exception_mutex_);
157 error = std::exchange(first_exception_, {});
159 if (error) std::rethrow_exception(error);
164 std::deque<Task> tasks;
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();
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();
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);
197 bool steal(std::size_t thief,
Task& task) {
199 for (std::size_t offset = 1; offset < queues_.size(); ++offset) {
200 const std::size_t victim = (thief + offset) % queues_.size();
202 if (try_steal_from(thief, victim, task,
true))
return true;
204 for (std::size_t offset = 1; offset < queues_.size(); ++offset) {
205 const std::size_t victim = (thief + offset) % queues_.size();
207 if (try_steal_from(thief, victim, task,
false))
return true;
212 void worker(std::size_t index) {
213 while (!stop_.load(std::memory_order_acquire)) {
215 if (pop_local(index, task) || steal(index, task)) {
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;
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_;