fix(threadpool): batches run every task once, and Stop ends the whole pool
- Enqueue(vector) dropped tasks when the count didn't divide by the workers: the last one got only the remainder. Tasks are now split as evenly as the count allows, each worker's taskCount grows by what it was given (the single-task Enqueue picks by it), and Start(1), which has no workers, leaves the batch for the caller's JoinPool. - Stop wakes every slot, JoinPool's too, after joining the workers: a thread that had joined the pool slept on. It takes each slot's mutex before notifying, so a waiter between its check and its sleep can't miss the wake-up, and works on the pool it started with. Again, or before Start, it does nothing; joining a thread twice threw. Its documentation says what it doesn't do: stop from a pool task, or from two threads at once, run tasks enqueued once it has begun, or have JoinPool wait for it. - Tests: EnqueueBatch, StopJoinPool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0837bbab4c
commit
f71cb43e85
5 changed files with 308 additions and 149 deletions
77
tests/EnqueueBatch/main.cpp
Normal file
77
tests/EnqueueBatch/main.cpp
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// ThreadPool::Enqueue(vector) runs every task exactly once, whatever the
|
||||
// task count. When the count didn't divide by the number of worker threads,
|
||||
// the last worker got only the remainder and tasksPerThread tasks were
|
||||
// dropped. The counts here cover fewer tasks than workers, an exact multiple,
|
||||
// one more and one less than a multiple, and a large uneven batch, then a
|
||||
// batch of single Enqueue calls, which rely on the per-thread task counts the
|
||||
// batch path also kept wrong.
|
||||
|
||||
#include <cstdlib>
|
||||
import Crafter.Thread;
|
||||
import std;
|
||||
using namespace Crafter;
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void Check(bool ok, std::string_view what) {
|
||||
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
||||
if (!ok) ++failures;
|
||||
}
|
||||
|
||||
// Waits until `done` reaches `expected` or a few seconds pass.
|
||||
bool WaitFor(const std::atomic<std::uint32_t>& done, std::uint32_t expected) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (done.load() < expected) {
|
||||
if (std::chrono::steady_clock::now() > deadline) return false;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void RunBatch(std::uint32_t count) {
|
||||
std::atomic<std::uint32_t> done = 0;
|
||||
std::vector<std::atomic<std::uint32_t>> runs(count);
|
||||
std::vector<std::function<void(void)>> tasks;
|
||||
tasks.reserve(count);
|
||||
for (std::uint32_t i = 0; i < count; ++i) {
|
||||
tasks.emplace_back([&runs, &done, i] {
|
||||
runs[i]++;
|
||||
done++;
|
||||
});
|
||||
}
|
||||
ThreadPool::Enqueue(std::move(tasks));
|
||||
const bool finished = WaitFor(done, count);
|
||||
// A little longer, in case something runs twice.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
const bool once = std::ranges::all_of(runs, [](const std::atomic<std::uint32_t>& r) { return r.load() == 1; });
|
||||
Check(finished && once, std::format("a batch of {} tasks runs every task exactly once ({} of {} done)", count, done.load(), count));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
ThreadPool::Start(4);
|
||||
const std::uint32_t workers = ThreadPool::activeThreads;
|
||||
std::println(" {} worker threads", workers);
|
||||
|
||||
for (std::uint32_t count : {1u, workers - 1, workers, workers + 1, 3 * workers + 2, 1000u}) {
|
||||
RunBatch(count);
|
||||
}
|
||||
|
||||
// Single Enqueue calls pick the least loaded worker by its task count;
|
||||
// a count left wrong by a batch would starve or flood one of them.
|
||||
std::atomic<std::uint32_t> done = 0;
|
||||
for (std::uint32_t i = 0; i < 200; ++i) {
|
||||
ThreadPool::Enqueue([&done] { done++; });
|
||||
}
|
||||
Check(WaitFor(done, 200), std::format("200 single tasks after the batches all run ({} done)", done.load()));
|
||||
|
||||
ThreadPool::Stop();
|
||||
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
|
||||
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
Loading…
Reference in a new issue