Sourcemeta Core 0.0.0
Loading...
Searching...
No Matches
parallel_for_each.h
1#ifndef SOURCEMETA_CORE_PARALLEL_FOR_EACH_H_
2#define SOURCEMETA_CORE_PARALLEL_FOR_EACH_H_
3
4#include <algorithm> // std::max
5#include <climits> // UINT_MAX
6#include <concepts> // std::copyable, std::invocable
7#include <exception> // std::exception_ptr, std::current_exception, std::rethrow_exception
8#include <functional> // std::function
9#include <iterator> // std::input_iterator, std::iter_reference_t
10#include <mutex> // std::mutex, std::lock_guard
11#include <queue> // std::queue
12#include <stdexcept> // std::runtime_error
13#include <thread> // std::thread
14#include <utility> // std::forward
15#include <vector> // std::vector
16
17#if defined(_WIN32)
18#include <process.h> // _beginthreadex
19#define NOMINMAX
20#include <windows.h>
21#else
22#include <pthread.h>
23#endif
24
25namespace sourcemeta::core {
26
27#ifndef DOXYGEN
28#if defined(_WIN32)
29// See
30// https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/beginthread-beginthreadex?view=msvc-170
31inline unsigned __stdcall parallel_for_each_windows_thread_start(
32 void *argument) {
33 auto *function_ptr = static_cast<std::function<void()> *>(argument);
34 (*function_ptr)();
35 delete function_ptr;
36 return 0;
37}
38#endif
39
40// If thread creation fails after some workers have already started, those
41// workers keep referencing the stack locals of the spawning frame, so unwinding
42// past them must be avoided. Drain the remaining tasks so the running workers
43// stop pulling new work and exit, then join every already-created worker before
44// propagating the failure
45template <typename Iterator>
46inline auto parallel_for_each_drain_and_join(std::queue<Iterator> &tasks,
47 std::mutex &queue_mutex,
48 std::vector<std::thread> &workers)
49 -> void {
50 {
51 std::lock_guard<std::mutex> lock{queue_mutex};
52 std::queue<Iterator> empty;
53 tasks.swap(empty);
54 }
55
56 for (auto &worker_thread : workers) {
57 worker_thread.join();
58 }
59}
60#endif
61
93template <typename Iterator, typename Callback>
94 requires std::input_iterator<Iterator> && std::copyable<Iterator> &&
95 std::invocable<Callback, std::iter_reference_t<Iterator>,
96 std::size_t, std::size_t>
98 Iterator first, Iterator last, Callback &&callback,
99 const std::size_t parallelism = std::thread::hardware_concurrency(),
100 const std::size_t stack_size_bytes = 0) -> void {
101 const auto effective_parallelism{(std::max)(parallelism, 1uz)};
102
103 // Empty list
104 if (first == last) {
105 return;
106 }
107
108 // If it is a single element or there is no parallelism,
109 // just do a normal loop without dealing with threads
110 if (effective_parallelism == 1 || std::next(first) == last) {
111 std::size_t cursor{1};
112 for (auto iterator = first; iterator != last; ++iterator, ++cursor) {
113 callback(*iterator, effective_parallelism, cursor);
114 }
115
116 return;
117 }
118
119 std::queue<Iterator> tasks;
120 for (auto iterator = first; iterator != last; ++iterator) {
121 tasks.push(iterator);
122 }
123
124 std::mutex queue_mutex;
125 std::mutex exception_mutex;
126
127 auto effective_callback = std::forward<Callback>(callback);
128
129 std::exception_ptr exception = nullptr;
130 auto handle_exception = [&exception_mutex,
131 &exception](std::exception_ptr pointer) {
132 std::lock_guard<std::mutex> lock{exception_mutex};
133 if (!exception) {
134 exception = pointer;
135 }
136 };
137
138 std::vector<std::thread> workers;
139 workers.reserve(effective_parallelism);
140
141 const auto total{tasks.size()};
142
143 // Worker function that runs the actual per-item work and captures the
144 // environment by reference. It will be heap-copied into the native thread
145 // API.
146 auto worker_callable = [&tasks, &queue_mutex, &effective_callback,
147 &handle_exception, effective_parallelism, total] {
148 try {
149 while (true) {
150 Iterator iterator;
151 std::size_t cursor{0};
152 {
153 std::lock_guard<std::mutex> lock{queue_mutex};
154 if (tasks.empty()) {
155 return;
156 }
157 iterator = tasks.front();
158 cursor = total - tasks.size() + 1;
159 tasks.pop();
160 }
161 effective_callback(*iterator, effective_parallelism, cursor);
162 }
163 } catch (...) {
164 handle_exception(std::current_exception());
165 }
166 };
167
168 // TODO: Replace std::function with std::move_only_function once
169 // Apple Clang ships libc++ 19+ (__cpp_lib_move_only_function)
170#if defined(_WIN32)
171 for (std::size_t index = 0; index < effective_parallelism; ++index) {
172 auto *heap_function = new std::function<void()>(worker_callable);
173 if (stack_size_bytes > static_cast<std::size_t>(UINT_MAX)) {
174 delete heap_function;
175 parallel_for_each_drain_and_join(tasks, queue_mutex, workers);
176 throw std::runtime_error(
177 "The requested stack size is too large for this platform");
178 }
179
180 auto raw_handle = _beginthreadex(
181 nullptr, static_cast<unsigned>(stack_size_bytes),
182 &parallel_for_each_windows_thread_start, heap_function, 0, nullptr);
183 if (raw_handle == 0) {
184 delete heap_function;
185 parallel_for_each_drain_and_join(tasks, queue_mutex, workers);
186 throw std::runtime_error("Could not create thread");
187 }
188
189 HANDLE thread_handle = reinterpret_cast<HANDLE>(raw_handle);
190 workers.emplace_back([thread_handle] {
191 WaitForSingleObject(thread_handle, INFINITE);
192 CloseHandle(thread_handle);
193 });
194 }
195#else
196 for (std::size_t index = 0; index < effective_parallelism; ++index) {
197 // We can't use std::thread, as it doesn't let us tweak the thread stack
198 // size
199 pthread_attr_t attr;
200 pthread_attr_init(&attr);
201 if (stack_size_bytes > 0) {
202 pthread_attr_setstacksize(&attr, stack_size_bytes);
203 }
204
205 auto *heap_function = new std::function<void()>(worker_callable);
206 pthread_t pthread_handle;
207 auto raw_handle = pthread_create(
208 &pthread_handle, &attr,
209 [](void *arg) -> void * {
210 auto *function_ptr = static_cast<std::function<void()> *>(arg);
211 (*function_ptr)();
212 delete function_ptr;
213 return nullptr;
214 },
215 heap_function);
216 if (raw_handle != 0) {
217 pthread_attr_destroy(&attr);
218 delete heap_function;
219 parallel_for_each_drain_and_join(tasks, queue_mutex, workers);
220 throw std::runtime_error("Could not create thread");
221 }
222 workers.emplace_back(
223 [pthread_handle] { pthread_join(pthread_handle, nullptr); });
224 pthread_attr_destroy(&attr);
225 }
226#endif
227
228 for (auto &worker_thread : workers) {
229 worker_thread.join();
230 }
231
232 if (exception) {
233 std::rethrow_exception(exception);
234 }
235}
236
237} // namespace sourcemeta::core
238
239#endif
auto parallel_for_each(Iterator first, Iterator last, Callback &&callback, const std::size_t parallelism=std::thread::hardware_concurrency(), const std::size_t stack_size_bytes=0) -> void
Definition parallel_for_each.h:97