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::scoped_lock
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#ifndef NOMINMAX
20#define NOMINMAX
21#endif
22#include <windows.h>
23#else
24#include <pthread.h>
25#endif
26
27namespace sourcemeta::core {
28
29#ifndef DOXYGEN
30#if defined(_WIN32)
31// See
32// https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/beginthread-beginthreadex?view=msvc-170
33inline unsigned __stdcall parallel_for_each_windows_thread_start(
34 void *argument) {
35 auto *function_ptr = static_cast<std::function<void()> *>(argument);
36 (*function_ptr)();
37 delete function_ptr;
38 return 0;
39}
40#endif
41#endif
42
75template <typename Iterator, typename Callback>
76 requires std::input_iterator<Iterator> && std::copyable<Iterator> &&
77 std::invocable<Callback, std::iter_reference_t<Iterator>,
78 std::size_t, std::size_t>
80 Iterator first, Iterator last, Callback &&callback,
81 const std::size_t parallelism = std::thread::hardware_concurrency(),
82 const std::size_t stack_size_bytes = 0) -> void {
83 const auto effective_parallelism{(std::max)(parallelism, 1uz)};
84
85 // Empty list
86 if (first == last) {
87 return;
88 }
89
90 // If it is a single element or there is no parallelism,
91 // just do a normal loop without dealing with threads
92 if (effective_parallelism == 1 || std::next(first) == last) {
93 std::size_t cursor{1};
94 for (auto iterator = first; iterator != last; ++iterator, ++cursor) {
95 callback(*iterator, effective_parallelism, cursor);
96 }
97
98 return;
99 }
100
101 std::queue<Iterator> tasks;
102 for (auto iterator = first; iterator != last; ++iterator) {
103 tasks.push(iterator);
104 }
105
106 std::mutex queue_mutex;
107 std::mutex exception_mutex;
108
109 auto effective_callback = std::forward<Callback>(callback);
110
111 std::exception_ptr exception = nullptr;
112 auto handle_exception = [&exception_mutex,
113 &exception](const std::exception_ptr &pointer) {
114 std::scoped_lock lock{exception_mutex};
115 if (!exception) {
116 exception = pointer;
117 }
118 };
119
120 std::vector<std::thread> workers;
121 workers.reserve(effective_parallelism);
122
123 const auto total{tasks.size()};
124
125 // Worker function that runs the actual per-item work and captures the
126 // environment by reference. It will be heap-copied into the native thread
127 // API.
128 auto worker_callable = [&tasks, &queue_mutex, &effective_callback,
129 &handle_exception, effective_parallelism, total] {
130 try {
131 while (true) {
132 Iterator iterator;
133 std::size_t cursor{0};
134 {
135 std::scoped_lock lock{queue_mutex};
136 if (tasks.empty()) {
137 return;
138 }
139 iterator = tasks.front();
140 cursor = total - tasks.size() + 1;
141 tasks.pop();
142 }
143 effective_callback(*iterator, effective_parallelism, cursor);
144 }
145 } catch (...) {
146 handle_exception(std::current_exception());
147 }
148 };
149
150 const char *creation_error{nullptr};
151
152 // TODO: Replace std::function with std::move_only_function once
153 // Apple Clang ships libc++ 19+ (__cpp_lib_move_only_function)
154#if defined(_WIN32)
155 for (std::size_t index = 0; index < effective_parallelism; ++index) {
156 auto *heap_function = new std::function<void()>(worker_callable);
157 if (stack_size_bytes > static_cast<std::size_t>(UINT_MAX)) {
158 delete heap_function;
159 creation_error =
160 "The requested stack size is too large for this platform";
161 break;
162 }
163
164 auto raw_handle = _beginthreadex(
165 nullptr, static_cast<unsigned>(stack_size_bytes),
166 &parallel_for_each_windows_thread_start, heap_function, 0, nullptr);
167 if (raw_handle == 0) {
168 delete heap_function;
169 creation_error = "Could not create thread";
170 break;
171 }
172
173 HANDLE thread_handle = reinterpret_cast<HANDLE>(raw_handle);
174 workers.emplace_back([thread_handle] {
175 WaitForSingleObject(thread_handle, INFINITE);
176 CloseHandle(thread_handle);
177 });
178 }
179#else
180 for (std::size_t index = 0; index < effective_parallelism; ++index) {
181 // We can't use std::thread, as it doesn't let us tweak the thread stack
182 // size
183 pthread_attr_t attr;
184 pthread_attr_init(&attr);
185 // A stack size the platform cannot honour, such as one below its minimum
186 // or one that is not a multiple of its page size, must be reported rather
187 // than dropped in favour of the default, as the caller asked for it to
188 // bound how deep the work it hands over may recurse
189 if (stack_size_bytes > 0 &&
190 pthread_attr_setstacksize(&attr, stack_size_bytes) != 0) {
191 pthread_attr_destroy(&attr);
192 creation_error = "The requested stack size is not supported by this "
193 "platform";
194 break;
195 }
196
197 auto *heap_function = new std::function<void()>(worker_callable);
198 pthread_t pthread_handle;
199 auto raw_handle = pthread_create(
200 &pthread_handle, &attr,
201 [](void *arg) -> void * {
202 auto *function_ptr = static_cast<std::function<void()> *>(arg);
203 (*function_ptr)();
204 delete function_ptr;
205 return nullptr;
206 },
207 heap_function);
208 if (raw_handle != 0) {
209 pthread_attr_destroy(&attr);
210 delete heap_function;
211 creation_error = "Could not create thread";
212 break;
213 }
214 workers.emplace_back(
215 [pthread_handle] { pthread_join(pthread_handle, nullptr); });
216 pthread_attr_destroy(&attr);
217 }
218#endif
219
220 // If thread creation failed after some workers have already started, those
221 // workers keep referencing the stack locals of this frame, so unwinding past
222 // them must be avoided. Drain the remaining tasks so the running workers stop
223 // pulling new work and exit, then join every already-created worker before
224 // propagating the failure
225 if (creation_error != nullptr) {
226 std::scoped_lock lock{queue_mutex};
227 std::queue<Iterator> empty;
228 tasks.swap(empty);
229 }
230
231 for (auto &worker_thread : workers) {
232 worker_thread.join();
233 }
234
235 if (creation_error != nullptr) {
236 throw std::runtime_error(creation_error);
237 }
238
239 if (exception) {
240 std::rethrow_exception(exception);
241 }
242}
243
244} // namespace sourcemeta::core
245
246#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:79