35#define IS_SINGLE_THREADED 0
39#define IS_SINGLE_THREADED 0
40#elif (__ANDROID__ || __linux || __unix || __posix)
43#define IS_SINGLE_THREADED 0
47#define IS_SINGLE_THREADED 1
52#define WIN32_LEAN_AND_MEAN
55#define WINDOWS_EXTRA_LEAN
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);
84 using Byte =
unsigned char;
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.");
117 struct alignas(k_CachelineSize)
Task
119 static constexpr std::size_t k_SizeOfMembers =
120 sizeof(
const char*) +
123 sizeof(std::uint8_t) +
124 sizeof(std::atomic_bool);
133 Byte user_data[k_TaskPaddingDataSize];
139 is_ready_for_gc{
false},
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.");
153 unsigned char storage[
sizeof(
Task)];
177 std::mutex init_mutex = {};
178 std::condition_variable init_cv = {};
179 std::atomic_uint32_t num_workers_ready = {};
214 is_ready_for_gc->store(
true, std::memory_order_release);
217#if JOB_SYS_ASSERTIONS
223 std::fprintf(stderr,
"JobSystem [%s:%i] Assertion '%s' Failed.\n", filename, line_number, msg);
234 static void WakeUpAllWorkers()
noexcept
239 static void WakeUpOneWorker()
noexcept
244 static void Sleep()
noexcept
248 if (job_system->
is_running.load(std::memory_order_relaxed))
283 for (std::size_t i = 0u; i < capacity_minus_one; ++i)
285 memory[i].
next = &memory[i + 1u];
287 memory[capacity_minus_one].
next =
nullptr;
309 JobAssert(result !=
nullptr,
"Allocation failure.");
311 return new (result)
job::Task(name, job_fn, counter);
347 if (ctx.task_counter !=
nullptr)
349 ctx.task_counter->unfinished_tasks.fetch_sub(1, std::memory_order_release);
372 while (read_idx != num_tasks)
376 const bool task_is_finished = task_ptr->
is_ready_for_gc.load(std::memory_order_acquire);
378 if (task_is_finished)
380 task_pool::DeallocateTask(&
task_pool, task_ptr);
384 allocated_tasks[write_idx++] = task_handle;
388 worker->num_allocated_tasks = write_idx;
396 while (read_idx < num_tasks && num_gc < max_tasks_to_gc)
400 const bool task_is_finished = task_ptr->
is_ready_for_gc.load(std::memory_order_acquire);
402 if (task_is_finished)
404 allocated_tasks[read_idx] = allocated_tasks[--num_tasks];
406 task_pool::DeallocateTask(&
task_pool, task_ptr);
415 worker->num_allocated_tasks = num_tasks;
422 const std::uint32_t other_worker_id = pcg32_boundedrand_r(&
worker->rng_state, num_workers);
437 worker->normal_queue.Pop(&task_ptr);
439 if (task_ptr.
isNull() && !is_main_thread)
441 worker->worker_queue.Pop(&task_ptr);
447 if (other_worker !=
worker)
449 other_worker->normal_queue.Steal(&result);
451 if (result.
isNull() && !is_main_thread)
453 other_worker->worker_queue.Steal(&result);
462 task_ptr = TrySteal(
worker->last_stolen_worker);
469 task_ptr = TrySteal(random_worker);
476 worker->last_stolen_worker = random_worker;
482 task::RunTaskFunction(
task, worker::GetCurrentID());
493 job_system->
is_running.store(
true, std::memory_order_relaxed);
494 init_lock->
init_cv.notify_all();
498 std::unique_lock<std::mutex> lock(init_lock->
init_mutex);
499 init_lock->
init_cv.wait(lock, [init_lock]() ->
bool {
507 std::atomic_thread_fence(std::memory_order_acquire);
512 const HANDLE handle = GetCurrentThread();
517 const DWORD_PTR affinity_mask = 1ull << thread_id;
518 const DWORD_PTR affinity_result = SetThreadAffinityMask(handle, affinity_mask);
520 if (affinity_result > 0)
530 const unsigned int thread_index =
unsigned int(
worker - job_system->
workers);
532 char thread_name[32] = u8
"";
533 wchar_t thread_name_w[
sizeof(thread_name)] = L
"";
535 const int c_size = std::snprintf(thread_name,
sizeof(thread_name),
"job::Worker%u", thread_index);
537 std::mbstowcs(thread_name_w, thread_name, c_size);
539 const HRESULT hr = SetThreadDescription(handle, thread_name_w);
540 JobAssert(SUCCEEDED(hr),
"Failed to set thread name.");
546 WaitForAllThreadsReady(job_system);
556 while (job_system->
is_running.load(std::memory_order_relaxed))
558 if (!worker::TryRunTask(
worker))
580 system::WakeUpAllWorkers();
584 worker::TryRunTask(
worker);
590 static bool IsPointerAligned(
const void*
const ptr,
const std::size_t alignment)
noexcept
592 return (
reinterpret_cast<std::uintptr_t
>(ptr) & (alignment - 1u)) == 0u;
595 static void* AlignPointer(
const void*
const ptr,
const std::size_t alignment)
noexcept
597 const std::size_t required_alignment_mask = alignment - 1;
599 return reinterpret_cast<void*
>(
reinterpret_cast<std::uintptr_t
>(ptr) + required_alignment_mask & ~required_alignment_mask);
606 std::size_t num_elements;
610 static Span<T> LinearAlloc(
void*& ptr,
const std::size_t num_elements)
noexcept
612 void*
const result = AlignPointer(ptr,
alignof(T));
614 ptr =
static_cast<unsigned char*
>(result) +
sizeof(T) * num_elements;
616 for (std::size_t i = 0; i < num_elements; ++i)
618 new (
static_cast<T*
>(result) + i) T;
621 return Span<T>{
static_cast<T*
>(result), num_elements};
625 static T* SpanAlloc(Span<T>*
const span,
const std::size_t num_elements)
noexcept
627 JobAssert(num_elements <= span->num_elements,
"Out of bounds span alloc.");
629 T*
const result = span->ptr;
631 span->ptr += num_elements;
632 span->num_elements -= num_elements;
637 static std::size_t AlignedSizeUp(
const std::size_t size,
const std::size_t alignment)
noexcept
639 const std::size_t remainder = size % alignment;
641 return remainder != 0 ? size + (alignment - remainder) : size;
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;
650 in_out_reqs->byte_size +=
sizeof(T) * num_elements;
653 static bool IsPowerOf2(
const std::size_t value)
noexcept
655 return (value & (value - 1)) == 0;
669 JobAssert(num_tasks_per_worker <= std::uint16_t(-1),
"Too many task items per worker.");
671 return std::uint16_t(num_tasks_per_worker);
674 static std::uint32_t TotalNumTasks(
const job::WorkerID num_threads,
const std::uint16_t num_tasks_per_worker)
noexcept
676 return num_tasks_per_worker * num_threads;
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.");
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);
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);
707 const bool needs_delete = memory ==
nullptr;
711 memory = ::operator
new[](memory_requirements.byte_size, std::align_val_t{memory_requirements.alignment});
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`.");
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);
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);
730 job_system->
workers = all_workers.ptr;
743 GetSystemInfo(&sysinfo);
745 switch (sysinfo.wProcessorArchitecture)
747 case PROCESSOR_ARCHITECTURE_AMD64:
752 case PROCESSOR_ARCHITECTURE_ARM:
757 case PROCESSOR_ARCHITECTURE_ARM64:
762 case PROCESSOR_ARCHITECTURE_IA64:
767 case PROCESSOR_ARCHITECTURE_INTEL:
772 case PROCESSOR_ARCHITECTURE_UNKNOWN:
783 for (std::uint64_t worker_index = 0; worker_index < num_threads; ++worker_index)
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;
799 std::atomic_thread_fence(std::memory_order_release);
800 for (std::uint64_t worker_index = 1; worker_index < num_threads; ++worker_index)
802 worker::InitializeThread(job_system->
workers + worker_index);
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.");
813#if IS_SINGLE_THREADED
816 const auto n = std::thread::hardware_concurrency();
817 return n != 0 ? n : 1;
824 GetSystemInfo(&sysinfo);
825 return sysinfo.dwNumberOfProcessors;
827 return sysconf(_SC_NPROCESSORS_ONLN) ;
831 std::size_t len =
sizeof(numCPU);
835 mib[1] = HW_AVAILCPU;
838 sysctl(mib, 2, &numCPU, &len, NULL, 0);
843 sysctl(mib, 2, &numCPU, &len, NULL, 0);
850 return mpctl(MPC_GETNUMSPUS, NULL, NULL);
852 return sysconf(_SC_NPROC_ONLN);
854 NSUInteger a = [[NSProcessInfo processInfo] processorCount];
855 NSUInteger b = [[NSProcessInfo processInfo] activeProcessorCount];
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.");
894 const std::uint32_t num_workers = job_system->
num_workers;
897 while (job_system->
is_running.load(std::memory_order_relaxed) !=
true) {}
901 job_system->
is_running.store(
false, std::memory_order_relaxed);
905 system::WakeUpAllWorkers();
907 for (std::uint32_t i = 0; i < num_workers; ++i)
913 worker::ShutdownThread(
worker);
916 worker->~ThreadLocalState();
921 job_system->~JobSystemContext();
927 ::operator
delete[](job_system, job_system->
system_alloc_size, std::align_val_t{job_system->system_alloc_alignment});
935 system::WakeUpAllWorkers();
941 worker::TryRunTask(
worker);
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
956 const WorkerID worker_id = worker::GetCurrentID();
962 if (
worker->num_allocated_tasks == max_tasks_per_worker)
964 worker::GarbageCollectAllocatedTasks(
worker);
966 if (
worker->num_allocated_tasks == max_tasks_per_worker)
969 system::WakeUpAllWorkers();
971 while (
worker->num_allocated_tasks == max_tasks_per_worker)
973 worker::TryRunTask(
worker);
974 worker::GarbageCollectAllocatedTasks(
worker);
980 Task*
const task = task_pool::AllocateTask(&
worker->task_allocator, name, func, counter);
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;
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.");
994 InitUserData(aligned_ptr, user_data);
995 task->userdata_align =
static_cast<std::uint8_t
>(alignment_offset);
998 worker->allocated_tasks[
worker->num_allocated_tasks++] = task_hdl;
1007 task::SubmitQPushHelper(task_ptr,
worker, &
worker->normal_queue);
1012 task::SubmitQPushHelper(task_ptr,
worker, &
worker->worker_queue);
1016#if defined(__GNUC__)
1017 __builtin_unreachable();
1018#elif defined(_MSC_VER)
1026 if (num_pending_jobs >= num_workers)
1028 system::WakeUpAllWorkers();
1032 system::WakeUpOneWorker();
1036#if defined(_MSC_VER)
1037#define NativePause YieldProcessor
1038#elif defined(__clang__) && defined(__SSE__) || defined(__INTEL_COMPILER)
1039#include <xmmintrin.h>
1040#define NativePause _mm_pause
1041#elif defined(__arm__)
1043#define NativePause __yield
1045#define NativePause() __asm__ __volatile__("yield")
1048#define NativePause std::this_thread::yield
1062 std::this_thread::yield();
1067#undef IS_SINGLE_THREADED
1069#if defined(_MSC_VER)
1071#pragma warning(push)
1072#pragma warning(disable : 4244)
1073#pragma warning(disable : 4146)
1077#include "pcg_basic.c"
1079#if defined(_MSC_VER)
API for a multi-threading job system.
#define JobAssert(expr, msg)
Concurrent Queue Implmementations for different situations.
static thread_local job::ThreadLocalState * g_CurrentWorker
static job::JobSystemContext * g_JobSystem
void(*)(const PrivateCtx &ctx) JobFn
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
const char * sys_arch_str
std::atomic_bool is_running
void WaitOn(const Counter &counter) noexcept
Blocks until all tasks associated with counter are done while This function will block but do work wh...
std::mutex worker_sleep_mutex
void Shutdown() noexcept
This will deallocate any memory used by the system and shutdown any threads created by 'bfjob::initia...
std::atomic< job::TaskPtr > AtomicTaskPtr
std::condition_variable worker_sleep_cv
static constexpr std::size_t k_CachelineSize
InitializationLock init_lock
job::ThreadLocalState * workers
ThreadLocalState * last_stolen_worker
SPMCDeque< job::TaskPtr > worker_queue
bool IsMainThread() noexcept
Allows for querying if we are currently executing in the main thread.
std::uint64_t job_steal_rng_seed
The RNG for work queue stealing will be seeded with this value.
TaskHandle TaskHandleType
std::atomic< TaskHandle > AtomicTaskHandleType
SPMCDeque< job::TaskPtr > normal_queue
std::uint16_t worker_queue_size
Number of tasks in each worker's QueueType::WorkerOnly queue. (Must be power of two)
QueueMode
Determines which threads the task will be allowed to run on.
@ 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
std::uint16_t WorkerID
The id type of each worker thread.
WorkerID CurrentWorker() noexcept
The current id of the current thread. This function can be called by any thread concurrently.
TaskMemoryBlock * freelist
std::atomic_size_t num_available_jobs
const char * ProcessorArchitectureName() noexcept
An implementation defined name for the CPU architecture of the device. This function can be called by...
TaskHandleType num_allocated_tasks
std::uint32_t num_tasks_per_worker
std::uint16_t NumWorkers() noexcept
Returns the number of workers created by the system. This function can be called by any thread concur...
std::uint16_t num_threads
Use 0 to indicate using the number of cores available on the system.
std::atomic_uint32_t num_workers_ready
std::atomic_int32_t AtomicInt32
void PauseProcessor() noexcept
CPU pause instruction to indicate when you are in a spin wait loop.
std::size_t system_alloc_alignment
static constexpr TaskHandle NullTaskHandle
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...
pcg_state_setseq_64 rng_state
std::atomic_uint32_t num_user_threads_setup
TaskHandle * allocated_tasks
std::uint32_t num_workers
std::condition_variable init_cv
std::size_t system_alloc_size
static constexpr std::size_t k_ExpectedTaskSize
@ SUCCESS
Returned from Push, Pop and Steal.
std::uint16_t normal_queue_size
Number of tasks in each worker's QueueType::Default queue. (Must be power of two)
std::size_t NumSystemThreads() noexcept
Makes system calls to grab the number threads / processors on the device. This function can be called...
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.
The runtime configuration for the Job System.
The memory requirements for a given configuration JobSystemCreateOptions.
JobSystemMemoryRequirements(const JobSystemCreateOptions &options={}) noexcept
std::uint8_t userdata_align
Alignment offset needed by userdata.
Task(const char *const name, const job::internal::JobFn job_fn, job::Counter *const counter) noexcept
Counter * counter
The counter to be decremented.
const char * name
Debug name of this task.
std::atomic_bool is_ready_for_gc
Set to true when the task can be reused.
internal::JobFn job_fn
The function that will be run.
bool isNull() const noexcept
std::atomic_bool * is_ready_for_gc
void ReleaseTaskToPool() const