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:
Jorijn van der Graaf 2026-10-20 19:06:37 +02:00
commit f71cb43e85
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
5 changed files with 308 additions and 149 deletions

View file

@ -1,32 +1,37 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
export module Crafter.Thread:ThreadPool;
import std;
namespace Crafter {
struct ThreadStorage {
std::vector<std::function<void(void)>> tasks;
std::vector<std::function<void(void)>> buffer;
std::condition_variable cv;
std::mutex mutex;
std::atomic<unsigned int> taskCount;
std::thread thread;
};
export class ThreadPool {
public:
static inline std::atomic<bool> stopped;
static void Start(int maxThreads = -1);
static void Stop();
static void Enqueue(std::function<void(void)> task);
static void Enqueue(std::vector<std::function<void(void)>> tasks);
static void JoinPool();
static inline unsigned int threadCount;
static inline std::atomic<unsigned int> activeThreads;
private:
static inline std::unordered_map<std::thread::id, ThreadStorage*> threadIdMap;
static inline ThreadStorage* threadStorage;
static void WaitAndExecuteTask(ThreadStorage* threadStorage);
};
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
export module Crafter.Thread:ThreadPool;
import std;
namespace Crafter {
struct ThreadStorage {
std::vector<std::function<void(void)>> tasks;
std::vector<std::function<void(void)>> buffer;
std::condition_variable cv;
std::mutex mutex;
std::atomic<unsigned int> taskCount;
std::thread thread;
};
export class ThreadPool {
public:
static inline std::atomic<bool> stopped;
static void Start(int maxThreads = -1);
// Stops the workers, each once it has run what it was given, and ends
// a JoinPool; a task enqueued once Stop has begun may not run. From
// one thread at a time, not the pool's own: a task can't stop the
// pool it runs in. Again, or before Start, it does nothing. JoinPool
// can return before Stop has: Start a new pool once Stop returned.
static void Stop();
static void Enqueue(std::function<void(void)> task);
static void Enqueue(std::vector<std::function<void(void)>> tasks);
static void JoinPool();
static inline unsigned int threadCount;
static inline std::atomic<unsigned int> activeThreads;
private:
static inline std::unordered_map<std::thread::id, ThreadStorage*> threadIdMap;
static inline ThreadStorage* threadStorage;
static void WaitAndExecuteTask(ThreadStorage* threadStorage);
};
}