载入中...
搜索中...
未找到
JobSystemThreadPool.cpp
浏览该文件的文档.
2
3#include "common/Exception.h"
4#include "thread/ThreadPool.h"
5
6#include <algorithm>
7#include <condition_variable>
8#include <cstddef>
9#include <deque>
10#include <mutex>
11#include <new>
12#include <string>
13#include <thread>
14#include <utility>
15#include <vector>
16
17namespace eve {
18namespace thread {
19
20namespace {
21
22enum class JobStatus { Pending, Scheduled, Running, Done, Failed };
23enum class JobScope { Heap, Frame };
24
25struct JobImpl;
26
27thread_local const void *tlsCurrentState = nullptr;
28thread_local JobImpl *tlsCurrentJob = nullptr;
29
30bool isDoneStatus(JobStatus status) {
31 return status == JobStatus::Done || status == JobStatus::Failed;
32}
33
34} // namespace
35
36namespace {
37
43class FrameArena {
44public:
45 FrameArena() = default;
46 ~FrameArena() { reset(); }
47
54 void *alloc(size_t size, size_t align) {
55 if (size > kArenaBlockSize)
56 throw eve::Exception("JobSystem arena allocation exceeds block size");
57 if (blocks_.empty())
58 blocks_.emplace_back(kArenaBlockSize);
59 size_t aligned = (offset_ + align - 1) & ~(align - 1);
60 if (aligned + size > blocks_.back().size()) {
61 blocks_.emplace_back(kArenaBlockSize);
62 aligned = 0;
63 }
64 void *ptr = blocks_.back().data() + aligned;
65 offset_ = aligned + size;
66 return ptr;
67 }
68
74 void track(void *ptr, void (*destroy)(void *)) {
75 tracked_.push_back({ptr, destroy});
76 }
77
79 void reset() {
80 for (auto &entry : tracked_)
81 entry.destroy(entry.ptr);
82 tracked_.clear();
83 offset_ = 0;
84 }
85
86private:
87 static constexpr size_t kArenaBlockSize = 64 * 1024;
88
89 struct Tracked {
90 void *ptr;
91 void (*destroy)(void *);
92 };
93
94 std::vector<std::vector<std::byte>> blocks_;
95 std::vector<Tracked> tracked_;
96 size_t offset_ = 0;
97};
98
99} // namespace
100
102 mutable std::mutex mu;
103 std::condition_variable cv;
104 std::deque<JobImpl *> ready;
105 int outstanding = 0;
107 int workerCount = 0;
108 bool stopping = false;
109 FrameArena arena;
110
111 JobImpl *createJobLocked(JobScope scope, JobFunc body, JobFunc onComplete);
112 void scheduleLocked(JobImpl *job);
113 void enqueueLocked(JobImpl *job);
114 void runJob(JobImpl *job);
115 void completeJob(JobImpl *job, bool ok);
116 void fireCompletion(JobImpl *job);
117 void recordError(JobImpl *job, const std::string &message);
118 void releaseDependents(JobImpl *job);
119 void waitJob(JobImpl *job);
120 void waitFrameJobs();
121 int autoChunk(int count) const;
122
123 static void workerLoop(State *state);
124};
125
126namespace {
127
128struct JobImpl final : public Job {
129 JobImpl(JobSystemThreadPool::State *owner, JobScope scope_, JobFunc fn, JobFunc onDone)
130 : state(owner), scope(scope_), body(std::move(fn)), onComplete(std::move(onDone)) {}
131
132 ~JobImpl() override {
133 // parallel_for children are owned by their join job and are guaranteed
134 // complete before the join finishes, so deleting them here is safe.
135 // Frame-scope jobs never populate ownedChildren (the arena owns them).
136 for (JobImpl *child : ownedChildren)
137 delete child;
138 }
139
140 void wait() override;
141 bool isDone() const override;
142 bool hasFailed() const override;
143 std::string getError() const override;
144 int getPendingDependencyCount() const override;
145 void addDependency(Job *predecessor) override;
146 void setCompletionCallback(JobFunc callback) override;
147
148 JobSystemThreadPool::State *state;
149 JobScope scope;
152 int depCount = 0;
153 std::vector<JobImpl *> dependents;
154 std::vector<JobImpl *> ownedChildren;
155 JobStatus status = JobStatus::Pending;
156 bool scheduled = false;
157 bool enqueued = false;
158 std::string error;
159};
160
161class TaskGroupImpl final : public TaskGroup {
162public:
163 TaskGroupImpl(JobSystemThreadPool::State *owner, JobScope scope_)
164 : state(owner), scope(scope_) {}
165 ~TaskGroupImpl() override = default;
166
167 Job *fork(JobFunc body) override { return fork(std::move(body), JobFunc{}); }
168
169 Job *fork(JobFunc body, JobFunc onComplete) override {
170 if (!body)
171 throw eve::Exception("TaskGroup::fork: null function");
172 JobImpl *job = nullptr;
173 {
174 std::lock_guard<std::mutex> lock(state->mu);
175 if (state->stopping)
176 throw eve::Exception("JobSystem is stopped");
177 job = state->createJobLocked(scope, std::move(body), std::move(onComplete));
178 state->scheduleLocked(job);
179 children.push_back(job);
180 }
181 state->cv.notify_one();
182 return job;
183 }
184
185 void wait() override {
186 std::vector<JobImpl *> snapshot;
187 {
188 std::lock_guard<std::mutex> lock(state->mu);
189 snapshot = children;
190 }
191 for (JobImpl *child : snapshot)
192 state->waitJob(child);
193 std::lock_guard<std::mutex> lock(state->mu);
194 children.clear();
195 }
196
197 int getPendingCount() const override {
198 std::lock_guard<std::mutex> lock(state->mu);
199 int pending = 0;
200 for (JobImpl *child : children) {
201 if (!isDoneStatus(child->status))
202 ++pending;
203 }
204 return pending;
205 }
206
207 JobSystemThreadPool::State *state;
208 JobScope scope;
209 std::vector<JobImpl *> children;
210};
211
212} // namespace
213
214// ---- State ----
215
218 if (scope == JobScope::Frame) {
219 void *mem = arena.alloc(sizeof(JobImpl), alignof(JobImpl));
220 auto *job = new (mem) JobImpl(this, scope, std::move(body), std::move(onComplete));
221 arena.track(job, [](void *ptr) { static_cast<JobImpl *>(ptr)->~JobImpl(); });
222 return job;
223 }
224 return new JobImpl(this, scope, std::move(body), std::move(onComplete));
225}
226
228 job->scheduled = true;
229 ++outstanding;
230 if (job->scope == JobScope::Frame)
231 ++outstandingFrame;
232 if (job->depCount == 0)
233 enqueueLocked(job);
234}
235
237 if (job->enqueued)
238 return;
239 job->enqueued = true;
240 ready.push_back(job);
241}
242
244 {
245 std::lock_guard<std::mutex> lock(mu);
246 job->status = JobStatus::Running;
247 }
248
249 // A job body that calls waitAll()/stop() must be treated as a worker even
250 // when it is help-executed inline by a waiting thread (fork/join path),
251 // otherwise those calls would re-enter the wait they are running inside.
252 const void *previousState = tlsCurrentState;
253 if (previousState != this)
254 tlsCurrentState = this;
255
256 JobImpl *previous = tlsCurrentJob;
257 tlsCurrentJob = job;
258 bool ok = true;
259 if (job->body) {
260 try {
261 job->body();
262 } catch (const eve::Exception &e) {
263 ok = false;
264 recordError(job, e.what());
265 } catch (const std::exception &e) {
266 ok = false;
267 recordError(job, e.what());
268 } catch (...) {
269 ok = false;
270 recordError(job, "unknown exception");
271 }
272 }
273 tlsCurrentJob = previous;
274
275 completeJob(job, ok);
276 tlsCurrentState = previousState;
277}
278
280 {
281 std::lock_guard<std::mutex> lock(mu);
282 job->status = ok ? JobStatus::Done : JobStatus::Failed;
283 }
284 cv.notify_all();
285
286 // Completion callback runs before dependents are released so it can
287 // publish results downstream jobs consume.
288 fireCompletion(job);
289 releaseDependents(job);
290
291 // Decrement the outstanding counters only after the job's completion
292 // callback and dependent release have finished using it. Otherwise a
293 // waiter (beginFrame/endFrame -> waitFrameJobs) can observe
294 // outstandingFrame == 0 and reset the per-frame arena while this worker is
295 // still touching the job (fireCompletion / releaseDependents), destroying
296 // it out from under the worker — a use-after-free that manifests as a
297 // lost forked job (hang) or a crash.
298 {
299 std::lock_guard<std::mutex> lock(mu);
300 --outstanding;
301 if (job->scope == JobScope::Frame)
302 --outstandingFrame;
303 }
304 cv.notify_all();
305}
306
308 if (!job->onComplete)
309 return;
310 try {
311 job->onComplete();
312 } catch (const eve::Exception &e) {
313 recordError(job, std::string("completion callback: ") + e.what());
314 } catch (const std::exception &e) {
315 recordError(job, std::string("completion callback: ") + e.what());
316 } catch (...) {
317 recordError(job, "completion callback: unknown exception");
318 }
319}
320
321void JobSystemThreadPool::State::recordError(JobImpl *job, const std::string &message) {
322 std::lock_guard<std::mutex> lock(mu);
323 if (!job->error.empty())
324 job->error += "; ";
325 job->error += message;
326}
327
329 bool woke = false;
330 {
331 std::lock_guard<std::mutex> lock(mu);
332 for (JobImpl *dep : job->dependents) {
333 if (dep->depCount > 0)
334 --dep->depCount;
335 if (dep->depCount == 0 && dep->scheduled && !dep->enqueued) {
336 enqueueLocked(dep);
337 woke = true;
338 }
339 }
340 job->dependents.clear();
341 }
342 if (woke)
343 cv.notify_all();
344}
345
347 std::unique_lock<std::mutex> lock(mu);
348 if (isDoneStatus(job->status))
349 return;
350 if (tlsCurrentJob == job)
351 throw eve::Exception("JobSystem: a job cannot wait on itself");
352
353 for (;;) {
354 if (isDoneStatus(job->status))
355 return;
356 if (!ready.empty()) {
357 JobImpl *next = ready.front();
358 ready.pop_front();
359 lock.unlock();
360 runJob(next);
361 lock.lock();
362 continue;
363 }
364 if (stopping)
365 return;
366 cv.wait(lock);
367 }
368}
369
371 std::unique_lock<std::mutex> lock(mu);
372 while (outstandingFrame > 0) {
373 if (!ready.empty()) {
374 JobImpl *next = ready.front();
375 ready.pop_front();
376 lock.unlock();
377 runJob(next);
378 lock.lock();
379 continue;
380 }
381 if (stopping)
382 return;
383 cv.wait(lock);
384 }
385 arena.reset();
386}
387
389 const int target = std::max(1, workerCount * 4);
390 return std::max(1, (count + target - 1) / target);
391}
392
394 tlsCurrentState = state;
395 for (;;) {
396 JobImpl *job = nullptr;
397 {
398 std::unique_lock<std::mutex> lock(state->mu);
399 state->cv.wait(lock, [state] { return state->stopping || !state->ready.empty(); });
400 if (state->stopping && state->ready.empty())
401 break;
402 job = state->ready.front();
403 state->ready.pop_front();
404 }
405 if (job)
406 state->runJob(job);
407 }
408 tlsCurrentState = nullptr;
409}
410
411// ---- JobImpl ----
412
413void JobImpl::wait() {
414 state->waitJob(this);
415}
416
417bool JobImpl::isDone() const {
418 std::lock_guard<std::mutex> lock(state->mu);
419 return isDoneStatus(status);
420}
421
422bool JobImpl::hasFailed() const {
423 std::lock_guard<std::mutex> lock(state->mu);
424 return status == JobStatus::Failed;
425}
426
427std::string JobImpl::getError() const {
428 std::lock_guard<std::mutex> lock(state->mu);
429 return error;
430}
431
432int JobImpl::getPendingDependencyCount() const {
433 std::lock_guard<std::mutex> lock(state->mu);
434 return depCount;
435}
436
437void JobImpl::addDependency(Job *predecessor) {
438 auto *pred = dynamic_cast<JobImpl *>(predecessor);
439 if (!pred || pred->state != state)
440 throw eve::Exception("JobSystem::addDependency: job does not belong to this system");
441 if (pred == this)
442 throw eve::Exception("JobSystem::addDependency: a job cannot depend on itself");
443
444 std::lock_guard<std::mutex> lock(state->mu);
445 if (scheduled)
446 throw eve::Exception("JobSystem::addDependency: job already scheduled");
447 if (isDoneStatus(pred->status))
448 return; // Predecessor already finished; nothing to wait for.
449 ++depCount;
450 pred->dependents.push_back(this);
451}
452
453void JobImpl::setCompletionCallback(JobFunc callback) {
454 bool alreadyDone = false;
455 {
456 std::lock_guard<std::mutex> lock(state->mu);
457 if (isDoneStatus(status)) {
458 alreadyDone = true;
459 } else {
460 onComplete = std::move(callback);
461 return;
462 }
463 }
464 if (alreadyDone && callback) {
465 try {
466 callback();
467 } catch (const eve::Exception &e) {
468 state->recordError(this, std::string("completion callback: ") + e.what());
469 } catch (const std::exception &e) {
470 state->recordError(this, std::string("completion callback: ") + e.what());
471 } catch (...) {
472 state->recordError(this, "completion callback: unknown exception");
473 }
474 }
475}
476
477// ---- JobSystemThreadPool ----
478
479namespace {
480
481JobImpl *castJob(JobSystemThreadPool::State *state, Job *job) {
482 if (!job)
483 throw eve::Exception("JobSystem: null job");
484 auto *impl = dynamic_cast<JobImpl *>(job);
485 if (!impl || impl->state != state)
486 throw eve::Exception("JobSystem: job does not belong to this system");
487 return impl;
488}
489
490} // namespace
491
493 if (workerCount <= 0) {
494 workerCount = static_cast<int>(std::thread::hardware_concurrency());
495 if (workerCount <= 0)
496 workerCount = 1;
497 }
498 state_ = std::make_shared<State>();
499 pool_ = std::make_unique<ThreadPool>(workerCount);
500 state_->workerCount = pool_->getWorkerCount();
501 poolTasks_.reserve(static_cast<size_t>(state_->workerCount));
502 try {
503 for (int i = 0; i < state_->workerCount; ++i) {
504 auto state = state_;
505 poolTasks_.push_back(pool_->submit([state] { State::workerLoop(state.get()); }));
506 }
507 } catch (...) {
508 {
509 std::lock_guard<std::mutex> lock(state_->mu);
510 state_->stopping = true;
511 }
512 state_->cv.notify_all();
513 if (pool_)
514 pool_->stop();
515 for (Task *task : poolTasks_)
516 delete task;
517 poolTasks_.clear();
518 throw;
519 }
520}
521
523 try {
524 std::lock_guard<std::mutex> lifecycleLock(lifecycleMu_);
525 const bool fromWorker = tlsCurrentState == state_.get();
526 {
527 std::lock_guard<std::mutex> lock(state_->mu);
528 state_->stopping = true;
529 }
530 state_->cv.notify_all();
531
532 if (fromWorker) {
533 // Destroyed from inside one of our jobs: joining any worker could
534 // deadlock (another worker may wait on this task). Mirror
535 // ThreadPool's worker-caller path — detach; worker loops only
536 // touch shared State, which outlives them through their capture.
537 for (Task *task : poolTasks_)
538 delete task;
539 poolTasks_.clear();
540 pool_.reset();
541 return;
542 }
543 if (pool_)
544 pool_->stop();
545 for (Task *task : poolTasks_)
546 delete task;
547 poolTasks_.clear();
548 } catch (...) {
549 }
550}
551
553 return state_->workerCount;
554}
555
557 std::lock_guard<std::mutex> lock(state_->mu);
558 return !state_->stopping;
559}
560
562 std::lock_guard<std::mutex> lock(state_->mu);
563 return static_cast<int>(state_->ready.size());
564}
565
567 std::lock_guard<std::mutex> lock(state_->mu);
568 return state_->outstanding;
569}
570
572 JobImpl *job = nullptr;
573 {
574 std::lock_guard<std::mutex> lock(state_->mu);
575 if (state_->stopping)
576 throw eve::Exception("JobSystem is stopped");
577 job = state_->createJobLocked(JobScope::Heap, std::move(body), JobFunc{});
578 state_->scheduleLocked(job);
579 }
580 state_->cv.notify_one();
581 return job;
582}
583
585 std::lock_guard<std::mutex> lock(state_->mu);
586 return state_->createJobLocked(JobScope::Heap, std::move(body), JobFunc{});
587}
588
590 JobImpl *impl = castJob(state_.get(), job);
591 {
592 std::lock_guard<std::mutex> lock(state_->mu);
593 if (state_->stopping)
594 throw eve::Exception("JobSystem is stopped");
595 if (impl->scheduled)
596 throw eve::Exception("JobSystem::schedule: job already scheduled");
597 state_->scheduleLocked(impl);
598 }
599 state_->cv.notify_one();
600}
601
602Job *JobSystemThreadPool::parallelFor(int first, int last, ParallelForBody body, int chunk) {
603 return parallelForImpl(first, last, std::move(body), chunk, false);
604}
605
607 return new TaskGroupImpl(state_.get(), JobScope::Heap);
608}
609
611 JobImpl *job = nullptr;
612 {
613 std::lock_guard<std::mutex> lock(state_->mu);
614 if (state_->stopping)
615 throw eve::Exception("JobSystem is stopped");
616 job = state_->createJobLocked(JobScope::Frame, std::move(body), JobFunc{});
617 state_->scheduleLocked(job);
618 }
619 state_->cv.notify_one();
620 return job;
621}
622
624 std::lock_guard<std::mutex> lock(state_->mu);
625 return state_->createJobLocked(JobScope::Frame, std::move(body), JobFunc{});
626}
627
629 return parallelForImpl(first, last, std::move(body), chunk, true);
630}
631
633 std::lock_guard<std::mutex> lock(state_->mu);
634 void *mem = state_->arena.alloc(sizeof(TaskGroupImpl), alignof(TaskGroupImpl));
635 auto *group = new (mem) TaskGroupImpl(state_.get(), JobScope::Frame);
636 state_->arena.track(group, [](void *ptr) { static_cast<TaskGroupImpl *>(ptr)->~TaskGroupImpl(); });
637 return group;
638}
639
641 state_->waitFrameJobs();
642}
643
645 state_->waitFrameJobs();
646}
647
649 if (tlsCurrentState == state_.get())
650 throw eve::Exception("JobSystem::waitAll cannot be called from its worker");
651 State *state = state_.get();
652 std::unique_lock<std::mutex> lock(state->mu);
653 while (state->outstanding > 0) {
654 if (!state->ready.empty()) {
655 JobImpl *next = state->ready.front();
656 state->ready.pop_front();
657 lock.unlock();
658 state->runJob(next);
659 lock.lock();
660 continue;
661 }
662 if (state->stopping)
663 return;
664 state->cv.wait(lock);
665 }
666}
667
669 std::lock_guard<std::mutex> lifecycleLock(lifecycleMu_);
670 if (tlsCurrentState == state_.get())
671 throw eve::Exception("JobSystem::stop cannot be called from its worker");
672 {
673 std::lock_guard<std::mutex> lock(state_->mu);
674 state_->stopping = true;
675 }
676 state_->cv.notify_all();
677 if (pool_)
678 pool_->stop();
679 for (Task *task : poolTasks_)
680 delete task;
681 poolTasks_.clear();
682}
683
684Job *JobSystemThreadPool::parallelForImpl(int first, int last, ParallelForBody body, int chunk,
685 bool frameScope) {
686 if (!body)
687 throw eve::Exception("JobSystem::parallelFor: null function");
688 if (last <= first)
689 return frameScope ? submitFrame(JobFunc{}) : submit(JobFunc{});
690
691 const JobScope scope = frameScope ? JobScope::Frame : JobScope::Heap;
692 const int count = last - first;
693 if (chunk <= 0)
694 chunk = state_->autoChunk(count);
695 const int taskCount = (count + chunk - 1) / chunk;
696
697 std::lock_guard<std::mutex> lock(state_->mu);
698 if (state_->stopping)
699 throw eve::Exception("JobSystem is stopped");
700
701 JobImpl *join = state_->createJobLocked(scope, JobFunc{}, JobFunc{});
702 join->depCount = taskCount;
703 if (scope == JobScope::Heap)
704 join->ownedChildren.reserve(static_cast<size_t>(taskCount));
705
706 std::vector<JobImpl *> children;
707 children.reserve(static_cast<size_t>(taskCount));
708 for (int t = 0; t < taskCount; ++t) {
709 const int begin = first + t * chunk;
710 const int end = std::min(last, begin + chunk);
711 JobImpl *child =
712 state_->createJobLocked(scope, [body, begin, end] { body(begin, end); }, JobFunc{});
713 child->dependents.push_back(join);
714 children.push_back(child);
715 if (scope == JobScope::Heap)
716 join->ownedChildren.push_back(child);
717 }
718
719 // Schedule the join first so it sits waiting, then the children; the join
720 // becomes ready when the last child completes.
721 state_->scheduleLocked(join);
722 for (JobImpl *child : children)
723 state_->scheduleLocked(child);
724 state_->cv.notify_all();
725 return join;
726}
727
728} // namespace thread
729} // namespace eve
void * impl
bool enqueued
JobFunc body
std::vector< JobImpl * > dependents
JobStatus status
std::string error
JobFunc onComplete
std::vector< JobImpl * > ownedChildren
JobSystemThreadPool::State * state
JobScope scope
void(* destroy)(void *)
void * ptr
bool scheduled
const FusedGroup & group
SettlementPipeline::Stage fn
int children
Definition TreeMesh.cpp:177
virtual const char * what() const
Returns a string containing reason for the exception.
Definition Exception.h:23
Job * createJob(JobFunc body) override
Create a paused job (dependencies can be wired, then schedule()).
void endFrame() override
End the frame: join outstanding frame jobs and recycle the arena.
Job * submitFrame(JobFunc body) override
Create a frame-scoped job and schedule it immediately.
Job * parallelFor(int first, int last, ParallelForBody body, int chunk=1) override
Run body over [first, last) in parallel.
Job * parallelForFrame(int first, int last, ParallelForBody body, int chunk=1) override
Frame-scoped parallel_for; see parallelFor().
TaskGroup * createTaskGroup() override
Create a fork/join task group.
int getPendingCount() const override
Number of ready jobs waiting for a worker (approximate).
void waitAll() override
Block until every scheduled job has finished.
int getOutstandingCount() const override
Number of scheduled jobs that have not finished (approximate).
int getWorkerCount() const override
Number of scheduler worker threads.
Job * submit(JobFunc body) override
Create a job and schedule it immediately.
void stop() override
Stop accepting work and join workers. Idempotent.
void schedule(Job *job) override
Kick a paused job so it can run.
TaskGroup * createFrameTaskGroup() override
Create a fork/join task group whose children are frame-scoped.
bool isRunning() const override
Whether the system is still accepting new jobs.
Job * createFrameJob(JobFunc body) override
Create a paused frame-scoped job.
void beginFrame() override
Start a new frame: join leftover frame jobs and recycle the arena.
Handle to a single job inside a JobSystem.
Definition JobSystem.h:41
Fork/join group: fork() spawns scheduled children, wait() joins them.
Definition JobSystem.h:106
A job executed by a ThreadPool worker. Status strings (no enums): "pending" | "running" | "done" | "f...
Definition Task.h:16
std::function< void(int first, int last)> ParallelForBody
Body of a parallel_for subrange.
Definition JobSystem.h:22
std::function< void()> JobFunc
A unit of work executed by a JobSystem worker.
Definition JobSystem.h:15
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
Definition Widget.cpp:387
Definition Build.cpp:11
JobImpl * createJobLocked(JobScope scope, JobFunc body, JobFunc onComplete)
void recordError(JobImpl *job, const std::string &message)