libsemigroups  v3.0.0
C++ library for semigroups and monoids
Loading...
Searching...
No Matches
race.hpp
1//
2// libsemigroups - C++ library for semigroups and monoids
3// Copyright (C) 2019-2025 James D. Mitchell
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with this program. If not, see <http://www.gnu.org/licenses/>.
17//
18
19// This file contains the declaration of the Race class template for
20// competitively running different functions/methods in different threads, and
21// obtaining the winner.
22
23// TODO(later):
24// 1. consider if keeping killed off methods has any uses
25// 2. update run_until to be similar to Runner::run_until
26// 3. add tpp file
27
28#ifndef LIBSEMIGROUPS_DETAIL_RACE_HPP_
29#define LIBSEMIGROUPS_DETAIL_RACE_HPP_
30
31#include <chrono> // for nanoseconds
32#include <cstddef> // for size_t
33#include <memory> // for std::shared_ptr
34#include <thread> // for mutex
35#include <type_traits> // for invoke_result_t
36#include <vector> // for vector
37
38#include "libsemigroups/constants.hpp" // for UNDEFINED
39#include "libsemigroups/debug.hpp" // for LIBSEMIGROUPS_ASSERT
40#include "libsemigroups/exception.hpp" // for LIBSEMIGROUPS_EXCEPTION
41#include "libsemigroups/runner.hpp" // for Runner
42
43#include "report.hpp" // for report_default, REPORT_TIME
44#include "stl.hpp" // for IsCallable
45#include "timer.hpp" // for Timer
46
47namespace libsemigroups {
48 namespace detail {
49 class Race : public Reporter {
50 size_t _max_threads;
51 std::mutex _mtx;
52 std::vector<std::shared_ptr<Runner>> _runners;
53 std::shared_ptr<Runner> _winner;
54 size_t _winner_index;
55
56 public:
57 // Construct an empty Race object, with maximum number of threads set to
58 // std::thread::hardware_concurrency.
59 Race();
60
61 // TODO(1) to cpp
62 Race& init() {
63 _max_threads = std::thread::hardware_concurrency();
64 // do nothing to the _mtx
65 _runners.clear();
66 _winner = nullptr;
67 _winner_index = UNDEFINED;
68 return *this;
69 }
70
71 Race(Race const& other) : Race() {
72 // Can't use = default because std::mutex is non-copyable.
73 _max_threads = other._max_threads;
74 // do nothing to the _mtx
75 _runners = other._runners;
76 _winner = other._winner;
77 _winner_index = other._winner_index;
78 }
79
80 // TODO(1) to cpp
81 Race(Race&& other)
82 : _max_threads(std::move(other._max_threads)),
83 _mtx(),
84 _runners(std::move(other._runners)),
85 _winner(std::move(other._winner)),
86 _winner_index(std::move(other._winner_index)) {}
87
88 // TODO(1) to cpp
89 Race& operator=(Race const& other) {
90 // Can't use = default because std::mutex is non-copyable.
91 _max_threads = other._max_threads;
92 // do nothing to the _mtx
93 _runners = other._runners;
94 _winner = other._winner;
95 _winner_index = other._winner_index;
96 return *this;
97 }
98
99 // TODO(1) to cpp
100 Race& operator=(Race&& other) {
101 // Can't use = default because std::mutex is non-copyable.
102 _max_threads = std::move(other._max_threads);
103 // do nothing to the _mtx
104 _runners = std::move(other._runners);
105 _winner = std::move(other._winner);
106 _winner_index = std::move(other._winner_index);
107 return *this;
108 }
109
110 ~Race();
111
112 // Set the maximum number of threads, throws if try to set to 0.
113 Race& max_threads(size_t val) noexcept {
114 LIBSEMIGROUPS_ASSERT(val != 0);
115 _max_threads = val;
116 return *this;
117 }
118
119 [[nodiscard]] size_t max_threads() const noexcept {
120 return _max_threads;
121 }
122
123 // Runs the method Runner::run on every Runner in the Race, and returns
124 // the one that finishes first. The losers are deleted.
125 [[nodiscard]] std::shared_ptr<Runner> winner() {
126 run();
127 return _winner;
128 }
129
130 [[nodiscard]] size_t winner_index() const {
131 return _winner_index;
132 }
133
134 [[nodiscard]] size_t winner_index() {
135 run();
136 return _winner_index;
137 }
138
139 [[nodiscard]] bool finished() const noexcept {
140 return _winner != nullptr && _winner->finished();
141 }
142
143 // Adds a Runner to the race, throws if the race is already over.
144 void add_runner(std::shared_ptr<Runner>);
145
146 using const_iterator =
147 typename std::vector<std::shared_ptr<Runner>>::const_iterator;
148
149 [[nodiscard]] const_iterator begin() const noexcept {
150 return _runners.cbegin();
151 }
152
153 [[nodiscard]] const_iterator end() const noexcept {
154 return _runners.cend();
155 }
156
157 // Returns an iterator pointing to the first Runner in the Race.
158 [[nodiscard]] const_iterator cbegin() const noexcept {
159 return _runners.cbegin();
160 }
161
162 // Returns an iterator pointing to one past the last Runner in the Race.
163 [[nodiscard]] const_iterator cend() const noexcept {
164 return _runners.cend();
165 }
166
167 // Returns \c true if there are no Runners in the race, and \c false
168 // otherwise.
169 // std::vector::empty is noexcept
170 [[nodiscard]] bool empty() const noexcept {
171 return _runners.empty();
172 }
173
174 // std::vector::size is noexcept
175 [[nodiscard]] size_t number_of_runners() const noexcept {
176 return _runners.size();
177 }
178
179 // Runs the race to completion.
180 void run();
181
182 // Runs the race for the specified amount of time.
183 void run_for(std::chrono::nanoseconds);
184
185 // Runs until \p func returns \c true, or the race is over. This
186 // repeatedly calls Race::run_for for \p check_interval, and then checks
187 // whether or not \p func() returns true. The object \p func can be any
188 // callable object with 0 parameters and that returns a bool.
189 // This is definitely tested but doesn't show up in the code coverage for
190 // some reason.
191 template <typename Func>
192 void run_until(Func&& func) {
193 static_assert(
194 std::is_same_v<std::invoke_result_t<Func>, bool>,
195 "the result type of calling an object of type Func (the template "
196 "parameter) must be bool!");
197 if (empty()) {
198 LIBSEMIGROUPS_EXCEPTION("no runners given, cannot run_until");
199 }
200 // while (!func() && _winner == nullptr) {
201 // // if _winner != nullptr, then the race is over.
202 // run_for(check_interval);
203 // check_interval *= 2;
204 // }
205 // return;
206 report_default("{}: running until predicate returns true or finished\n",
207 report_prefix());
208 run_func([&func](std::shared_ptr<Runner> r) -> void {
209 r->run_until(std::forward<Func>(func));
210 });
211 }
212
213 template <typename T>
214 bool has() const {
215 return find_runner<T>() != nullptr;
216 }
217
218 template <typename T>
219 [[nodiscard]] std::shared_ptr<T> find_runner() const {
220 static_assert(std::is_base_of<Runner, T>::value,
221 "the template parameter must be derived from Runner");
222 // We use find_if so that this works even if we haven't computed
223 // anything at all.
224 auto it = std::find_if(_runners.begin(),
225 _runners.end(),
226 [](std::shared_ptr<Runner> const& m) {
227 auto& r = *(m.get());
228 return typeid(r) == typeid(T);
229 });
230 if (it != _runners.end()) {
231 return std::static_pointer_cast<T>(*it);
232 } else {
233 return nullptr;
234 }
235 }
236
237 void erase_runners(const_iterator pos) {
238 _runners.erase(pos);
239 }
240
241 void erase_runners(const_iterator first, const_iterator last) {
242 _runners.erase(first, last);
243 }
244
245 private:
246 void clear_runners_after_race() {
247 if (_winner != nullptr) {
248 for (auto rnnr : _runners) {
249 if (rnnr != _winner) {
250 rnnr.reset();
251 }
252 }
253 _runners.clear();
254 _runners.push_back(_winner);
255 }
256 }
257
258 // Runs the callable object \p func on every Runner in parallel.
259 template <typename Func>
260 void run_func(Func&& func) {
261 static_assert(
262 std::is_same_v<std::invoke_result_t<Func, std::shared_ptr<Runner>>,
263 void>,
264 "the result type of calling an object of type Func (the template "
265 "parameter) must be void!");
266 using libsemigroups::detail::string_time;
267 LIBSEMIGROUPS_ASSERT(!empty());
269 if (_winner == nullptr) {
270 size_t nr_threads = std::min(_runners.size(), _max_threads);
271 if (nr_threads == 1) {
272 report_default("{}: using 0 additional threads\n", report_prefix());
273 func(_runners.at(0));
274 if (_runners.at(0)->success()) {
275 _winner = _runners.at(0);
276 _winner_index = 0;
277 clear_runners_after_race();
278 }
279 report_default("{}: elapsed time {}\n",
281 string_time(delta(start_time())));
282 return;
283 }
284 for (size_t i = 0; i < _runners.size(); ++i) {
285 if (_runners[i]->success()) {
286 report_default("{}: using 0 additional threads\n",
287 report_prefix());
288 _winner = _runners[i];
289 _winner_index = i;
290 report_default("{}: #{} already finished successfully!\n",
292 i);
293 // delete the other runners?
294 clear_runners_after_race();
295 return;
296 }
297 }
298
299 std::vector<std::thread::id> tids(_runners.size(),
301
302 report_default("{}: using {} / {} additional threads\n",
304 nr_threads,
306 detail::Timer tmr;
307 LIBSEMIGROUPS_ASSERT(nr_threads != 0);
308
309 auto thread_func = [this, &func, &tids](size_t pos) {
310 tids[pos] = std::this_thread::get_id();
311 try {
312 func(_runners.at(pos));
313 } catch (std::exception const& e) {
314 size_t tid = thread_id(tids[pos]);
315 report_default("{}: exception thrown by #{}:\n{}\n",
317 tid,
318 e.what());
319 return;
320 }
321 // Stop two Runner* objects from killing each other
322 {
323 std::lock_guard<std::mutex> lg(_mtx);
324 if (_runners.at(pos)->success()) {
325 for (auto it = _runners.begin(); it < _runners.begin() + pos;
326 it++) {
327 (*it)->kill();
328 }
329 for (auto it = _runners.begin() + pos + 1; it < _runners.end();
330 it++) {
331 (*it)->kill();
332 }
333 }
334 }
335 };
336 reset_thread_ids();
337
338 std::vector<std::thread> t;
339 for (size_t i = 0; i < nr_threads; ++i) {
340 t.push_back(std::thread(thread_func, i));
341 }
342 for (size_t i = 0; i < nr_threads; ++i) {
343 t.at(i).join();
344 }
345 report_default("{}: elapsed time {}\n",
347 string_time(delta(start_time())));
348 for (auto method = _runners.begin(); method < _runners.end();
349 ++method) {
350 if ((*method)->success()) {
351 LIBSEMIGROUPS_ASSERT(_winner == nullptr);
352 _winner = *method;
353 _winner_index = method - _runners.begin();
354 size_t tid = thread_id(tids.at(method - _runners.begin()));
355 report_default("{}: #{} is the winner!\n", report_prefix(), tid);
356 break;
357 }
358 }
359 clear_runners_after_race();
360 }
361 }
362 };
363 } // namespace detail
364} // namespace libsemigroups
365
366#endif // LIBSEMIGROUPS_DETAIL_RACE_HPP_
T at(T... args)
Reporter const & reset_start_time() const
Reset the start time (and last report) to now.
Definition runner.hpp:250
std::string const & report_prefix() const noexcept
Get the current prefix string for reporting.
Definition runner.hpp:320
static std::chrono::nanoseconds delta(std::chrono::high_resolution_clock::time_point const &t)
The time between a given point and now.
Definition runner.hpp:65
time_point start_time() const noexcept
Get the start time.
Definition runner.hpp:238
T find_if(T... args)
T forward(T... args)
T get_id(T... args)
Undefined const UNDEFINED
Value for something undefined.
#define LIBSEMIGROUPS_EXCEPTION(...)
Throw a LibsemigroupsException.
Definition exception.hpp:95
T hardware_concurrency(T... args)
T min(T... args)
T move(T... args)
Namespace for everything in the libsemigroups library.
Definition action.hpp:44
void report_default(std::string_view sv, Args &&... args)
No doc.
Definition report.hpp:157
T static_pointer_cast(T... args)
T push_back(T... args)
T what(T... args)