File indexing completed on 2026-10-29 08:25:01
0001
0002
0003
0004
0005
0006
0007
0008 #include <algorithms/algorithm.h>
0009 #include <algorithms/logger.h>
0010 #include <algorithms/random.h>
0011
0012 #include <catch2/catch_test_macros.hpp>
0013
0014 #include <algorithm>
0015 #include <cstddef>
0016 #include <cstdint>
0017 #include <functional>
0018 #include <latch>
0019 #include <limits>
0020 #include <mutex>
0021 #include <set>
0022 #include <string>
0023 #include <string_view>
0024 #include <thread>
0025 #include <vector>
0026
0027 using namespace algorithms;
0028
0029 namespace {
0030
0031 constexpr std::size_t kThreads = 8;
0032 constexpr std::size_t kIterations = 1000;
0033
0034
0035 void runConcurrently(const std::function<void(std::size_t)>& f) {
0036 std::latch start{kThreads};
0037 std::vector<std::jthread> threads;
0038 for (std::size_t t = 0; t < kThreads; ++t) {
0039 threads.emplace_back([&f, &start, t] {
0040 start.arrive_and_wait();
0041 f(t);
0042 });
0043 }
0044 }
0045
0046
0047 class Scale : public Algorithm<Input<int, std::optional<int>, std::vector<int>>,
0048 Output<int, std::vector<int>>> {
0049 public:
0050 Scale()
0051 : Algorithm{"Scale",
0052 {"value", "offset", "values"},
0053 {"scaled", "scaledValues"},
0054 "Scales values by a factor, with an optional offset"} {}
0055
0056 void process(const Input& input, const Output& output) const final {
0057 const auto& [value, offset, values] = input;
0058 const auto& [scaled, scaled_values] = output;
0059
0060 const int add = (offset != nullptr) ? *offset : 0;
0061 *scaled = *value * m_factor + add;
0062 for (std::size_t i = 0; i < std::min(values.size(), scaled_values.size()); ++i) {
0063 *scaled_values[i] = *values[i] * m_factor + add;
0064 }
0065 }
0066
0067 private:
0068 Property<int> m_factor{this, "factor", 3, "Scale factor"};
0069 };
0070
0071 class TestLogger : public LoggerMixin {
0072 public:
0073 TestLogger() : LoggerMixin("TestLogger") {}
0074 using LoggerMixin::endmsg;
0075 using LoggerMixin::info;
0076 };
0077
0078
0079 class Recorder {
0080 public:
0081
0082 static Recorder& instance() {
0083 static Recorder& recorder = []() -> Recorder& {
0084 static Recorder r;
0085 LogSvc::instance().init([](LogLevel, std::string_view, std::string_view msg) {
0086 std::lock_guard<std::mutex> lock{r.m_mutex};
0087 r.m_messages.emplace_back(msg);
0088 });
0089 return r;
0090 }();
0091 return recorder;
0092 }
0093 void clear() {
0094 std::lock_guard<std::mutex> lock{m_mutex};
0095 m_messages.clear();
0096 }
0097 std::vector<std::string> messages() {
0098 std::lock_guard<std::mutex> lock{m_mutex};
0099 return m_messages;
0100 }
0101
0102 private:
0103 std::mutex m_mutex;
0104 std::vector<std::string> m_messages;
0105 };
0106
0107
0108 constexpr const char* kIndexedSvcNames[] = {"IndexedSvc0", "IndexedSvc1", "IndexedSvc2",
0109 "IndexedSvc3", "IndexedSvc4", "IndexedSvc5",
0110 "IndexedSvc6", "IndexedSvc7"};
0111 template <std::size_t N> class IndexedSvc : public Service<IndexedSvc<N>> {
0112 public:
0113 void init() {}
0114 constexpr static const char* kName = kIndexedSvcNames[N];
0115
0116 protected:
0117 IndexedSvc() : Service<IndexedSvc<N>>(kName) {}
0118 friend class Service<IndexedSvc<N>>;
0119 };
0120
0121 }
0122
0123 TEST_CASE("Algorithms can process concurrently on one instance", "[multithreading]") {
0124 Scale algo;
0125 algo.setProperty("factor", 2);
0126 const Scale& shared = algo;
0127
0128 std::vector<std::size_t> failures(kThreads, 0);
0129 runConcurrently([&](std::size_t t) {
0130 for (std::size_t i = 0; i < kIterations; ++i) {
0131 const int value = static_cast<int>(t * kIterations + i);
0132 const int offset = static_cast<int>(t);
0133 const std::vector<int> values{value, value + 1};
0134 int scaled = 0;
0135 std::vector<int> scaled_values(2, 0);
0136
0137 shared.process({&value, (i % 2 == 0) ? &offset : nullptr, {&values[0], &values[1]}},
0138 {&scaled, {&scaled_values[0], &scaled_values[1]}});
0139
0140 const int add = (i % 2 == 0) ? offset : 0;
0141 if (scaled != 2 * value + add || scaled_values[0] != 2 * value + add ||
0142 scaled_values[1] != 2 * (value + 1) + add) {
0143 ++failures[t];
0144 }
0145 }
0146 });
0147 CHECK(failures == std::vector<std::size_t>(kThreads, 0));
0148 }
0149
0150 TEST_CASE("Algorithms can be constructed and configured concurrently", "[multithreading]") {
0151 std::vector<int> results(kThreads, 0);
0152 runConcurrently([&](std::size_t t) {
0153 Scale algo;
0154 algo.setProperty("factor", static_cast<int>(t));
0155 algo.validate();
0156 const int value = 10;
0157 int scaled = 0;
0158 algo.process({&value, nullptr, {}}, {&scaled, {}});
0159 results[t] = scaled;
0160 });
0161 for (std::size_t t = 0; t < kThreads; ++t) {
0162 CHECK(results[t] == static_cast<int>(10 * t));
0163 }
0164 }
0165
0166 TEST_CASE("Properties can be read concurrently", "[multithreading]") {
0167 const Scale algo;
0168 std::vector<std::size_t> failures(kThreads, 0);
0169 runConcurrently([&](std::size_t t) {
0170 for (std::size_t i = 0; i < kIterations; ++i) {
0171 if (algo.getProperty<int64_t>("factor") != 3 || !algo.hasProperty("factor") ||
0172 !algo.missingProperties().empty()) {
0173 ++failures[t];
0174 }
0175 }
0176 });
0177 CHECK(failures == std::vector<std::size_t>(kThreads, 0));
0178 }
0179
0180 TEST_CASE("Loggers report complete messages from concurrent threads", "[multithreading]") {
0181 auto& recorder = Recorder::instance();
0182 recorder.clear();
0183 const TestLogger logger;
0184
0185 runConcurrently([&](std::size_t t) {
0186 for (std::size_t i = 0; i < kIterations / 10; ++i) {
0187 logger.info(fmt::format("plain {} {}", t, i));
0188 logger.info("formatted {} {}", t, i);
0189 }
0190 });
0191
0192 std::set<std::string> expected;
0193 for (std::size_t t = 0; t < kThreads; ++t) {
0194 for (std::size_t i = 0; i < kIterations / 10; ++i) {
0195 expected.insert(fmt::format("plain {} {}", t, i));
0196 expected.insert(fmt::format("formatted {} {}", t, i));
0197 }
0198 }
0199 const auto messages = recorder.messages();
0200 CHECK(messages.size() == expected.size());
0201 CHECK(std::set<std::string>(messages.begin(), messages.end()) == expected);
0202 }
0203
0204 TEST_CASE("Logger streams report complete messages from concurrent threads",
0205 "[multithreading]") {
0206 auto& recorder = Recorder::instance();
0207 recorder.clear();
0208 const TestLogger logger;
0209
0210 runConcurrently([&](std::size_t t) {
0211 for (std::size_t i = 0; i < kIterations / 10; ++i) {
0212 logger.info() << "stream " << t << " " << i << TestLogger::endmsg;
0213 }
0214 });
0215
0216 std::set<std::string> expected;
0217 for (std::size_t t = 0; t < kThreads; ++t) {
0218 for (std::size_t i = 0; i < kIterations / 10; ++i) {
0219 expected.insert(fmt::format("stream {} {}", t, i));
0220 }
0221 }
0222 const auto messages = recorder.messages();
0223 CHECK(messages.size() == expected.size());
0224 CHECK(std::set<std::string>(messages.begin(), messages.end()) == expected);
0225 }
0226
0227 TEST_CASE("Different services can be accessed for the first time concurrently",
0228 "[multithreading]") {
0229 std::vector<const ServiceBase*> services(kThreads, nullptr);
0230 runConcurrently([&](std::size_t t) {
0231 switch (t) {
0232 case 0: services[t] = &IndexedSvc<0>::instance(); break;
0233 case 1: services[t] = &IndexedSvc<1>::instance(); break;
0234 case 2: services[t] = &IndexedSvc<2>::instance(); break;
0235 case 3: services[t] = &IndexedSvc<3>::instance(); break;
0236 case 4: services[t] = &IndexedSvc<4>::instance(); break;
0237 case 5: services[t] = &IndexedSvc<5>::instance(); break;
0238 case 6: services[t] = &IndexedSvc<6>::instance(); break;
0239 default: services[t] = &IndexedSvc<7>::instance(); break;
0240 }
0241 });
0242 const auto& registered = ServiceSvc::instance().services();
0243 for (std::size_t t = 0; t < kThreads; ++t) {
0244 const auto name = "IndexedSvc" + std::to_string(t);
0245 REQUIRE(registered.count(name) == 1);
0246 CHECK(registered.at(name) == services[t]);
0247 }
0248 }
0249
0250 TEST_CASE("A service can be accessed for the first time concurrently", "[multithreading]") {
0251 std::vector<const RandomSvc*> services(kThreads, nullptr);
0252 runConcurrently([&](std::size_t t) { services[t] = &RandomSvc::instance(); });
0253 CHECK(std::all_of(services.begin(), services.end(),
0254 [&](const auto* svc) { return svc == services[0]; }));
0255 }
0256
0257 TEST_CASE("A generator can be shared between threads", "[multithreading]") {
0258 const Generator gen{RandomSvc::instance().createEngine(1), 16};
0259 std::vector<std::vector<uint64_t>> draws(kThreads);
0260 runConcurrently([&](std::size_t t) {
0261 for (std::size_t i = 0; i < kIterations; ++i) {
0262 draws[t].push_back(gen.uniform_int<uint64_t>(0, std::numeric_limits<uint64_t>::max()));
0263 }
0264 });
0265
0266
0267 std::set<uint64_t> unique;
0268 for (const auto& d : draws) {
0269 unique.insert(d.begin(), d.end());
0270 }
0271 CHECK(unique.size() == kThreads * kIterations);
0272 }
0273
0274 TEST_CASE("Generators of one engine can be used in parallel threads", "[multithreading]") {
0275 const auto engine = RandomSvc::instance().createEngine(2);
0276 std::vector<std::vector<uint64_t>> draws(kThreads);
0277 std::vector<double> gaussians(kThreads, 0.);
0278 runConcurrently([&](std::size_t t) {
0279 const Generator gen{engine, 16};
0280 for (std::size_t i = 0; i < kIterations; ++i) {
0281 draws[t].push_back(gen.uniform_int<uint64_t>(0, std::numeric_limits<uint64_t>::max()));
0282 gaussians[t] += gen.gaussian();
0283 gen.poisson(2.5);
0284 }
0285 });
0286
0287
0288 std::set<uint64_t> unique;
0289 for (const auto& d : draws) {
0290 unique.insert(d.begin(), d.end());
0291 }
0292 CHECK(unique.size() == kThreads * kIterations);
0293 }