mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-15 23:40:29 +02:00
Keep joinable handles in an intrusive producer queue. Remove the completion counter whose final notification could access already-freed manager state. Drain again after joining to cover dedicated tasks spawned by joined workers. External producers must already be quiescent during teardown, as required by the pooled queues.
476 lines
15 KiB
C++
476 lines
15 KiB
C++
#include <cstdio>
|
|
#include <algorithm>
|
|
#include <vector>
|
|
|
|
#include <retro_atomic.h>
|
|
#include <rthreads/rthreads.h>
|
|
#include <rthreads/retro_eventcount.h>
|
|
|
|
#include "Common/Log.h"
|
|
#include "Common/Thread/ThreadUtil.h"
|
|
#include "Common/Thread/ThreadManager.h"
|
|
|
|
// Threads and task scheduling
|
|
//
|
|
// * The threadpool should contain a number of threads that's the the number of cores,
|
|
// plus a fixed number more for I/O-limited background tasks.
|
|
// * Parallel compute-limited loops should use as many threads as there are cores.
|
|
// They should always be scheduled to the first N threads.
|
|
// * For some tasks, splitting the input values up linearly between the threads
|
|
// is not fair. However, we ignore that for now.
|
|
//
|
|
// Nothing here takes a lock:
|
|
// * Each pool (compute, I/O) has a bounded MPMC ring per priority that any worker of the
|
|
// pool takes from. A full ring spills into an MPSC stack that one worker drains whole.
|
|
// * Each worker has an MPSC stack per priority for tasks pinned to it.
|
|
// * Idle workers park on their own eventcount. A worker publishes its idle bit before it
|
|
// re-checks for work, and a producer claims an idle bit after publishing, with a
|
|
// seq_cst fence on both sides, so either the producer finds the bit or the worker finds
|
|
// the task.
|
|
|
|
#if !defined(RETRO_ATOMIC_HAS_PTR)
|
|
#error "The thread manager needs pointer atomics."
|
|
#endif
|
|
|
|
const int MIN_IO_BLOCKING_THREADS = 4;
|
|
static constexpr int TASK_PRIORITY_COUNT = (int)TaskPriority::COUNT;
|
|
static constexpr int RING_SIZE = 1024;
|
|
static constexpr int RING_MASK = RING_SIZE - 1;
|
|
static constexpr int IDLE_BITS_PER_WORD = 31;
|
|
|
|
ThreadManager g_threadManager;
|
|
|
|
static inline Task *TaskFromNode(mpsc_stack_node_t *node) {
|
|
return ((TaskLink *)node)->self;
|
|
}
|
|
|
|
static inline int WrapAdd(int a, int b) {
|
|
return (int)((unsigned)a + (unsigned)b);
|
|
}
|
|
|
|
// Bounded multi-producer multi-consumer ring of tasks (per-cell sequence numbers).
|
|
struct TaskRing {
|
|
struct Cell {
|
|
retro_atomic_int_t seq;
|
|
Task *task;
|
|
};
|
|
|
|
Cell cells[RING_SIZE];
|
|
alignas(64) retro_atomic_int_t enqueuePos;
|
|
alignas(64) retro_atomic_int_t dequeuePos;
|
|
|
|
TaskRing() {
|
|
for (int i = 0; i < RING_SIZE; i++) {
|
|
retro_atomic_int_init(&cells[i].seq, i);
|
|
cells[i].task = nullptr;
|
|
}
|
|
retro_atomic_int_init(&enqueuePos, 0);
|
|
retro_atomic_int_init(&dequeuePos, 0);
|
|
}
|
|
|
|
bool Push(Task *task) {
|
|
int pos = retro_atomic_load_relaxed_int(&enqueuePos);
|
|
Cell *cell;
|
|
for (;;) {
|
|
cell = &cells[pos & RING_MASK];
|
|
const int seq = retro_atomic_load_acquire_int(&cell->seq);
|
|
const int diff = (int)((unsigned)seq - (unsigned)pos);
|
|
if (diff == 0) {
|
|
if (retro_atomic_cas_int(&enqueuePos, pos, WrapAdd(pos, 1)))
|
|
break;
|
|
pos = retro_atomic_load_relaxed_int(&enqueuePos);
|
|
} else if (diff < 0) {
|
|
return false; // full
|
|
} else {
|
|
pos = retro_atomic_load_relaxed_int(&enqueuePos);
|
|
}
|
|
}
|
|
cell->task = task;
|
|
retro_atomic_store_release_int(&cell->seq, WrapAdd(pos, 1));
|
|
return true;
|
|
}
|
|
|
|
Task *Pop() {
|
|
int pos = retro_atomic_load_relaxed_int(&dequeuePos);
|
|
Cell *cell;
|
|
for (;;) {
|
|
cell = &cells[pos & RING_MASK];
|
|
const int seq = retro_atomic_load_acquire_int(&cell->seq);
|
|
const int diff = (int)((unsigned)seq - (unsigned)WrapAdd(pos, 1));
|
|
if (diff == 0) {
|
|
if (retro_atomic_cas_int(&dequeuePos, pos, WrapAdd(pos, 1)))
|
|
break;
|
|
pos = retro_atomic_load_relaxed_int(&dequeuePos);
|
|
} else if (diff < 0) {
|
|
return nullptr; // empty
|
|
} else {
|
|
pos = retro_atomic_load_relaxed_int(&dequeuePos);
|
|
}
|
|
}
|
|
Task *task = cell->task;
|
|
retro_atomic_store_release_int(&cell->seq, WrapAdd(pos, RING_SIZE));
|
|
return task;
|
|
}
|
|
|
|
bool HasItems() {
|
|
const int pos = retro_atomic_load_acquire_int(&dequeuePos);
|
|
const int seq = retro_atomic_load_acquire_int(&cells[pos & RING_MASK].seq);
|
|
return seq == WrapAdd(pos, 1);
|
|
}
|
|
};
|
|
|
|
struct TaskPool {
|
|
TaskRing ring[TASK_PRIORITY_COUNT];
|
|
mpsc_stack_t overflow[TASK_PRIORITY_COUNT];
|
|
std::vector<TaskThreadContext *> workers;
|
|
// One bit per worker (31 per word), set while the worker is parked or about to park.
|
|
retro_atomic_int_t *idle = nullptr;
|
|
int idleWords = 0;
|
|
|
|
TaskPool() {
|
|
for (int p = 0; p < TASK_PRIORITY_COUNT; p++)
|
|
mpsc_stack_init(&overflow[p]);
|
|
}
|
|
~TaskPool() {
|
|
delete[] idle;
|
|
}
|
|
};
|
|
|
|
struct GlobalThreadContext {
|
|
TaskPool pools[2]; // CPU_COMPUTE, IO_BLOCKING
|
|
std::vector<TaskThreadContext *> threads_;
|
|
|
|
mpsc_stack_t dedicatedThreads;
|
|
};
|
|
|
|
struct DedicatedStart {
|
|
mpsc_stack_node_t node;
|
|
Task *task;
|
|
sthread_t *thread;
|
|
};
|
|
|
|
struct LocalFifo {
|
|
mpsc_stack_node_t *head = nullptr;
|
|
mpsc_stack_node_t *tail = nullptr;
|
|
|
|
void Append(mpsc_stack_node_t *oldestFirst) {
|
|
if (!oldestFirst)
|
|
return;
|
|
if (tail)
|
|
tail->next = oldestFirst;
|
|
else
|
|
head = oldestFirst;
|
|
mpsc_stack_node_t *last = oldestFirst;
|
|
while (last->next)
|
|
last = last->next;
|
|
tail = last;
|
|
}
|
|
|
|
Task *Pop() {
|
|
mpsc_stack_node_t *node = head;
|
|
if (!node)
|
|
return nullptr;
|
|
head = node->next;
|
|
if (!head)
|
|
tail = nullptr;
|
|
node->next = nullptr;
|
|
return TaskFromNode(node);
|
|
}
|
|
};
|
|
|
|
struct TaskThreadContext {
|
|
sthread_t *thread = nullptr;
|
|
GlobalThreadContext *global = nullptr;
|
|
TaskPool *pool = nullptr;
|
|
int index = 0; // global index
|
|
int poolIndex = 0; // index within its pool
|
|
TaskType type = TaskType::CPU_COMPUTE;
|
|
char name[16]{};
|
|
|
|
mpsc_stack_t inbox[TASK_PRIORITY_COUNT]; // pinned tasks, any producer
|
|
LocalFifo local[TASK_PRIORITY_COUNT]; // worker-private, in order
|
|
retro_eventcount_t wake;
|
|
retro_atomic_int_t cancelled;
|
|
};
|
|
|
|
static TaskPool &PoolFor(GlobalThreadContext *global, TaskType type) {
|
|
return global->pools[type == TaskType::CPU_COMPUTE ? 0 : 1];
|
|
}
|
|
|
|
ThreadManager::ThreadManager() : global_(new GlobalThreadContext()) {
|
|
mpsc_stack_init(&global_->dedicatedThreads);
|
|
}
|
|
|
|
ThreadManager::~ThreadManager() {
|
|
// Make sure the worker threads are stopped and joined before we free global_ -
|
|
// otherwise, if Teardown() was never called (e.g. an early return between Init()
|
|
// and the caller's normal shutdown path), the still-running threads would go on
|
|
// touching global_'s queues after they're freed here.
|
|
Teardown();
|
|
delete global_;
|
|
}
|
|
|
|
static Task *TakeTask(TaskThreadContext *thread) {
|
|
TaskPool *pool = thread->pool;
|
|
for (int p = 0; p < TASK_PRIORITY_COUNT; ++p) {
|
|
Task *task = thread->local[p].Pop();
|
|
if (task)
|
|
return task;
|
|
if (!mpsc_stack_empty(&thread->inbox[p])) {
|
|
thread->local[p].Append(mpsc_stack_reverse(mpsc_stack_drain(&thread->inbox[p])));
|
|
if ((task = thread->local[p].Pop()) != nullptr)
|
|
return task;
|
|
}
|
|
if ((task = pool->ring[p].Pop()) != nullptr)
|
|
return task;
|
|
if (!mpsc_stack_empty(&pool->overflow[p])) {
|
|
thread->local[p].Append(mpsc_stack_reverse(mpsc_stack_drain(&pool->overflow[p])));
|
|
if ((task = thread->local[p].Pop()) != nullptr)
|
|
return task;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static bool HasWork(TaskThreadContext *thread) {
|
|
TaskPool *pool = thread->pool;
|
|
for (int p = 0; p < TASK_PRIORITY_COUNT; ++p) {
|
|
if (thread->local[p].head || !mpsc_stack_empty(&thread->inbox[p]) ||
|
|
pool->ring[p].HasItems() || !mpsc_stack_empty(&pool->overflow[p]))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void WorkerThreadFunc(void *arg) {
|
|
TaskThreadContext *thread = (TaskThreadContext *)arg;
|
|
if (thread->type == TaskType::CPU_COMPUTE) {
|
|
snprintf(thread->name, sizeof(thread->name), "PoolW %d", thread->index);
|
|
} else {
|
|
_assert_(thread->type == TaskType::IO_BLOCKING);
|
|
snprintf(thread->name, sizeof(thread->name), "PoolW IO %d", thread->index);
|
|
}
|
|
SetCurrentThreadName(thread->name);
|
|
|
|
|
|
TaskPool *pool = thread->pool;
|
|
retro_atomic_int_t *idleWord = &pool->idle[thread->poolIndex / IDLE_BITS_PER_WORD];
|
|
const int idleBit = 1 << (thread->poolIndex % IDLE_BITS_PER_WORD);
|
|
|
|
while (!retro_atomic_load_acquire_int(&thread->cancelled)) {
|
|
Task *task = TakeTask(thread);
|
|
if (task) {
|
|
// The task itself takes care of notifying anyone waiting on it.
|
|
task->Run();
|
|
task->Release();
|
|
continue;
|
|
}
|
|
|
|
// Nothing to do: publish idleness, then re-check before sleeping.
|
|
retro_atomic_fetch_or_int(idleWord, idleBit);
|
|
retro_atomic_thread_fence_seq_cst();
|
|
const int key = retro_eventcount_prepare_wait(&thread->wake);
|
|
if (HasWork(thread) || retro_atomic_load_acquire_int(&thread->cancelled)) {
|
|
retro_eventcount_cancel_wait(&thread->wake);
|
|
} else {
|
|
retro_eventcount_commit_wait(&thread->wake, key);
|
|
}
|
|
retro_atomic_fetch_and_int(idleWord, ~idleBit);
|
|
}
|
|
|
|
}
|
|
|
|
// Wakes one parked worker of the pool, if there is one. Call after publishing the task.
|
|
static void WakeOneIdle(TaskPool &pool) {
|
|
retro_atomic_thread_fence_seq_cst();
|
|
for (int w = 0; w < pool.idleWords; w++) {
|
|
int bits = retro_atomic_load_acquire_int(&pool.idle[w]);
|
|
while (bits) {
|
|
const int bit = bits & -bits;
|
|
const int old = retro_atomic_fetch_and_int(&pool.idle[w], ~bit);
|
|
if (old & bit) {
|
|
int worker = w * IDLE_BITS_PER_WORD;
|
|
for (int b = bit; b > 1; b >>= 1)
|
|
worker++;
|
|
retro_eventcount_notify(&pool.workers[worker]->wake);
|
|
return;
|
|
}
|
|
bits = old & ~bit;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ThreadManager::Teardown() {
|
|
// Dedicated tasks can enqueue or wait for pooled work. Drain them while
|
|
// the pools are still running, before cancellation removes their workers.
|
|
auto joinDedicated = [this] {
|
|
for (;;) {
|
|
mpsc_stack_node_t *node = mpsc_stack_drain(&global_->dedicatedThreads);
|
|
if (!node)
|
|
break;
|
|
while (node) {
|
|
mpsc_stack_node_t *next = node->next;
|
|
DedicatedStart *start = (DedicatedStart *)node;
|
|
sthread_join(start->thread);
|
|
delete start;
|
|
node = next;
|
|
}
|
|
}
|
|
};
|
|
joinDedicated();
|
|
|
|
for (TaskThreadContext *threadCtx : global_->threads_) {
|
|
retro_atomic_store_release_int(&threadCtx->cancelled, 1);
|
|
retro_eventcount_notify(&threadCtx->wake);
|
|
}
|
|
|
|
for (TaskThreadContext *threadCtx : global_->threads_) {
|
|
sthread_join(threadCtx->thread);
|
|
threadCtx->thread = nullptr;
|
|
}
|
|
// A pooled task finishing during cancellation may have launched another
|
|
// dedicated task. Keep the pool storage alive until those producers exit.
|
|
joinDedicated();
|
|
|
|
// No worker picks up anything once cancelled, so whatever is still queued will never
|
|
// run. Cancel and release it so it doesn't leak (and waiters on it are released).
|
|
for (TaskThreadContext *threadCtx : global_->threads_) {
|
|
for (int p = 0; p < TASK_PRIORITY_COUNT; ++p) {
|
|
threadCtx->local[p].Append(mpsc_stack_reverse(mpsc_stack_drain(&threadCtx->inbox[p])));
|
|
while (Task *task = threadCtx->local[p].Pop())
|
|
TeardownTask(task);
|
|
}
|
|
}
|
|
for (TaskPool &pool : global_->pools) {
|
|
for (int p = 0; p < TASK_PRIORITY_COUNT; ++p) {
|
|
while (Task *task = pool.ring[p].Pop())
|
|
TeardownTask(task);
|
|
mpsc_stack_node_t *node = mpsc_stack_reverse(mpsc_stack_drain(&pool.overflow[p]));
|
|
while (node) {
|
|
mpsc_stack_node_t *next = node->next;
|
|
node->next = nullptr;
|
|
TeardownTask(TaskFromNode(node));
|
|
node = next;
|
|
}
|
|
}
|
|
pool.workers.clear();
|
|
delete[] pool.idle;
|
|
pool.idle = nullptr;
|
|
pool.idleWords = 0;
|
|
}
|
|
|
|
for (TaskThreadContext *threadCtx : global_->threads_) {
|
|
retro_eventcount_free(&threadCtx->wake);
|
|
delete threadCtx;
|
|
}
|
|
global_->threads_.clear();
|
|
|
|
}
|
|
|
|
void ThreadManager::TeardownTask(Task *task) {
|
|
if (!task)
|
|
return;
|
|
|
|
if (task->Cancellable()) {
|
|
task->Cancel();
|
|
} else {
|
|
WARN_LOG(Log::System, "ThreadManager::Teardown() dropping a non-cancellable task that will never run");
|
|
}
|
|
task->Release();
|
|
}
|
|
|
|
void ThreadManager::Init(int numRealCores, int numLogicalCoresPerCpu) {
|
|
if (IsInitialized()) {
|
|
Teardown();
|
|
}
|
|
|
|
numComputeThreads_ = numRealCores * numLogicalCoresPerCpu;
|
|
// Double it for the IO blocking threads.
|
|
int numThreads = numComputeThreads_ + std::max(MIN_IO_BLOCKING_THREADS, numComputeThreads_);
|
|
numThreads_ = numThreads;
|
|
|
|
INFO_LOG(Log::System, "ThreadManager::Init(compute threads: %d, all: %d)", numComputeThreads_, numThreads_);
|
|
|
|
for (int i = 0; i < numThreads; i++) {
|
|
TaskThreadContext *thread = new TaskThreadContext();
|
|
thread->global = global_;
|
|
thread->type = i < numComputeThreads_ ? TaskType::CPU_COMPUTE : TaskType::IO_BLOCKING;
|
|
thread->index = i;
|
|
thread->pool = &PoolFor(global_, thread->type);
|
|
thread->poolIndex = (int)thread->pool->workers.size();
|
|
for (int p = 0; p < TASK_PRIORITY_COUNT; ++p)
|
|
mpsc_stack_init(&thread->inbox[p]);
|
|
retro_eventcount_init(&thread->wake);
|
|
retro_atomic_int_init(&thread->cancelled, 0);
|
|
thread->pool->workers.push_back(thread);
|
|
global_->threads_.push_back(thread);
|
|
}
|
|
|
|
for (TaskPool &pool : global_->pools) {
|
|
pool.idleWords = ((int)pool.workers.size() + IDLE_BITS_PER_WORD - 1) / IDLE_BITS_PER_WORD;
|
|
pool.idle = pool.idleWords ? new retro_atomic_int_t[pool.idleWords] : nullptr;
|
|
for (int w = 0; w < pool.idleWords; w++)
|
|
retro_atomic_int_init(&pool.idle[w], 0);
|
|
}
|
|
|
|
// Start the threads only once every context and pool is complete.
|
|
for (TaskThreadContext *thread : global_->threads_) {
|
|
thread->thread = sthread_create(&WorkerThreadFunc, thread);
|
|
_assert_msg_(thread->thread, "ThreadManager: failed to start worker %d", thread->index);
|
|
}
|
|
}
|
|
|
|
static void DedicatedThreadFunc(void *arg) {
|
|
DedicatedStart *start = (DedicatedStart *)arg;
|
|
Task *task = start->task;
|
|
|
|
SetCurrentThreadName("DedicatedThreadTask");
|
|
task->Run();
|
|
task->Release();
|
|
}
|
|
|
|
void ThreadManager::EnqueueTask(Task *task) {
|
|
if (task->Type() == TaskType::DEDICATED_THREAD) {
|
|
DedicatedStart *start = new DedicatedStart{ {}, task, nullptr };
|
|
sthread_t *th = sthread_create(&DedicatedThreadFunc, start);
|
|
if (th) {
|
|
start->thread = th;
|
|
mpsc_stack_push(&global_->dedicatedThreads, &start->node);
|
|
} else {
|
|
// Couldn't start a thread; run it here rather than lose it.
|
|
DedicatedThreadFunc(start);
|
|
delete start;
|
|
}
|
|
return;
|
|
}
|
|
|
|
_assert_msg_(IsInitialized(), "ThreadManager not initialized");
|
|
|
|
const int queueIndex = (int)task->Priority();
|
|
TaskPool &pool = PoolFor(global_, task->Type());
|
|
_assert_msg_(!pool.workers.empty(), "ThreadManager: no threads available for task type %d (was Init() called with 0 compute threads?)", (int)task->Type());
|
|
|
|
if (!pool.ring[queueIndex].Push(task)) {
|
|
// Ring full - never block the producer, spill into the overflow stack instead.
|
|
mpsc_stack_push(&pool.overflow[queueIndex], &task->link_.node);
|
|
}
|
|
WakeOneIdle(pool);
|
|
}
|
|
|
|
void ThreadManager::EnqueueTaskOnThread(int threadNum, Task *task) {
|
|
_assert_msg_(task->Type() != TaskType::DEDICATED_THREAD, "Dedicated thread tasks can't be put on specific threads");
|
|
|
|
_assert_msg_(threadNum >= 0 && threadNum < (int)global_->threads_.size(), "Bad threadnum %d(/%d) or not initialized", threadNum, (int)global_->threads_.size());
|
|
TaskThreadContext *thread = global_->threads_[threadNum];
|
|
mpsc_stack_push(&thread->inbox[(int)task->Priority()], &task->link_.node);
|
|
retro_eventcount_notify(&thread->wake);
|
|
}
|
|
|
|
int ThreadManager::GetNumLooperThreads() const {
|
|
return numComputeThreads_;
|
|
}
|
|
|
|
bool ThreadManager::IsInitialized() const {
|
|
return !global_->threads_.empty();
|
|
}
|