test/unittest/UnitThreadPool.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // Copyright (c) 2021-2026 ChilliBits. All rights reserved. | ||
| 2 | |||
| 3 | #include <gtest/gtest.h> | ||
| 4 | |||
| 5 | #include <atomic> | ||
| 6 | #include <mutex> | ||
| 7 | #include <ranges> | ||
| 8 | #include <stdexcept> | ||
| 9 | #include <string> | ||
| 10 | #include <thread> | ||
| 11 | #include <unordered_map> | ||
| 12 | |||
| 13 | #include <util/Concurrency.h> | ||
| 14 | #include <util/ThreadPool.h> | ||
| 15 | |||
| 16 | // LCOV_EXCL_START | ||
| 17 | |||
| 18 | namespace spice::testing { | ||
| 19 | |||
| 20 | using namespace spice::compiler; | ||
| 21 | |||
| 22 | − | TEST(ThreadPoolTest, RunsAllSubmittedTasks) { | |
| 23 | − | ThreadPool pool(4); | |
| 24 | − | std::atomic<size_t> executedTasks = 0; | |
| 25 | − | for (size_t i = 0; i < 1000; i++) | |
| 26 | − | pool.submit([&executedTasks] { ++executedTasks; }); | |
| 27 | − | pool.waitForAll(); | |
| 28 | |||
| 29 | − | EXPECT_EQ(1000u, executedTasks.load()); | |
| 30 | − | EXPECT_EQ(4u, pool.getThreadCount()); | |
| 31 | − | } | |
| 32 | |||
| 33 | − | TEST(ThreadPoolTest, RethrowsExceptionOfEarliestFailingTask) { | |
| 34 | − | ThreadPool pool(4); | |
| 35 | // Two tasks fail. Independent of which worker gets there first, the failure of the task that was submitted first has | ||
| 36 | // to win, so that the reported compiler error does not depend on the scheduling. | ||
| 37 | − | for (size_t i = 0; i < 200; i++) | |
| 38 | − | pool.submit([i] { | |
| 39 | − | if (i == 7 || i == 90) | |
| 40 | − | throw std::runtime_error("task " + std::to_string(i)); | |
| 41 | − | }); | |
| 42 | |||
| 43 | try { | ||
| 44 | − | pool.waitForAll(); | |
| 45 | − | FAIL() << "Expected the failure of the earliest task to be re-thrown"; // GCOV_EXCL_LINE | |
| 46 | − | } catch (const std::runtime_error &e) { | |
| 47 | − | EXPECT_EQ("task 7", std::string(e.what())); | |
| 48 | − | } | |
| 49 | − | } | |
| 50 | |||
| 51 | − | TEST(ThreadPoolTest, IsReusableAfterFailure) { | |
| 52 | − | ThreadPool pool(2); | |
| 53 | − | pool.submit([] { throw std::runtime_error("boom"); }); | |
| 54 | − | EXPECT_THROW(pool.waitForAll(), std::runtime_error); | |
| 55 | |||
| 56 | − | std::atomic<size_t> executedTasks = 0; | |
| 57 | − | for (size_t i = 0; i < 100; i++) | |
| 58 | − | pool.submit([&executedTasks] { ++executedTasks; }); | |
| 59 | − | pool.waitForAll(); | |
| 60 | |||
| 61 | − | EXPECT_EQ(100u, executedTasks.load()); | |
| 62 | − | } | |
| 63 | |||
| 64 | − | TEST(ConcurrencyTest, ConditionalLockOnlyLocksInParallelSection) { | |
| 65 | − | std::mutex mutex; | |
| 66 | // try_lock on a mutex that is already held by the calling thread is undefined behavior, so the probe has to run on | ||
| 67 | // another thread | ||
| 68 | − | const auto isLockedByUs = [&mutex] { | |
| 69 | − | bool couldLock = false; | |
| 70 | − | std::thread probe([&] { | |
| 71 | − | couldLock = mutex.try_lock(); | |
| 72 | − | if (couldLock) | |
| 73 | − | mutex.unlock(); | |
| 74 | − | }); | |
| 75 | − | probe.join(); | |
| 76 | − | return !couldLock; | |
| 77 | − | }; | |
| 78 | |||
| 79 | − | ASSERT_FALSE(concurrentPassesRunning.load()); | |
| 80 | |||
| 81 | { // Outside of a parallel section the lock is a no-op | ||
| 82 | − | ConditionalLock lock(mutex); | |
| 83 | − | EXPECT_FALSE(isLockedByUs()); | |
| 84 | − | } | |
| 85 | |||
| 86 | { // Inside a parallel section the mutex is held for the lifetime of the lock | ||
| 87 | − | const ParallelSection parallelSection; | |
| 88 | − | ASSERT_TRUE(concurrentPassesRunning.load()); | |
| 89 | − | ConditionalLock lock(mutex); | |
| 90 | − | EXPECT_TRUE(isLockedByUs()); | |
| 91 | − | } | |
| 92 | |||
| 93 | − | EXPECT_FALSE(concurrentPassesRunning.load()); | |
| 94 | } | ||
| 95 | |||
| 96 | − | TEST(ConcurrencyTest, ConditionalLockGuardsSharedMapUnderContention) { | |
| 97 | − | ThreadPool pool(4); | |
| 98 | − | std::unordered_map<size_t, size_t> sharedMap; // Stands in for the type registry / lookup caches | |
| 99 | |||
| 100 | { | ||
| 101 | − | std::mutex mutex; | |
| 102 | − | const ParallelSection parallelSection; | |
| 103 | − | for (size_t i = 0; i < 4; i++) | |
| 104 | − | pool.submit([&] { | |
| 105 | − | for (size_t key = 0; key < 10000; key++) { | |
| 106 | − | ConditionalLock lock(mutex); | |
| 107 | − | sharedMap[key % 500]++; | |
| 108 | − | } | |
| 109 | − | }); | |
| 110 | − | pool.waitForAll(); | |
| 111 | − | } | |
| 112 | |||
| 113 | − | ASSERT_EQ(500u, sharedMap.size()); | |
| 114 | − | for (const auto &count : sharedMap | std::views::values) | |
| 115 | − | EXPECT_EQ(4u * 20u, count); | |
| 116 | − | } | |
| 117 | |||
| 118 | } // namespace spice::testing | ||
| 119 | |||
| 120 | // LCOV_EXCL_STOP | ||
| 121 |