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
|
|
@ -1,119 +1,147 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module Crafter.Thread:ThreadPool_impl;
|
||||
import std;
|
||||
import :ThreadPool;
|
||||
|
||||
using namespace Crafter;
|
||||
|
||||
|
||||
void ThreadPool::Start(int maxThreads)
|
||||
{
|
||||
stopped = false;
|
||||
if (maxThreads > 0) {
|
||||
threadCount = maxThreads;
|
||||
}
|
||||
else {
|
||||
threadCount = std::thread::hardware_concurrency();
|
||||
}
|
||||
threadStorage = new ThreadStorage[threadCount];
|
||||
for (unsigned int i = 0; i < threadCount - 1; i++)
|
||||
{
|
||||
threadStorage[i].thread = std::thread(ThreadPool::WaitAndExecuteTask, threadStorage + i);
|
||||
threadIdMap[threadStorage[i].thread.get_id()] = &threadStorage[i];
|
||||
}
|
||||
threadIdMap[std::this_thread::get_id()] = &threadStorage[threadCount - 1];
|
||||
activeThreads = threadCount - 1;
|
||||
}
|
||||
|
||||
void ThreadPool::Stop()
|
||||
{
|
||||
stopped = true;
|
||||
for (unsigned int i = 0; i < threadCount - 1; i++)
|
||||
{
|
||||
threadStorage[i].cv.notify_one();
|
||||
threadStorage[i].thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void ThreadPool::Enqueue(std::function<void(void)> task)
|
||||
{
|
||||
unsigned int min = std::numeric_limits<unsigned int>::max();
|
||||
unsigned int minIndex = 0;
|
||||
for (unsigned int i = 0; i < activeThreads; i++)
|
||||
{
|
||||
unsigned int taskCount = threadStorage[i].taskCount;
|
||||
if (taskCount == 0) {
|
||||
minIndex = i;
|
||||
break;
|
||||
}
|
||||
if (taskCount < min) {
|
||||
min = taskCount;
|
||||
minIndex = i;
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard lk(threadStorage[minIndex].mutex);
|
||||
threadStorage[minIndex].buffer.push_back(task);
|
||||
threadStorage[minIndex].taskCount++;
|
||||
}
|
||||
threadStorage[minIndex].cv.notify_one();
|
||||
}
|
||||
|
||||
void ThreadPool::Enqueue(std::vector<std::function<void(void)>> tasks)
|
||||
{
|
||||
unsigned int size = tasks.size();
|
||||
unsigned int tasksPerThread = size / activeThreads;
|
||||
int remainder = size % activeThreads;
|
||||
|
||||
if(remainder) {
|
||||
for (unsigned int i = 0; i < activeThreads - 1; i++)
|
||||
{
|
||||
{
|
||||
std::lock_guard lk(threadStorage[i].mutex);
|
||||
threadStorage[i].buffer.insert(threadStorage[i].buffer.end(), tasks.begin() + tasksPerThread * i, tasks.begin() + (tasksPerThread * i + tasksPerThread));
|
||||
threadStorage[i].taskCount += tasksPerThread * i;
|
||||
}
|
||||
threadStorage[i].cv.notify_one();
|
||||
}
|
||||
{
|
||||
std::lock_guard lk(threadStorage[activeThreads - 1].mutex);
|
||||
threadStorage[activeThreads - 1].buffer.insert(threadStorage[activeThreads - 1].buffer.end(), tasks.begin() + tasksPerThread * (activeThreads - 1), tasks.begin() + (tasksPerThread * (activeThreads - 1) + remainder));
|
||||
threadStorage[activeThreads - 1].taskCount += tasksPerThread + remainder;
|
||||
}
|
||||
threadStorage[activeThreads - 1].cv.notify_one();
|
||||
} else{
|
||||
for (unsigned int i = 0; i < activeThreads; i++)
|
||||
{
|
||||
{
|
||||
std::lock_guard lk(threadStorage[i].mutex);
|
||||
threadStorage[i].buffer.insert(threadStorage[i].buffer.end(), tasks.begin() + tasksPerThread * i, tasks.begin() + (tasksPerThread * i + tasksPerThread));
|
||||
threadStorage[i].taskCount += tasksPerThread * i;
|
||||
}
|
||||
threadStorage[i].cv.notify_one();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ThreadPool::JoinPool()
|
||||
{
|
||||
activeThreads++;
|
||||
ThreadPool::WaitAndExecuteTask(threadStorage + threadCount - 1);
|
||||
}
|
||||
|
||||
void ThreadPool::WaitAndExecuteTask(ThreadStorage* threadStorage)
|
||||
{
|
||||
while (!stopped || !threadStorage->buffer.empty()) {
|
||||
std::unique_lock<std::mutex> lock(threadStorage->mutex);
|
||||
threadStorage->cv.wait(lock, [threadStorage] { return !threadStorage->buffer.empty() || stopped; });
|
||||
threadStorage->buffer.swap(threadStorage->tasks);
|
||||
lock.unlock();
|
||||
for (std::function<void(void)> task : threadStorage->tasks)
|
||||
{
|
||||
task();
|
||||
threadStorage->taskCount--;
|
||||
}
|
||||
threadStorage->tasks.clear();
|
||||
}
|
||||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module Crafter.Thread:ThreadPool_impl;
|
||||
import std;
|
||||
import :ThreadPool;
|
||||
|
||||
using namespace Crafter;
|
||||
|
||||
|
||||
void ThreadPool::Start(int maxThreads)
|
||||
{
|
||||
stopped = false;
|
||||
if (maxThreads > 0) {
|
||||
threadCount = maxThreads;
|
||||
}
|
||||
else {
|
||||
threadCount = std::thread::hardware_concurrency();
|
||||
}
|
||||
threadStorage = new ThreadStorage[threadCount];
|
||||
for (unsigned int i = 0; i < threadCount - 1; i++)
|
||||
{
|
||||
threadStorage[i].thread = std::thread(ThreadPool::WaitAndExecuteTask, threadStorage + i);
|
||||
threadIdMap[threadStorage[i].thread.get_id()] = &threadStorage[i];
|
||||
}
|
||||
threadIdMap[std::this_thread::get_id()] = &threadStorage[threadCount - 1];
|
||||
activeThreads = threadCount - 1;
|
||||
}
|
||||
|
||||
void ThreadPool::Stop()
|
||||
{
|
||||
// This pool's own: a JoinPool caller may Start another as soon as it
|
||||
// returns, replacing threadStorage.
|
||||
ThreadStorage* const storage = threadStorage;
|
||||
const unsigned int count = threadCount;
|
||||
if (storage == nullptr) return;
|
||||
stopped = true;
|
||||
// Taking the slot's mutex orders the store above before the next check
|
||||
// of `stopped` by whoever waits on the slot: without it one between
|
||||
// checking the condition and going to sleep misses the notify, and never
|
||||
// wakes.
|
||||
auto wake = [](ThreadStorage& slot) {
|
||||
{
|
||||
std::lock_guard lk(slot.mutex);
|
||||
}
|
||||
slot.cv.notify_one();
|
||||
};
|
||||
for (unsigned int i = 0; i < count - 1; i++) wake(storage[i]);
|
||||
for (unsigned int i = 0; i < count - 1; i++)
|
||||
{
|
||||
if (storage[i].thread.joinable()) storage[i].thread.join();
|
||||
}
|
||||
// The JoinPool caller's slot last: a JoinPool waiting there returns once
|
||||
// the workers have stopped (one busy with a task sees `stopped` itself
|
||||
// when it's done).
|
||||
wake(storage[count - 1]);
|
||||
}
|
||||
|
||||
void ThreadPool::Enqueue(std::function<void(void)> task)
|
||||
{
|
||||
unsigned int min = std::numeric_limits<unsigned int>::max();
|
||||
unsigned int minIndex = 0;
|
||||
for (unsigned int i = 0; i < activeThreads; i++)
|
||||
{
|
||||
unsigned int taskCount = threadStorage[i].taskCount;
|
||||
if (taskCount == 0) {
|
||||
minIndex = i;
|
||||
break;
|
||||
}
|
||||
if (taskCount < min) {
|
||||
min = taskCount;
|
||||
minIndex = i;
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard lk(threadStorage[minIndex].mutex);
|
||||
threadStorage[minIndex].buffer.push_back(task);
|
||||
threadStorage[minIndex].taskCount++;
|
||||
}
|
||||
threadStorage[minIndex].cv.notify_one();
|
||||
}
|
||||
|
||||
void ThreadPool::Enqueue(std::vector<std::function<void(void)>> tasks)
|
||||
{
|
||||
const unsigned int workers = activeThreads;
|
||||
if (workers == 0) {
|
||||
// Start(1) runs no worker threads; like a single Enqueue, the batch
|
||||
// waits for the caller's JoinPool.
|
||||
{
|
||||
std::lock_guard lk(threadStorage[0].mutex);
|
||||
threadStorage[0].taskCount += static_cast<unsigned int>(tasks.size());
|
||||
threadStorage[0].buffer.insert(threadStorage[0].buffer.end(),
|
||||
std::make_move_iterator(tasks.begin()), std::make_move_iterator(tasks.end()));
|
||||
}
|
||||
threadStorage[0].cv.notify_one();
|
||||
return;
|
||||
}
|
||||
|
||||
// As even a split as the count allows: the first `remainder` workers
|
||||
// take one task more. Every task goes to exactly one worker, and each
|
||||
// worker's taskCount grows by what it was given, which the single-task
|
||||
// Enqueue relies on to pick the least loaded worker.
|
||||
const std::size_t perWorker = tasks.size() / workers;
|
||||
const std::size_t remainder = tasks.size() % workers;
|
||||
std::size_t first = 0;
|
||||
for (unsigned int i = 0; i < workers && first < tasks.size(); i++)
|
||||
{
|
||||
const std::size_t count = perWorker + (i < remainder ? 1 : 0);
|
||||
{
|
||||
std::lock_guard lk(threadStorage[i].mutex);
|
||||
threadStorage[i].taskCount += static_cast<unsigned int>(count);
|
||||
threadStorage[i].buffer.insert(threadStorage[i].buffer.end(),
|
||||
std::make_move_iterator(tasks.begin() + static_cast<std::ptrdiff_t>(first)),
|
||||
std::make_move_iterator(tasks.begin() + static_cast<std::ptrdiff_t>(first + count)));
|
||||
}
|
||||
threadStorage[i].cv.notify_one();
|
||||
first += count;
|
||||
}
|
||||
}
|
||||
|
||||
void ThreadPool::JoinPool()
|
||||
{
|
||||
activeThreads++;
|
||||
ThreadPool::WaitAndExecuteTask(threadStorage + threadCount - 1);
|
||||
}
|
||||
|
||||
void ThreadPool::WaitAndExecuteTask(ThreadStorage* threadStorage)
|
||||
{
|
||||
while (true) {
|
||||
std::unique_lock<std::mutex> lock(threadStorage->mutex);
|
||||
threadStorage->cv.wait(lock, [threadStorage] { return !threadStorage->buffer.empty() || stopped; });
|
||||
// The buffer is only read under the lock: the loop used to test it
|
||||
// unlocked while Enqueue appended to it. Stopping still waits for
|
||||
// everything queued to run.
|
||||
if (threadStorage->buffer.empty()) {
|
||||
return;
|
||||
}
|
||||
threadStorage->buffer.swap(threadStorage->tasks);
|
||||
lock.unlock();
|
||||
for (std::function<void(void)>& task : threadStorage->tasks)
|
||||
{
|
||||
task();
|
||||
threadStorage->taskCount--;
|
||||
}
|
||||
threadStorage->tasks.clear();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue