-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathJobSystemThreadPool.cpp
More file actions
574 lines (472 loc) · 14.4 KB
/
Copy pathJobSystemThreadPool.cpp
File metadata and controls
574 lines (472 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#include <Jolt/Core/JobSystemThreadPool.h>
#include <Jolt/Core/Profiler.h>
#include <Jolt/Core/FPException.h>
#include <chrono>
using namespace std::chrono_literals;
JPH_SUPPRESS_WARNINGS_STD_BEGIN
#include <algorithm>
JPH_SUPPRESS_WARNINGS_STD_END
#ifdef JPH_PLATFORM_WINDOWS
JPH_SUPPRESS_WARNING_PUSH
JPH_MSVC_SUPPRESS_WARNING(5039) // winbase.h(13179): warning C5039: 'TpSetCallbackCleanupGroup': pointer or reference to potentially throwing function passed to 'extern "C"' function under -EHc. Undefined behavior may occur if this function throws an exception.
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
JPH_SUPPRESS_WARNING_POP
#endif
JPH_NAMESPACE_BEGIN
JobSystemThreadPool::Semaphore::Semaphore()
{
#ifdef JPH_PLATFORM_WINDOWS
mSemaphore = CreateSemaphore(nullptr, 0, INT_MAX, nullptr);
#endif
}
JobSystemThreadPool::Semaphore::~Semaphore()
{
#ifdef JPH_PLATFORM_WINDOWS
CloseHandle(mSemaphore);
#endif
}
void JobSystemThreadPool::Semaphore::Release(uint inNumber)
{
JPH_ASSERT(inNumber > 0);
#ifdef JPH_PLATFORM_WINDOWS
int old_value = mCount.fetch_add(inNumber);
if (old_value < 0)
{
int new_value = old_value + (int)inNumber;
int num_to_release = min(new_value, 0) - old_value;
::ReleaseSemaphore(mSemaphore, num_to_release, nullptr);
}
#else
lock_guard lock(mLock);
mCount += (int)inNumber;
if (inNumber > 1)
mWaitVariable.notify_all();
else
mWaitVariable.notify_one();
#endif
}
void JobSystemThreadPool::Semaphore::Acquire(uint inNumber)
{
JPH_ASSERT(inNumber > 0);
#ifdef JPH_PLATFORM_WINDOWS
int old_value = mCount.fetch_sub(inNumber);
int new_value = old_value - (int)inNumber;
if (new_value < 0)
{
int num_to_acquire = min(old_value, 0) - new_value;
for (int i = 0; i < num_to_acquire; ++i)
WaitForSingleObject(mSemaphore, INFINITE);
}
#else
unique_lock lock(mLock);
mCount -= (int)inNumber;
mWaitVariable.wait(lock, [this]() { return mCount >= 0; });
#endif
}
JobSystemThreadPool::BarrierImpl::BarrierImpl()
{
for (atomic<Job *> &j : mJobs)
j = nullptr;
}
JobSystemThreadPool::BarrierImpl::~BarrierImpl()
{
JPH_ASSERT(IsEmpty());
}
void JobSystemThreadPool::BarrierImpl::AddJob(const JobHandle &inJob)
{
JPH_PROFILE_FUNCTION();
bool release_semaphore = false;
// Set the barrier on the job, this returns true if the barrier was successfully set (otherwise the job is already done and we don't need to add it to our list)
Job *job = inJob.GetPtr();
if (job->SetBarrier(this))
{
// If the job can be executed we want to release the semaphore an extra time to allow the waiting thread to start executing it
mNumToAcquire++;
if (job->CanBeExecuted())
{
release_semaphore = true;
mNumToAcquire++;
}
// Add the job to our job list
job->AddRef();
uint write_index = mJobWriteIndex++;
while (write_index - mJobReadIndex >= cMaxJobs)
{
JPH_ASSERT(false, "Barrier full, stalling!");
this_thread::sleep_for(100us);
}
mJobs[write_index & (cMaxJobs - 1)] = job;
}
// Notify waiting thread that a new executable job is available
if (release_semaphore)
mSemaphore.Release();
}
void JobSystemThreadPool::BarrierImpl::AddJobs(const JobHandle *inHandles, uint inNumHandles)
{
JPH_PROFILE_FUNCTION();
bool release_semaphore = false;
for (const JobHandle *handle = inHandles, *handles_end = inHandles + inNumHandles; handle < handles_end; ++handle)
{
// Set the barrier on the job, this returns true if the barrier was successfully set (otherwise the job is already done and we don't need to add it to our list)
Job *job = handle->GetPtr();
if (job->SetBarrier(this))
{
// If the job can be executed we want to release the semaphore an extra time to allow the waiting thread to start executing it
mNumToAcquire++;
if (!release_semaphore && job->CanBeExecuted())
{
release_semaphore = true;
mNumToAcquire++;
}
// Add the job to our job list
job->AddRef();
uint write_index = mJobWriteIndex++;
while (write_index - mJobReadIndex >= cMaxJobs)
{
JPH_ASSERT(false, "Barrier full, stalling!");
this_thread::sleep_for(100us);
}
mJobs[write_index & (cMaxJobs - 1)] = job;
}
}
// Notify waiting thread that a new executable job is available
if (release_semaphore)
mSemaphore.Release();
}
void JobSystemThreadPool::BarrierImpl::OnJobFinished(Job *inJob)
{
JPH_PROFILE_FUNCTION();
mSemaphore.Release();
}
void JobSystemThreadPool::BarrierImpl::Wait()
{
while (mNumToAcquire > 0)
{
{
JPH_PROFILE("Execute Jobs");
// Go through all jobs
bool has_executed;
do
{
has_executed = false;
// Loop through the jobs and erase jobs from the beginning of the list that are done
while (mJobReadIndex < mJobWriteIndex)
{
atomic<Job *> &job = mJobs[mJobReadIndex & (cMaxJobs - 1)];
Job *job_ptr = job.load();
if (job_ptr == nullptr || !job_ptr->IsDone())
break;
// Job is finished, release it
job_ptr->Release();
job = nullptr;
++mJobReadIndex;
}
// Loop through the jobs and execute the first executable job
for (uint index = mJobReadIndex; index < mJobWriteIndex; ++index)
{
const atomic<Job *> &job = mJobs[index & (cMaxJobs - 1)];
Job *job_ptr = job.load();
if (job_ptr != nullptr && job_ptr->CanBeExecuted())
{
// This will only execute the job if it has not already executed
job_ptr->Execute();
has_executed = true;
break;
}
}
} while (has_executed);
}
// Wait for another thread to wake us when either there is more work to do or when all jobs have completed
int num_to_acquire = max(1, mSemaphore.GetValue()); // When there have been multiple releases, we acquire them all at the same time to avoid needlessly spinning on executing jobs
mSemaphore.Acquire(num_to_acquire);
mNumToAcquire -= num_to_acquire;
}
// All jobs should be done now, release them
while (mJobReadIndex < mJobWriteIndex)
{
atomic<Job *> &job = mJobs[mJobReadIndex & (cMaxJobs - 1)];
Job *job_ptr = job.load();
JPH_ASSERT(job_ptr != nullptr && job_ptr->IsDone());
job_ptr->Release();
job = nullptr;
++mJobReadIndex;
}
}
void JobSystemThreadPool::Init(uint inMaxJobs, uint inMaxBarriers, int inNumThreads)
{
JPH_ASSERT(mBarriers == nullptr); // Already initialized?
// Init freelist of barriers
mMaxBarriers = inMaxBarriers;
mBarriers = new BarrierImpl [inMaxBarriers];
// Init freelist of jobs
mJobs.Init(inMaxJobs, inMaxJobs);
// Init queue
for (atomic<Job *> &j : mQueue)
j = nullptr;
// Start the worker threads
StartThreads(inNumThreads);
}
JobSystemThreadPool::JobSystemThreadPool(uint inMaxJobs, uint inMaxBarriers, int inNumThreads)
{
Init(inMaxJobs, inMaxBarriers, inNumThreads);
}
void JobSystemThreadPool::StartThreads(int inNumThreads)
{
// Auto detect number of threads
if (inNumThreads < 0)
inNumThreads = thread::hardware_concurrency() - 1;
// If no threads are requested we're done
if (inNumThreads == 0)
return;
// Don't quit the threads
mQuit = false;
// Allocate heads
mHeads = new atomic<uint> [inNumThreads];
for (int i = 0; i < inNumThreads; ++i)
mHeads[i] = 0;
// Start running threads
JPH_ASSERT(mThreads.empty());
mThreads.reserve(inNumThreads);
for (int i = 0; i < inNumThreads; ++i)
{
// Name the thread
char name[64];
snprintf(name, sizeof(name), "Worker %d", int(i + 1));
// Create thread
mThreads.emplace_back([this, name, i] { ThreadMain(name, i); });
}
}
JobSystemThreadPool::~JobSystemThreadPool()
{
// Stop all worker threads
StopThreads();
// Ensure that none of the barriers are used
#ifdef JPH_ENABLE_ASSERTS
for (const BarrierImpl *b = mBarriers, *b_end = mBarriers + mMaxBarriers; b < b_end; ++b)
JPH_ASSERT(!b->mInUse);
#endif // JPH_ENABLE_ASSERTS
delete [] mBarriers;
}
void JobSystemThreadPool::StopThreads()
{
if (mThreads.empty())
return;
// Signal threads that we want to stop and wake them up
mQuit = true;
mSemaphore.Release((uint)mThreads.size());
// Wait for all threads to finish
for (thread &t : mThreads)
if (t.joinable())
t.join();
// Delete all threads
mThreads.clear();
// Ensure that there are no lingering jobs in the queue
for (uint head = 0; head != mTail; ++head)
{
// Fetch job
Job *job_ptr = mQueue[head & (cQueueLength - 1)].exchange(nullptr);
if (job_ptr != nullptr)
{
// And execute it
job_ptr->Execute();
job_ptr->Release();
}
}
// Destroy heads and reset tail
delete [] mHeads;
mHeads = nullptr;
mTail = 0;
}
JobHandle JobSystemThreadPool::CreateJob(const char *inJobName, ColorArg inColor, const JobFunction &inJobFunction, uint32 inNumDependencies)
{
JPH_PROFILE_FUNCTION();
// Loop until we can get a job from the free list
uint32 index;
for (;;)
{
index = mJobs.ConstructObject(inJobName, inColor, this, inJobFunction, inNumDependencies);
if (index != AvailableJobs::cInvalidObjectIndex)
break;
JPH_ASSERT(false, "No jobs available!");
this_thread::sleep_for(100us);
}
Job *job = &mJobs.Get(index);
// Construct handle to keep a reference, the job is queued below and may immediately complete
JobHandle handle(job);
// If there are no dependencies, queue the job now
if (inNumDependencies == 0)
QueueJob(job);
// Return the handle
return handle;
}
void JobSystemThreadPool::FreeJob(Job *inJob)
{
mJobs.DestructObject(inJob);
}
JobSystem::Barrier *JobSystemThreadPool::CreateBarrier()
{
JPH_PROFILE_FUNCTION();
// Find the first unused barrier
for (uint32 index = 0; index < mMaxBarriers; ++index)
{
bool expected = false;
if (mBarriers[index].mInUse.compare_exchange_strong(expected, true))
return &mBarriers[index];
}
return nullptr;
}
void JobSystemThreadPool::DestroyBarrier(Barrier *inBarrier)
{
JPH_PROFILE_FUNCTION();
// Check that no jobs are in the barrier
JPH_ASSERT(static_cast<BarrierImpl *>(inBarrier)->IsEmpty());
// Flag the barrier as unused
bool expected = true;
static_cast<BarrierImpl *>(inBarrier)->mInUse.compare_exchange_strong(expected, false);
JPH_ASSERT(expected);
}
void JobSystemThreadPool::WaitForJobs(Barrier *inBarrier)
{
JPH_PROFILE_FUNCTION();
// Let our barrier implementation wait for the jobs
static_cast<BarrierImpl *>(inBarrier)->Wait();
}
uint JobSystemThreadPool::GetHead() const
{
// Find the minimal value across all threads
uint head = mTail;
for (size_t i = 0; i < mThreads.size(); ++i)
head = min(head, mHeads[i].load());
return head;
}
void JobSystemThreadPool::QueueJobInternal(Job *inJob)
{
// Add reference to job because we're adding the job to the queue
inJob->AddRef();
// Need to read head first because otherwise the tail can already have passed the head
// We read the head outside of the loop since it involves iterating over all threads and we only need to update
// it if there's not enough space in the queue.
uint head = GetHead();
for (;;)
{
// Check if there's space in the queue
uint old_value = mTail;
if (old_value - head >= cQueueLength)
{
// We calculated the head outside of the loop, update head (and we also need to update tail to prevent it from passing head)
head = GetHead();
old_value = mTail;
// Second check if there's space in the queue
if (old_value - head >= cQueueLength)
{
// Wake up all threads in order to ensure that they can clear any nullptrs they may not have processed yet
mSemaphore.Release((uint)mThreads.size());
// Sleep a little (we have to wait for other threads to update their head pointer in order for us to be able to continue)
this_thread::sleep_for(100us);
continue;
}
}
// Write the job pointer if the slot is empty
Job *expected_job = nullptr;
bool success = mQueue[old_value & (cQueueLength - 1)].compare_exchange_strong(expected_job, inJob);
// Regardless of who wrote the slot, we will update the tail (if the successful thread got scheduled out
// after writing the pointer we still want to be able to continue)
mTail.compare_exchange_strong(old_value, old_value + 1);
// If we successfully added our job we're done
if (success)
break;
}
}
void JobSystemThreadPool::QueueJob(Job *inJob)
{
JPH_PROFILE_FUNCTION();
// If we have no worker threads, we can't queue the job either. We assume in this case that the job will be added to a barrier and that the barrier will execute the job when it's Wait() function is called.
if (mThreads.empty())
return;
// Queue the job
QueueJobInternal(inJob);
// Wake up thread
mSemaphore.Release();
}
void JobSystemThreadPool::QueueJobs(Job **inJobs, uint inNumJobs)
{
JPH_PROFILE_FUNCTION();
JPH_ASSERT(inNumJobs > 0);
// If we have no worker threads, we can't queue the job either. We assume in this case that the job will be added to a barrier and that the barrier will execute the job when it's Wait() function is called.
if (mThreads.empty())
return;
// Queue all jobs
for (Job **job = inJobs, **job_end = inJobs + inNumJobs; job < job_end; ++job)
QueueJobInternal(*job);
// Wake up threads
mSemaphore.Release(min(inNumJobs, (uint)mThreads.size()));
}
#ifdef JPH_PLATFORM_WINDOWS
// Sets the current thread name in MSVC debugger
static void SetThreadName(const char *inName)
{
#pragma pack(push, 8)
struct THREADNAME_INFO
{
DWORD dwType; // Must be 0x1000.
LPCSTR szName; // Pointer to name (in user addr space).
DWORD dwThreadID; // Thread ID (-1=caller thread).
DWORD dwFlags; // Reserved for future use, must be zero.
};
#pragma pack(pop)
THREADNAME_INFO info;
info.dwType = 0x1000;
info.szName = inName;
info.dwThreadID = (DWORD)-1;
info.dwFlags = 0;
__try
{
RaiseException(0x406D1388, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR *)&info);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
}
}
#endif
void JobSystemThreadPool::ThreadMain([[maybe_unused]] const char *inName, int inThreadIndex)
{
#ifdef JPH_PLATFORM_WINDOWS
SetThreadName(inName);
#endif
// Enable floating point exceptions
FPExceptionsEnable enable_exceptions;
JPH_UNUSED(enable_exceptions);
JPH_PROFILE_THREAD_START(inName);
atomic<uint> &head = mHeads[inThreadIndex];
while (!mQuit)
{
// Wait for jobs
mSemaphore.Acquire();
{
JPH_PROFILE("Executing Jobs");
// Loop over the queue
while (head != mTail)
{
// Exchange any job pointer we find with a nullptr
atomic<Job *> &job = mQueue[head & (cQueueLength - 1)];
if (job.load() != nullptr)
{
Job *job_ptr = job.exchange(nullptr);
if (job_ptr != nullptr)
{
// And execute it
job_ptr->Execute();
job_ptr->Release();
}
}
head++;
}
}
}
JPH_PROFILE_THREAD_END();
}
JPH_NAMESPACE_END