BluFedora Job System v1.0.0
This is a C++ job system library for use in game engines.
job_system.cpp
Go to the documentation of this file.
1/******************************************************************************/
18/******************************************************************************/
20
22
23#include "pcg_basic.h" /* pcg_state_setseq_64, pcg32_srandom_r, pcg32_boundedrand_r */
24
25#include <algorithm> /* partition, for_each, distance */
26#include <cstdio> /* fprintf, stderr */
27#include <cstdlib> /* abort */
28#include <limits> /* numeric_limits */
29#include <new> /* hardware_constructive_interference_size, hardware_destructive_interference_size */
30#include <thread> /* thread */
31
32#if _WIN32
33#define IS_WINDOWS 1
34#define IS_POSIX 0
35#define IS_SINGLE_THREADED 0
36#elif __APPLE__
37#define IS_WINDOWS 0
38#define IS_POSIX 1
39#define IS_SINGLE_THREADED 0
40#elif (__ANDROID__ || __linux || __unix || __posix)
41#define IS_WINDOWS 0
42#define IS_POSIX 1
43#define IS_SINGLE_THREADED 0
44#elif __EMSCRIPTEN__
45#define IS_WINDOWS 0
46#define IS_POSIX 0
47#define IS_SINGLE_THREADED 1
48#endif
49
50#if IS_WINDOWS
51
52#define WIN32_LEAN_AND_MEAN
53#define NOMINMAX
54#define VC_EXTRALEAN
55#define WINDOWS_EXTRA_LEAN
56
57#include <Windows.h> /* SYSTEM_INFO, GetSystemInfo */
58#elif IS_POSIX
59#include <unistd.h> // also macOS 10.5+
60#else // macOS <= 10.4
61// #include <sys/param.h>
62// #include <sys/sysctl.h>
63#endif
64
65namespace job
66{
67 // Constants
68
69#ifdef __cpp_lib_hardware_interference_size
70 static constexpr std::size_t k_CachelineSize = std::max(std::hardware_constructive_interference_size, std::hardware_destructive_interference_size);
71#else
72 static constexpr std::size_t k_CachelineSize = 64u;
73#endif
74
75 static constexpr std::size_t k_ExpectedTaskSize = std::max(std::size_t(128u), k_CachelineSize);
76
77 // Type Aliases
78
79 using TaskHandle = std::uint16_t;
81 using AtomicTaskHandleType = std::atomic<TaskHandle>;
83 using AtomicInt32 = std::atomic_int32_t;
84 using Byte = unsigned char;
85
86 static constexpr TaskHandle NullTaskHandle = std::numeric_limits<TaskHandle>::max();
87
88 // Struct Definitions
89
90 struct TaskPtr
91 {
94
95 job::TaskPtr() noexcept = default;
96
97 job::TaskPtr(WorkerID worker_id, TaskHandle task_idx) noexcept :
99 task_index{task_idx}
100 {
101 }
102
103 job::TaskPtr(std::nullptr_t) noexcept :
106 {
107 }
108
109 bool isNull() const noexcept { return task_index == NullTaskHandle; }
110 };
111
112 using AtomicTaskPtr = std::atomic<job::TaskPtr>;
113
114 static_assert(sizeof(job::TaskPtr) == sizeof(std::uint16_t) * 2u, "Expected to be the size of two uint16's.");
115 static_assert(sizeof(AtomicTaskPtr) == sizeof(job::TaskPtr) && AtomicTaskPtr::is_always_lock_free, "Expected to be lock-free so no extra data members should have been added.");
116
117 struct alignas(k_CachelineSize) Task
118 {
119 static constexpr std::size_t k_SizeOfMembers =
120 sizeof(const char*) +
121 sizeof(internal::JobFn) +
122 sizeof(Counter*) +
123 sizeof(std::uint8_t) +
124 sizeof(std::atomic_bool);
125
126 static constexpr std::size_t k_TaskPaddingDataSize = k_ExpectedTaskSize - k_SizeOfMembers;
127
128 const char* name;
131 std::uint8_t userdata_align;
132 std::atomic_bool is_ready_for_gc;
133 Byte user_data[k_TaskPaddingDataSize];
134
135 Task(const char* const name, const job::internal::JobFn job_fn, job::Counter* const counter) noexcept :
136 name{name},
137 job_fn{job_fn},
138 counter{counter},
139 is_ready_for_gc{false},
140 userdata_align{0},
141 user_data{}
142 {
143 counter->unfinished_tasks.fetch_add(1u, std::memory_order_release);
144 }
145 };
146
147 static_assert(sizeof(Task) == k_ExpectedTaskSize, "The task struct is expected to be this size.");
148 static_assert(std::is_trivially_destructible_v<Task>, "Task must be trivially destructible.");
149
150 union alignas(Task) TaskMemoryBlock
151 {
153 unsigned char storage[sizeof(Task)];
154 };
155 static_assert(sizeof(TaskMemoryBlock) == sizeof(Task) && alignof(TaskMemoryBlock) == alignof(Task), "TaskMemoryBlock should have no overhead.");
156
157 struct TaskPool
158 {
161 };
162
164 {
171 pcg_state_setseq_64 rng_state;
172 std::thread thread_id;
173 };
174
176 {
177 std::mutex init_mutex = {};
178 std::condition_variable init_cv = {};
179 std::atomic_uint32_t num_workers_ready = {};
180 };
181
183 {
184 // State that wont be changing during the system's runtime.
185
187 std::uint32_t num_workers;
188 std::atomic_uint32_t num_user_threads_setup;
189 std::uint32_t num_tasks_per_worker;
191 std::atomic_bool is_running;
193 const char* sys_arch_str;
194 std::size_t system_alloc_size;
196
197 // Shared Mutable State
198
200 std::condition_variable worker_sleep_cv;
201 std::atomic_size_t num_available_jobs;
202 };
203} // namespace Job
204
205// System Globals
206
208static thread_local job::ThreadLocalState* g_CurrentWorker = nullptr;
209
210// Internal API
211
213{
214 is_ready_for_gc->store(true, std::memory_order_release);
215}
216
217#if JOB_SYS_ASSERTIONS
218
219void job::internal::AssertHandler(const bool condition, const char* const filename, const int line_number, const char* const msg)
220{
221 if (!condition)
222 {
223 std::fprintf(stderr, "JobSystem [%s:%i] Assertion '%s' Failed.\n", filename, line_number, msg);
224 std::abort();
225 }
226}
227
228#endif
229
230namespace
231{
232 namespace system
233 {
234 static void WakeUpAllWorkers() noexcept
235 {
236 g_JobSystem->worker_sleep_cv.notify_all();
237 }
238
239 static void WakeUpOneWorker() noexcept
240 {
241 g_JobSystem->worker_sleep_cv.notify_one();
242 }
243
244 static void Sleep() noexcept
245 {
246 job::JobSystemContext* const job_system = g_JobSystem;
247
248 if (job_system->is_running.load(std::memory_order_relaxed))
249 {
251
252 if (job_system->num_available_jobs.load(std::memory_order_relaxed) == 0u)
253 {
254 std::unique_lock<std::mutex> lock(job_system->worker_sleep_mutex);
255 job_system->worker_sleep_cv.wait(lock, [job_system]() {
256 // NOTE(SR):
257 // Because the stl wants 'false' to mean continue waiting the logic is a bit confusing :/
258 //
259 // Returns false if the waiting should be continued, aka num_queued_jobs == 0u (also return true if not running).
260 //
261 // Wait If: running AND num_available_jobs == 0.
262 // Do Not Wait If: not running OR num_available_jobs != 0.
263 //
264 return !job_system->is_running || job_system->num_available_jobs.load(std::memory_order_relaxed) != 0; });
265 }
266 }
267 }
268
269 static job::ThreadLocalState* GetWorker(const job::WorkerID worker_id) noexcept
270 {
271 JobAssert(worker_id < job::NumWorkers(), "This thread was not created by the job system.");
272 return g_JobSystem->workers + worker_id;
273 }
274
275 } // namespace system
276
277 namespace task_pool
278 {
279 static void Initialize(job::TaskPool* const pool, job::TaskMemoryBlock* const memory, const job::TaskHandleType capacity) noexcept
280 {
281 const job::TaskHandleType capacity_minus_one = capacity - 1;
282
283 for (std::size_t i = 0u; i < capacity_minus_one; ++i)
284 {
285 memory[i].next = &memory[i + 1u];
286 }
287 memory[capacity_minus_one].next = nullptr;
288
289 pool->memory = memory;
290 pool->freelist = &memory[0];
291 }
292
293 static job::TaskHandle TaskToIndex(const job::TaskPool& pool, const job::Task* const task) noexcept
294 {
295 const job::TaskMemoryBlock* const block = reinterpret_cast<const job::TaskMemoryBlock*>(task);
296
297 return job::TaskHandle(block - pool.memory);
298 }
299
300 static job::Task* TaskFromIndex(const job::TaskPool& pool, const std::size_t idx) noexcept
301 {
302 return reinterpret_cast<job::Task*>(&pool.memory[idx]);
303 }
304
305 static job::Task* AllocateTask(job::TaskPool* const pool, const char* const name, const job::internal::JobFn job_fn, job::Counter* const counter) noexcept
306 {
307 job::TaskMemoryBlock* const result = std::exchange(pool->freelist, pool->freelist->next);
308
309 JobAssert(result != nullptr, "Allocation failure.");
310
311 return new (result) job::Task(name, job_fn, counter);
312 }
313
314 static void DeallocateTask(job::TaskPool* const pool, job::Task* const task) noexcept
315 {
316 task->~Task();
317
318 job::TaskMemoryBlock* const block = new (task) job::TaskMemoryBlock();
319
320 block->next = std::exchange(pool->freelist, block);
321 }
322 } // namespace task_pool
323
324 namespace task
325 {
326 static job::Task* TaskPtrToPointer(const job::TaskPtr ptr) noexcept
327 {
328 if (!ptr.isNull())
329 {
330 job::ThreadLocalState* const worker = system::GetWorker(ptr.worker_id);
331 job::Task* const result = task_pool::TaskFromIndex(worker->task_allocator, ptr.task_index);
332
333 return result;
334 }
335 else
336 {
337 return nullptr;
338 }
339 }
340
341 static void RunTaskFunction(job::Task* const self, const job::WorkerID worker_id) noexcept
342 {
343 const job::internal::PrivateCtx ctx{self->counter, worker_id, self->name, 0, self->user_data + self->userdata_align, &self->is_ready_for_gc};
344
345 (self->job_fn)(ctx);
346
347 if (ctx.task_counter != nullptr)
348 {
349 ctx.task_counter->unfinished_tasks.fetch_sub(1, std::memory_order_release);
350 }
351 }
352 } // namespace task
353
354 namespace worker
355 {
356 static job::WorkerID GetCurrentID() noexcept
357 {
358 JobAssert(g_CurrentWorker != nullptr, "This thread was not created by the job system.");
360 }
361
362 static void GarbageCollectAllocatedTasks(job::ThreadLocalState* const worker) noexcept
363 {
364 job::TaskHandle* const allocated_tasks = worker->allocated_tasks;
365 job::TaskPool& task_pool = worker->task_allocator;
366
367#if 0
368 const job::TaskHandleType num_tasks = worker->num_allocated_tasks;
369 job::TaskHandleType read_idx = 0u;
370 job::TaskHandleType write_idx = 0u;
371
372 while (read_idx != num_tasks)
373 {
374 const job::TaskHandle task_handle = allocated_tasks[read_idx++];
375 job::Task* const task_ptr = task_pool::TaskFromIndex(task_pool, task_handle);
376 const bool task_is_finished = task_ptr->is_ready_for_gc.load(std::memory_order_acquire);
377
378 if (task_is_finished)
379 {
380 task_pool::DeallocateTask(&task_pool, task_ptr);
381 }
382 else
383 {
384 allocated_tasks[write_idx++] = task_handle;
385 }
386 }
387
388 worker->num_allocated_tasks = write_idx;
389#else
390 constexpr job::TaskHandleType max_tasks_to_gc = 512u;
391
392 job::TaskHandleType num_tasks = worker->num_allocated_tasks;
393 job::TaskHandleType read_idx = 0u;
394 job::TaskHandleType num_gc = 0u;
395
396 while (read_idx < num_tasks && num_gc < max_tasks_to_gc)
397 {
398 const job::TaskHandle task_handle = allocated_tasks[read_idx];
399 job::Task* const task_ptr = task_pool::TaskFromIndex(task_pool, task_handle);
400 const bool task_is_finished = task_ptr->is_ready_for_gc.load(std::memory_order_acquire);
401
402 if (task_is_finished)
403 {
404 allocated_tasks[read_idx] = allocated_tasks[--num_tasks];
405
406 task_pool::DeallocateTask(&task_pool, task_ptr);
407 ++num_gc;
408 }
409 else
410 {
411 ++read_idx;
412 }
413 }
414
415 worker->num_allocated_tasks = num_tasks;
416#endif
417 }
418
419 static job::ThreadLocalState* RandomWorker(job::ThreadLocalState* const worker) noexcept
420 {
421 const std::uint32_t num_workers = g_JobSystem->num_workers;
422 const std::uint32_t other_worker_id = pcg32_boundedrand_r(&worker->rng_state, num_workers);
423
424 return system::GetWorker(job::WorkerID(other_worker_id));
425 }
426
427 static bool IsMainThread(const job::ThreadLocalState* const worker) noexcept
428 {
429 return worker == g_JobSystem->workers;
430 }
431
432 static bool TryRunTask(job::ThreadLocalState* const worker) noexcept
433 {
434 const bool is_main_thread = IsMainThread(worker);
435
436 job::TaskPtr task_ptr = nullptr;
437 worker->normal_queue.Pop(&task_ptr);
438
439 if (task_ptr.isNull() && !is_main_thread)
440 {
441 worker->worker_queue.Pop(&task_ptr);
442 }
443
444 const auto TrySteal = [is_main_thread, worker](job::ThreadLocalState* const other_worker) -> job::TaskPtr {
445 job::TaskPtr result = nullptr;
446
447 if (other_worker != worker)
448 {
449 other_worker->normal_queue.Steal(&result);
450
451 if (result.isNull() && !is_main_thread)
452 {
453 other_worker->worker_queue.Steal(&result);
454 }
455 }
456
457 return result;
458 };
459
460 if (task_ptr.isNull())
461 {
462 task_ptr = TrySteal(worker->last_stolen_worker);
463 }
464
465 if (task_ptr.isNull())
466 {
467 job::ThreadLocalState* const random_worker = RandomWorker(worker);
468
469 task_ptr = TrySteal(random_worker);
470
471 if (task_ptr.isNull())
472 {
473 return false;
474 }
475
476 worker->last_stolen_worker = random_worker;
477 }
478
479 g_JobSystem->num_available_jobs.fetch_sub(1, std::memory_order_relaxed);
480
481 job::Task* const task = task::TaskPtrToPointer(task_ptr);
482 task::RunTaskFunction(task, worker::GetCurrentID());
483
484 return true;
485 }
486
487 static void WaitForAllThreadsReady(job::JobSystemContext* const job_system) noexcept
488 {
489 job::InitializationLock* const init_lock = &job_system->init_lock;
490
491 if ((init_lock->num_workers_ready.fetch_add(1u, std::memory_order_relaxed) + 1) == g_JobSystem->num_workers)
492 {
493 job_system->is_running.store(true, std::memory_order_relaxed);
494 init_lock->init_cv.notify_all();
495 }
496 else
497 {
498 std::unique_lock<std::mutex> lock(init_lock->init_mutex);
499 init_lock->init_cv.wait(lock, [init_lock]() -> bool {
500 return init_lock->num_workers_ready.load(std::memory_order_relaxed) == g_JobSystem->num_workers;
501 });
502 }
503 }
504
505 static job::JobSystemContext* WorkerThreadSetup(job::ThreadLocalState* const worker)
506 {
507 std::atomic_thread_fence(std::memory_order_acquire);
508
509 job::JobSystemContext* const job_system = g_JobSystem;
510
511#if IS_WINDOWS
512 const HANDLE handle = GetCurrentThread();
513
514#if 0
515 // Put each thread on to dedicated core
516
517 const DWORD_PTR affinity_mask = 1ull << thread_id;
518 const DWORD_PTR affinity_result = SetThreadAffinityMask(handle, affinity_mask);
519
520 if (affinity_result > 0)
521 {
522 // Increase thread priority
523
524 // const BOOL priority_result = SetThreadPriority(handle, THREAD_PRIORITY_HIGHEST);
525 // JobAssert(priority_result != 0, "Failed to set thread priority.");
526 }
527#endif
528 // Name the thread
529
530 const unsigned int thread_index = unsigned int(worker - job_system->workers);
531
532 char thread_name[32] = u8"";
533 wchar_t thread_name_w[sizeof(thread_name)] = L"";
534
535 const int c_size = std::snprintf(thread_name, sizeof(thread_name), "job::Worker%u", thread_index);
536
537 std::mbstowcs(thread_name_w, thread_name, c_size);
538
539 const HRESULT hr = SetThreadDescription(handle, thread_name_w);
540 JobAssert(SUCCEEDED(hr), "Failed to set thread name.");
541 (void)hr;
542#endif
543
545
546 WaitForAllThreadsReady(job_system);
547
548 return job_system;
549 }
550
551 static void InitializeThread(job::ThreadLocalState* const worker) noexcept
552 {
553 worker->thread_id = std::thread([worker]() {
554 job::JobSystemContext* const job_system = WorkerThreadSetup(worker);
555
556 while (job_system->is_running.load(std::memory_order_relaxed))
557 {
558 if (!worker::TryRunTask(worker))
559 {
560 system::Sleep();
561 }
562 }
563 });
564 }
565
566 static void ShutdownThread(job::ThreadLocalState* const worker) noexcept
567 {
568 // Join throws an exception if the thread is not joinable. this should always be true.
569 worker->thread_id.join();
570 }
571 } // namespace worker
572
573 namespace task
574 {
575 static void SubmitQPushHelper(const job::TaskPtr task_ptr, job::ThreadLocalState* const worker, job::SPMCDeque<job::TaskPtr>* queue) noexcept
576 {
577 if (queue->Push(task_ptr) != job::SPMCDequeStatus::SUCCESS)
578 {
579 // Loop until we have successfully pushed to the queue.
580 system::WakeUpAllWorkers();
581 while (queue->Push(task_ptr) != job::SPMCDequeStatus::SUCCESS)
582 {
583 // If we could not push to the queues then just do some work.
584 worker::TryRunTask(worker);
585 }
586 }
587 }
588 } // namespace task
589
590 static bool IsPointerAligned(const void* const ptr, const std::size_t alignment) noexcept
591 {
592 return (reinterpret_cast<std::uintptr_t>(ptr) & (alignment - 1u)) == 0u;
593 }
594
595 static void* AlignPointer(const void* const ptr, const std::size_t alignment) noexcept
596 {
597 const std::size_t required_alignment_mask = alignment - 1;
598
599 return reinterpret_cast<void*>(reinterpret_cast<std::uintptr_t>(ptr) + required_alignment_mask & ~required_alignment_mask);
600 }
601
602 template<typename T>
603 struct Span
604 {
605 T* ptr;
606 std::size_t num_elements;
607 };
608
609 template<typename T>
610 static Span<T> LinearAlloc(void*& ptr, const std::size_t num_elements) noexcept
611 {
612 void* const result = AlignPointer(ptr, alignof(T));
613
614 ptr = static_cast<unsigned char*>(result) + sizeof(T) * num_elements;
615
616 for (std::size_t i = 0; i < num_elements; ++i)
617 {
618 new (static_cast<T*>(result) + i) T;
619 }
620
621 return Span<T>{static_cast<T*>(result), num_elements};
622 }
623
624 template<typename T>
625 static T* SpanAlloc(Span<T>* const span, const std::size_t num_elements) noexcept
626 {
627 JobAssert(num_elements <= span->num_elements, "Out of bounds span alloc.");
628
629 T* const result = span->ptr;
630
631 span->ptr += num_elements;
632 span->num_elements -= num_elements;
633
634 return result;
635 }
636
637 static std::size_t AlignedSizeUp(const std::size_t size, const std::size_t alignment) noexcept
638 {
639 const std::size_t remainder = size % alignment;
640
641 return remainder != 0 ? size + (alignment - remainder) : size;
642 }
643
644 template<typename T>
645 static void MemoryRequirementsPush(job::JobSystemMemoryRequirements* in_out_reqs, const std::size_t num_elements) noexcept
646 {
647 in_out_reqs->byte_size = AlignedSizeUp(in_out_reqs->byte_size, alignof(T));
648 in_out_reqs->alignment = in_out_reqs->alignment < alignof(T) ? alignof(T) : in_out_reqs->alignment;
649
650 in_out_reqs->byte_size += sizeof(T) * num_elements;
651 }
652
653 static bool IsPowerOf2(const std::size_t value) noexcept
654 {
655 return (value & (value - 1)) == 0;
656 }
657
658 namespace config
659 {
660 static job::WorkerID WorkerCount(const job::JobSystemCreateOptions& options) noexcept
661 {
662 return (options.num_threads ? options.num_threads : job::WorkerID(job::NumSystemThreads()));
663 }
664
665 static std::uint16_t NumTasksPerWorker(const job::JobSystemCreateOptions& options) noexcept
666 {
667 const std::size_t num_tasks_per_worker = std::size_t(options.normal_queue_size) + std::size_t(options.worker_queue_size);
668
669 JobAssert(num_tasks_per_worker <= std::uint16_t(-1), "Too many task items per worker.");
670
671 return std::uint16_t(num_tasks_per_worker);
672 }
673
674 static std::uint32_t TotalNumTasks(const job::WorkerID num_threads, const std::uint16_t num_tasks_per_worker) noexcept
675 {
676 return num_tasks_per_worker * num_threads;
677 }
678 } // namespace config
679
680} // namespace
681
682// Public API
683
685 options{options},
686 byte_size{0},
687 alignment{0}
688{
689 JobAssert(IsPowerOf2(options.normal_queue_size), "Normal queue size must be a power of two.");
690 JobAssert(IsPowerOf2(options.worker_queue_size), "Worker queue size must be a power of two.");
691
692 const WorkerID num_threads = config::WorkerCount(options);
693 const std::uint16_t num_tasks_per_worker = config::NumTasksPerWorker(options);
694 const std::uint32_t total_num_tasks = config::TotalNumTasks(num_threads, num_tasks_per_worker);
695
696 MemoryRequirementsPush<JobSystemContext>(this, 1u);
697 MemoryRequirementsPush<job::ThreadLocalState>(this, num_threads);
698 MemoryRequirementsPush<TaskMemoryBlock>(this, total_num_tasks);
699 MemoryRequirementsPush<AtomicTaskPtr>(this, total_num_tasks);
700 MemoryRequirementsPush<TaskHandle>(this, total_num_tasks);
701}
702
703void job::Initialize(const job::JobSystemMemoryRequirements& memory_requirements, void* memory) noexcept
704{
705 JobAssert(g_JobSystem == nullptr, "Already initialized.");
706
707 const bool needs_delete = memory == nullptr;
708
709 if (!memory)
710 {
711 memory = ::operator new[](memory_requirements.byte_size, std::align_val_t{memory_requirements.alignment});
712 }
713
714 JobAssert(memory != nullptr, "memory must be a valid pointer.");
715 JobAssert(IsPointerAligned(memory, memory_requirements.alignment), "memory must be a aligned to `memory_requirements.alignment`.");
716
717 const JobSystemCreateOptions& options = memory_requirements.options;
718 const std::uint64_t rng_seed = options.job_steal_rng_seed;
719 const WorkerID num_threads = config::WorkerCount(options);
720 const std::uint16_t num_tasks_per_worker = config::NumTasksPerWorker(options);
721 const std::uint32_t total_num_tasks = config::TotalNumTasks(num_threads, num_tasks_per_worker);
722
723 void* alloc_ptr = memory;
724 JobSystemContext* job_system = LinearAlloc<JobSystemContext>(alloc_ptr, 1u).ptr;
725 Span<job::ThreadLocalState> all_workers = LinearAlloc<job::ThreadLocalState>(alloc_ptr, num_threads);
726 Span<TaskMemoryBlock> all_tasks = LinearAlloc<TaskMemoryBlock>(alloc_ptr, total_num_tasks);
727 Span<AtomicTaskPtr> worker_task_ptrs = LinearAlloc<AtomicTaskPtr>(alloc_ptr, total_num_tasks);
728 Span<TaskHandle> all_task_handles = LinearAlloc<TaskHandle>(alloc_ptr, total_num_tasks);
729
730 job_system->workers = all_workers.ptr;
731 job_system->num_workers = num_threads;
732 job_system->num_user_threads_setup.store(0, std::memory_order_relaxed);
733 job_system->num_tasks_per_worker = num_tasks_per_worker;
734 job_system->sys_arch_str = "Unknown Arch";
735 job_system->num_available_jobs.store(0, std::memory_order_relaxed);
736 job_system->needs_delete = needs_delete;
737 job_system->system_alloc_size = memory_requirements.byte_size;
738 job_system->system_alloc_alignment = memory_requirements.alignment;
739 job_system->init_lock.num_workers_ready.store(1u, std::memory_order_relaxed); // Main thread already initialized.
740
741#if IS_WINDOWS
742 SYSTEM_INFO sysinfo;
743 GetSystemInfo(&sysinfo);
744
745 switch (sysinfo.wProcessorArchitecture)
746 {
747 case PROCESSOR_ARCHITECTURE_AMD64:
748 {
749 job_system->sys_arch_str = "x64 (Intel or AMD)";
750 break;
751 }
752 case PROCESSOR_ARCHITECTURE_ARM:
753 {
754 job_system->sys_arch_str = "ARM";
755 break;
756 }
757 case PROCESSOR_ARCHITECTURE_ARM64:
758 {
759 job_system->sys_arch_str = "ARM64";
760 break;
761 }
762 case PROCESSOR_ARCHITECTURE_IA64:
763 {
764 job_system->sys_arch_str = "Intel Itanium-Based";
765 break;
766 }
767 case PROCESSOR_ARCHITECTURE_INTEL:
768 {
769 job_system->sys_arch_str = "Intel x86";
770 break;
771 }
772 case PROCESSOR_ARCHITECTURE_UNKNOWN:
773 default:
774 {
775 job_system->sys_arch_str = "Unknown Arch";
776 break;
777 }
778 }
779#endif
780
781 job::ThreadLocalState* const main_thread_worker = job_system->workers;
782
783 for (std::uint64_t worker_index = 0; worker_index < num_threads; ++worker_index)
784 {
785 job::ThreadLocalState* const worker = SpanAlloc(&all_workers, 1u);
786
787 worker->normal_queue.Initialize(SpanAlloc(&worker_task_ptrs, options.normal_queue_size), options.normal_queue_size);
788 worker->worker_queue.Initialize(SpanAlloc(&worker_task_ptrs, options.worker_queue_size), options.worker_queue_size);
789 task_pool::Initialize(&worker->task_allocator, SpanAlloc(&all_tasks, num_tasks_per_worker), num_tasks_per_worker);
790 worker->allocated_tasks = SpanAlloc(&all_task_handles, num_tasks_per_worker);
791 worker->num_allocated_tasks = 0u;
792 pcg32_srandom_r(&worker->rng_state, worker_index + rng_seed, worker_index * 2u + 1u + rng_seed);
793 worker->last_stolen_worker = main_thread_worker;
794 }
795
796 g_JobSystem = job_system;
797 g_CurrentWorker = main_thread_worker;
798
799 std::atomic_thread_fence(std::memory_order_release);
800 for (std::uint64_t worker_index = 1; worker_index < num_threads; ++worker_index)
801 {
802 worker::InitializeThread(job_system->workers + worker_index);
803 }
804
805 JobAssert(all_workers.num_elements == 0u, "All elements expected to be allocated out.");
806 JobAssert(all_tasks.num_elements == 0u, "All elements expected to be allocated out.");
807 JobAssert(worker_task_ptrs.num_elements == 0u, "All elements expected to be allocated out.");
808 JobAssert(all_task_handles.num_elements == 0u, "All elements expected to be allocated out.");
809}
810
811std::size_t job::NumSystemThreads() noexcept
812{
813#if IS_SINGLE_THREADED
814 return 1;
815#else
816 const auto n = std::thread::hardware_concurrency();
817 return n != 0 ? n : 1;
818#endif
819
820#if 0
821
822#if IS_WINDOWS
823 SYSTEM_INFO sysinfo;
824 GetSystemInfo(&sysinfo);
825 return sysinfo.dwNumberOfProcessors;
826#elif IS_POSIX
827 return sysconf(_SC_NPROCESSORS_ONLN) /* * 2*/;
828#elif 0 // FreeBSD, MacOS X, NetBSD, OpenBSD
829 nt mib[4];
830 int numCPU;
831 std::size_t len = sizeof(numCPU);
832
833 /* set the mib for hw.ncpu */
834 mib[0] = CTL_HW;
835 mib[1] = HW_AVAILCPU; // alternatively, try HW_NCPU;
836
837 /* get the number of CPUs from the system */
838 sysctl(mib, 2, &numCPU, &len, NULL, 0);
839
840 if (numCPU < 1)
841 {
842 mib[1] = HW_NCPU;
843 sysctl(mib, 2, &numCPU, &len, NULL, 0);
844 if (numCPU < 1)
845 numCPU = 1;
846 }
847
848 return numCPU;
849#elif 0 // HPUX
850 return mpctl(MPC_GETNUMSPUS, NULL, NULL);
851#elif 0 // IRIX
852 return sysconf(_SC_NPROC_ONLN);
853#elif 0 // Objective-C (Mac OS X >=10.5 or iOS)
854 NSUInteger a = [[NSProcessInfo processInfo] processorCount];
855 NSUInteger b = [[NSProcessInfo processInfo] activeProcessorCount];
856
857 return a;
858#endif
859
860#endif
861}
862
863std::uint16_t job::NumWorkers() noexcept
864{
865 return std::uint16_t(g_JobSystem->num_workers);
866}
867
868const char* job::ProcessorArchitectureName() noexcept
869{
871}
872
874{
875 JobAssert(g_CurrentWorker != nullptr, "This thread was not created by the job system.");
877}
878
879bool job::IsMainThread() noexcept
880{
882}
883
884void job::Shutdown() noexcept
885{
886 JobAssert(g_JobSystem != nullptr, "Cannot shutdown when never initialized.");
887
888 static_assert(std::is_trivially_destructible_v<TaskMemoryBlock>, "TaskMemoryBlock's destructor not called.");
889 static_assert(std::is_trivially_destructible_v<job::TaskPtr>, "job::TaskPtr's destructor not called.");
890 static_assert(std::is_trivially_destructible_v<AtomicTaskPtr>, "AtomicTaskPtr's destructor not called.");
891 static_assert(std::is_trivially_destructible_v<TaskHandle>, "TaskHandle's destructor not called.");
892
893 JobSystemContext* const job_system = g_JobSystem;
894 const std::uint32_t num_workers = job_system->num_workers;
895
896 // Incase all threads are not initialized by the time shutdown is called.
897 while (job_system->is_running.load(std::memory_order_relaxed) != true) {}
898
899 {
900 std::unique_lock<std::mutex> lock(job_system->worker_sleep_mutex);
901 job_system->is_running.store(false, std::memory_order_relaxed);
902 }
903
904 // Allow one last update loop to allow them to end.
905 system::WakeUpAllWorkers();
906
907 for (std::uint32_t i = 0; i < num_workers; ++i)
908 {
909 job::ThreadLocalState* const worker = job_system->workers + i;
910
911 if (i != 0)
912 {
913 worker::ShutdownThread(worker);
914 }
915
916 worker->~ThreadLocalState();
917 }
918
919 const bool needs_delete = job_system->needs_delete;
920
921 job_system->~JobSystemContext();
922 g_CurrentWorker = nullptr;
923 g_JobSystem = nullptr;
924
925 if (needs_delete)
926 {
927 ::operator delete[](job_system, job_system->system_alloc_size, std::align_val_t{job_system->system_alloc_alignment});
928 }
929}
930
931void job::WaitOn(const Counter& counter) noexcept
932{
933 const WorkerID worker_id = CurrentWorker();
934
935 system::WakeUpAllWorkers();
936
937 job::ThreadLocalState* const worker = system::GetWorker(worker_id);
938
939 while (counter.unfinished_tasks.load(std::memory_order_acquire) != 0u)
940 {
941 worker::TryRunTask(worker);
942 }
943}
944
945// Member Fn Definitions
946
947void job::internal::DispatchImpl(const char* const name,
948 Counter* const counter,
949 const QueueMode queue,
950 const JobFn func,
951 const std::size_t user_data_size,
952 const std::size_t user_data_alignment,
953 const void* const user_data,
954 void (*InitUserData)(void* const user_data, const void* const in_user_data)) noexcept
955{
956 const WorkerID worker_id = worker::GetCurrentID();
957 job::ThreadLocalState* const worker = system::GetWorker(worker_id);
958 const std::uint32_t max_tasks_per_worker = g_JobSystem->num_tasks_per_worker;
959
960 // Try to ensure some tasks are free to allocate.
961 {
962 if (worker->num_allocated_tasks == max_tasks_per_worker)
963 {
964 worker::GarbageCollectAllocatedTasks(worker);
965
966 if (worker->num_allocated_tasks == max_tasks_per_worker)
967 {
968 // While we cannot allocate do some work.
969 system::WakeUpAllWorkers();
970
971 while (worker->num_allocated_tasks == max_tasks_per_worker)
972 {
973 worker::TryRunTask(worker);
974 worker::GarbageCollectAllocatedTasks(worker);
975 }
976 }
977 }
978 }
979
980 Task* const task = task_pool::AllocateTask(&worker->task_allocator, name, func, counter);
981 const TaskHandle task_hdl = task_pool::TaskToIndex(worker->task_allocator, task);
982 const job::TaskPtr task_ptr = {worker_id, task_pool::TaskToIndex(worker->task_allocator, task)};
983
984 // Copy user data
985 {
986 const Byte* const user_data_end = task->user_data + sizeof(task->user_data);
987 Byte* const aligned_ptr = static_cast<Byte*>(AlignPointer(task->user_data, user_data_alignment));
988 const Byte* const aligned_ptr_end = aligned_ptr + user_data_size;
989 const std::ptrdiff_t alignment_offset = aligned_ptr - task->user_data;
990
991 JobAssert(aligned_ptr_end <= user_data_end, "Userdata could not be stored in task.");
992 JobAssert(alignment_offset <= std::uint8_t(-1), "Alignment delta too large.");
993
994 InitUserData(aligned_ptr, user_data);
995 task->userdata_align = static_cast<std::uint8_t>(alignment_offset);
996 }
997
998 worker->allocated_tasks[worker->num_allocated_tasks++] = task_hdl;
999
1000 const WorkerID num_workers = NumWorkers();
1001
1002 // If we only have one thread running using the worker queue is invalid.
1003 switch ((num_workers == 1u) ? QueueMode::Default : queue)
1004 {
1005 case QueueMode::Default:
1006 {
1007 task::SubmitQPushHelper(task_ptr, worker, &worker->normal_queue);
1008 break;
1009 }
1011 {
1012 task::SubmitQPushHelper(task_ptr, worker, &worker->worker_queue);
1013 break;
1014 }
1015 default:
1016#if defined(__GNUC__) // GCC, Clang, ICC
1017 __builtin_unreachable();
1018#elif defined(_MSC_VER) // MSVC
1019 __assume(false);
1020#endif
1021 break;
1022 }
1023
1024 const std::size_t num_pending_jobs = g_JobSystem->num_available_jobs.fetch_add(1, std::memory_order_relaxed);
1025
1026 if (num_pending_jobs >= num_workers)
1027 {
1028 system::WakeUpAllWorkers();
1029 }
1030 else
1031 {
1032 system::WakeUpOneWorker();
1033 }
1034}
1035
1036#if defined(_MSC_VER)
1037#define NativePause YieldProcessor
1038#elif defined(__clang__) && defined(__SSE__) || defined(__INTEL_COMPILER) // || defined(__GNUC_PREREQ) && (__GNUC_PREREQ(4, 7) && defined(__SSE__))
1039#include <xmmintrin.h>
1040#define NativePause _mm_pause
1041#elif defined(__arm__)
1042#ifdef __CC_ARM
1043#define NativePause __yield
1044#else
1045#define NativePause() __asm__ __volatile__("yield")
1046#endif
1047#else
1048#define NativePause std::this_thread::yield
1049#endif
1050
1051void job::PauseProcessor() noexcept
1052{
1053 NativePause();
1054}
1055
1056#undef NativePause
1057
1058void job::YieldTimeSlice() noexcept
1059{
1060 // Windows : SwitchToThread()
1061 // Linux : sched_yield()
1062 std::this_thread::yield();
1063}
1064
1065#undef IS_WINDOWS
1066#undef IS_POSIX
1067#undef IS_SINGLE_THREADED
1068
1069#if defined(_MSC_VER)
1070
1071#pragma warning(push)
1072#pragma warning(disable : 4244)
1073#pragma warning(disable : 4146)
1074
1075#endif
1076
1077#include "pcg_basic.c"
1078
1079#if defined(_MSC_VER)
1080
1081#pragma warning(pop)
1082
1083#endif
1084
1085/******************************************************************************/
1086/*
1087 MIT License
1088
1089 Copyright (c) 2020-2026 Shareef Abdoul-Raheem
1090
1091 Permission is hereby granted, free of charge, to any person obtaining a copy
1092 of this software and associated documentation files (the "Software"), to deal
1093 in the Software without restriction, including without limitation the rights
1094 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1095 copies of the Software, and to permit persons to whom the Software is
1096 furnished to do so, subject to the following conditions:
1097
1098 The above copyright notice and this permission notice shall be included in all
1099 copies or substantial portions of the Software.
1100
1101 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1102 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1103 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1104 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1105 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1106 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1107 SOFTWARE.
1108*/
1109/******************************************************************************/
API for a multi-threading job system.
#define JobAssert(expr, msg)
Definition: job_api.hpp:32
Concurrent Queue Implmementations for different situations.
static thread_local job::ThreadLocalState * g_CurrentWorker
Definition: job_system.cpp:208
static job::JobSystemContext * g_JobSystem
Definition: job_system.cpp:207
#define NativePause
void(*)(const PrivateCtx &ctx) JobFn
Definition: job_api.hpp:235
void AssertHandler(const bool condition, const char *const filename, const int line_number, const char *const msg)
void DispatchImpl(const char *const name, Counter *const counter, const QueueMode queue, const JobFn func, const std::size_t user_data_size, const std::size_t user_data_alignment, const void *const user_data, void(*InitUserData)(void *const user_data, const void *const in_user_data)) noexcept
Definition: job_system.cpp:947
Definition: job_api.hpp:38
const char * sys_arch_str
Definition: job_system.cpp:193
std::atomic_bool is_running
Definition: job_system.cpp:191
void WaitOn(const Counter &counter) noexcept
Blocks until all tasks associated with counter are done while This function will block but do work wh...
Definition: job_system.cpp:931
std::mutex worker_sleep_mutex
Definition: job_system.cpp:199
void Shutdown() noexcept
This will deallocate any memory used by the system and shutdown any threads created by 'bfjob::initia...
Definition: job_system.cpp:884
std::atomic< job::TaskPtr > AtomicTaskPtr
Definition: job_system.cpp:112
std::condition_variable worker_sleep_cv
Definition: job_system.cpp:200
static constexpr std::size_t k_CachelineSize
Definition: job_system.cpp:72
InitializationLock init_lock
Definition: job_system.cpp:192
job::ThreadLocalState * workers
Definition: job_system.cpp:186
ThreadLocalState * last_stolen_worker
Definition: job_system.cpp:170
SPMCDeque< job::TaskPtr > worker_queue
Definition: job_system.cpp:166
bool IsMainThread() noexcept
Allows for querying if we are currently executing in the main thread.
Definition: job_system.cpp:879
std::uint64_t job_steal_rng_seed
The RNG for work queue stealing will be seeded with this value.
Definition: job_api.hpp:92
TaskHandle TaskHandleType
Definition: job_system.cpp:80
std::atomic< TaskHandle > AtomicTaskHandleType
Definition: job_system.cpp:81
SPMCDeque< job::TaskPtr > normal_queue
Definition: job_system.cpp:165
std::uint16_t worker_queue_size
Number of tasks in each worker's QueueType::WorkerOnly queue. (Must be power of two)
Definition: job_api.hpp:91
QueueMode
Determines which threads the task will be allowed to run on.
Definition: job_api.hpp:66
@ Default
Tasks in this queue will run on either the main or worker threads.
@ WorkerOnly
Tasks in this queue will never run on the main thread.
std::atomic_uint64_t unfinished_tasks
Definition: job_api.hpp:50
std::uint16_t WorkerID
The id type of each worker thread.
Definition: job_api.hpp:39
TaskMemoryBlock * memory
Definition: job_system.cpp:159
WorkerID CurrentWorker() noexcept
The current id of the current thread. This function can be called by any thread concurrently.
Definition: job_system.cpp:873
TaskMemoryBlock * next
Definition: job_system.cpp:152
TaskMemoryBlock * freelist
Definition: job_system.cpp:160
std::atomic_size_t num_available_jobs
Definition: job_system.cpp:201
const char * ProcessorArchitectureName() noexcept
An implementation defined name for the CPU architecture of the device. This function can be called by...
Definition: job_system.cpp:868
TaskHandleType num_allocated_tasks
Definition: job_system.cpp:169
std::uint32_t num_tasks_per_worker
Definition: job_system.cpp:189
std::uint16_t NumWorkers() noexcept
Returns the number of workers created by the system. This function can be called by any thread concur...
Definition: job_system.cpp:863
std::uint16_t num_threads
Use 0 to indicate using the number of cores available on the system.
Definition: job_api.hpp:89
std::atomic_uint32_t num_workers_ready
Definition: job_system.cpp:179
std::atomic_int32_t AtomicInt32
Definition: job_system.cpp:83
void PauseProcessor() noexcept
CPU pause instruction to indicate when you are in a spin wait loop.
std::size_t system_alloc_alignment
Definition: job_system.cpp:195
static constexpr TaskHandle NullTaskHandle
Definition: job_system.cpp:86
void Initialize(const JobSystemMemoryRequirements &memory_requirements={}, void *const memory=nullptr) noexcept
Sets up the Job system and creates all the worker threads. The thread that calls 'job::Initialize' is...
Definition: job_system.cpp:703
WorkerID WorkerIDType
Definition: job_system.cpp:82
unsigned char Byte
Definition: job_system.cpp:84
pcg_state_setseq_64 rng_state
Definition: job_system.cpp:171
std::atomic_uint32_t num_user_threads_setup
Definition: job_system.cpp:188
TaskHandle * allocated_tasks
Definition: job_system.cpp:168
std::uint32_t num_workers
Definition: job_system.cpp:187
std::condition_variable init_cv
Definition: job_system.cpp:178
std::size_t system_alloc_size
Definition: job_system.cpp:194
static constexpr std::size_t k_ExpectedTaskSize
Definition: job_system.cpp:75
@ SUCCESS
Returned from Push, Pop and Steal.
std::uint16_t TaskHandle
Definition: job_system.cpp:79
std::uint16_t normal_queue_size
Number of tasks in each worker's QueueType::Default queue. (Must be power of two)
Definition: job_api.hpp:90
std::size_t NumSystemThreads() noexcept
Makes system calls to grab the number threads / processors on the device. This function can be called...
Definition: job_system.cpp:811
void YieldTimeSlice() noexcept
Asks the OS to yield this threads execution to another thread on the current cpu core.
The only syncronization mechanism. Allows you to wait on tasks you asssociated with this counter.
Definition: job_api.hpp:49
The runtime configuration for the Job System.
Definition: job_api.hpp:88
The memory requirements for a given configuration JobSystemCreateOptions.
Definition: job_api.hpp:100
JobSystemMemoryRequirements(const JobSystemCreateOptions &options={}) noexcept
Definition: job_system.cpp:684
std::uint8_t userdata_align
Alignment offset needed by userdata.
Definition: job_system.cpp:131
Task(const char *const name, const job::internal::JobFn job_fn, job::Counter *const counter) noexcept
Definition: job_system.cpp:135
Counter * counter
The counter to be decremented.
Definition: job_system.cpp:130
const char * name
Debug name of this task.
Definition: job_system.cpp:128
std::atomic_bool is_ready_for_gc
Set to true when the task can be reused.
Definition: job_system.cpp:132
internal::JobFn job_fn
The function that will be run.
Definition: job_system.cpp:129
WorkerID worker_id
Definition: job_system.cpp:92
TaskHandle task_index
Definition: job_system.cpp:93
bool isNull() const noexcept
Definition: job_system.cpp:109
std::atomic_bool * is_ready_for_gc
Definition: job_api.hpp:230
void ReleaseTaskToPool() const
Definition: job_system.cpp:212