Fix reuse after free and API inconsistency in job system (#446)
* Fix a reuse after free in the job system Jobs were destroyed and recycled while still in use by wait() or run(). To fix this we introduce reference-counting of jobs. Jobs start with a ref-count of 1, which is decremented when a job naturally finishes. Additionally, all user-facing methods acquire a reference for the duration of the call. * Fix an API inconsistency with JobSystem JobSystem's API lets the user create jobs but not destroy them. Jobs are destroyed automatically, without a way for the caller to know when that happens. We now explicitly enforce that jobs are no longer valid when wait() returns. Multiple concurrent wait() are allowed however. This is enforced by clearing the job pointer upon returning from JobSystem::wait(Job* job). * Rename linked-list put/get to push/pop * Better fix for Job use after free There was still a race condition where a run()'ed job could be destroyed before wait() was called, wait would then use a destroyed object. The available APIs now are: run() - runs and destroys a job runAndWait() - run, then waits for and destroys a job runAndRetain() - runs and keep a reference to the job wait() - waits and destroys a job wait() can only be used with a job obtained with runAndRetain(). * Get rid of unused code This version of parallel_for has use-after-free issues anyways, since we changed the semantics of run/wait/etc... * Fix decRef() memory order decRef() must ensure that all access to the object have happened before destroying it. * Fix memory order in atomic linked list's pop() It needs acquire semantic, since we want to make sure that no read/write are reordered before the pop() -- which returns an object to the caller. * Fix memory order on runningJobCount we needed acquire semantic when about to destroy the last job -- it's similar to decRef. * Comment usages of std::memory_order_* * Fix AtomicFreeList A-B-A bug Turns out AtomicFreeList was not immune to the ABA bug. W're fixing it here by using a 64-bits CAS, which is available on aarch64 and armv7.
This commit is contained in:
@@ -34,14 +34,6 @@
|
||||
#include <utils/ThreadLocal.h>
|
||||
#include <utils/WorkStealingDequeue.h>
|
||||
|
||||
#ifdef WIN32
|
||||
// Size is chosen so that we can store at least std::function<> and a job size is a multiple of a
|
||||
// cacheline.
|
||||
# define JOB_PADDING (6+8)
|
||||
#else
|
||||
# define JOB_PADDING (6)
|
||||
#endif
|
||||
|
||||
namespace utils {
|
||||
|
||||
class JobSystem {
|
||||
@@ -60,22 +52,25 @@ public:
|
||||
Job(const Job&) = delete;
|
||||
Job(Job&&) = delete;
|
||||
|
||||
void* getData() { return padding; }
|
||||
void const* getData() const { return padding; }
|
||||
void* getData() { return storage; }
|
||||
void const* getData() const { return storage; }
|
||||
private:
|
||||
friend class JobSystem;
|
||||
|
||||
// Size is chosen so that we can store at least std::function<>, the alignas() qualifier
|
||||
// ensures we're multiple of a cache-line.
|
||||
static constexpr size_t JOB_STORAGE_SIZE = (sizeof(std::function<void()>) + sizeof(void*) - 1) / sizeof(void*);
|
||||
|
||||
// keep it first, so it's correctly aligned with all architectures
|
||||
// this is were we store the job's data, typically a std::function
|
||||
void* storage[JOB_STORAGE_SIZE];
|
||||
|
||||
JobFunc function;
|
||||
uint16_t parent;
|
||||
std::atomic<uint16_t> runningJobCount = { 0 };
|
||||
// on 64-bits systems, there is an extra 32-bits lost here
|
||||
void* padding[JOB_PADDING];
|
||||
std::atomic<uint16_t> runningJobCount = { 1 };
|
||||
mutable std::atomic<uint16_t> refCount = { 1 };
|
||||
};
|
||||
|
||||
static_assert(
|
||||
(sizeof(Job) % CACHELINE_SIZE == 0) ||
|
||||
(CACHELINE_SIZE % sizeof(Job) == 0),
|
||||
"A Job must be N cache-lines long or N Jobs must fit in a cache line exactly.");
|
||||
|
||||
explicit JobSystem(size_t threadCount = 0, size_t adoptableThreadsCount = 1) noexcept;
|
||||
|
||||
~JobSystem();
|
||||
@@ -177,7 +172,7 @@ public:
|
||||
};
|
||||
Job* job = create(parent, &stub::call);
|
||||
if (job) {
|
||||
job->padding[0] = data;
|
||||
job->storage[0] = data;
|
||||
}
|
||||
return job;
|
||||
}
|
||||
@@ -185,7 +180,7 @@ public:
|
||||
// creates a job from a KNOWN method pointer w/ object passed by value
|
||||
template<typename T, void(T::*method)(JobSystem&, Job*)>
|
||||
Job* createJob(Job* parent, T data) noexcept {
|
||||
static_assert(sizeof(data) <= sizeof(Job::padding), "user data too large");
|
||||
static_assert(sizeof(data) <= sizeof(Job::storage), "user data too large");
|
||||
struct stub {
|
||||
static void call(void* user, JobSystem& js, Job* job) noexcept {
|
||||
T* that = static_cast<T*>(user);
|
||||
@@ -195,7 +190,7 @@ public:
|
||||
};
|
||||
Job* job = create(parent, &stub::call);
|
||||
if (job) {
|
||||
new(job->padding) T(std::move(data));
|
||||
new(job->storage) T(std::move(data));
|
||||
}
|
||||
return job;
|
||||
}
|
||||
@@ -203,7 +198,7 @@ public:
|
||||
// creates a job from a functor passed by value
|
||||
template<typename T>
|
||||
Job* createJob(Job* parent, T functor) noexcept {
|
||||
static_assert(sizeof(functor) <= sizeof(Job::padding), "functor too large");
|
||||
static_assert(sizeof(functor) <= sizeof(Job::storage), "functor too large");
|
||||
struct stub {
|
||||
static void call(void* user, JobSystem& js, Job* job) noexcept {
|
||||
T& that = *static_cast<T*>(user);
|
||||
@@ -213,7 +208,7 @@ public:
|
||||
};
|
||||
Job* job = create(parent, &stub::call);
|
||||
if (job) {
|
||||
new(job->padding) T(std::move(functor));
|
||||
new(job->storage) T(std::move(functor));
|
||||
}
|
||||
return job;
|
||||
}
|
||||
@@ -221,17 +216,32 @@ public:
|
||||
// Add job to this thread's execution queue.
|
||||
// Current thread must be owned by JobSystem's thread pool. See adopt().
|
||||
enum runFlags { DONT_SIGNAL = 0x1 };
|
||||
void run(Job* job, uint32_t flags = 0) noexcept;
|
||||
void run(Job*& job, uint32_t flags = 0) noexcept;
|
||||
|
||||
// Wait on a job.
|
||||
// This version allow a call such as run(createJob(...));
|
||||
void run(Job*&& job, uint32_t flags = 0) noexcept {
|
||||
Job* p = job;
|
||||
run(p);
|
||||
}
|
||||
|
||||
// run a job and keep a reference to it. This job MUST BE waited on with wait().
|
||||
Job* runAndRetain(Job* job, uint32_t flags = 0) noexcept;
|
||||
|
||||
// Wait on a job and destroys it. The job must first be obtained from runAndRetain().
|
||||
// Current thread must be owned by JobSystem's thread pool. See adopt().
|
||||
void wait(Job const* job) noexcept;
|
||||
void wait(Job*& job) noexcept;
|
||||
|
||||
void runAndWait(Job* job) noexcept {
|
||||
run(job);
|
||||
void runAndWait(Job*& job) noexcept {
|
||||
runAndRetain(job);
|
||||
wait(job);
|
||||
}
|
||||
|
||||
// This version allow a call such as runAndWait(createJob(...));
|
||||
void runAndWait(Job*&& job) noexcept {
|
||||
Job* p = job;
|
||||
runAndWait(p);
|
||||
}
|
||||
|
||||
// jobs are normally finished automatically, this can be used to cancel a job
|
||||
// before it is run.
|
||||
void finish(Job* job) noexcept;
|
||||
@@ -284,11 +294,26 @@ private:
|
||||
uint32_t mask;
|
||||
};
|
||||
|
||||
class Pin {
|
||||
JobSystem& js;
|
||||
Job const* job;
|
||||
public:
|
||||
Pin(JobSystem& js, Job const* job) noexcept : js(js), job(job) {
|
||||
js.incRef(job);
|
||||
}
|
||||
~Pin() noexcept {
|
||||
js.decRef(job);
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(sizeof(ThreadState) % CACHELINE_SIZE == 0,
|
||||
"ThreadState doesn't align to a cache line");
|
||||
|
||||
static ThreadState& getState() noexcept;
|
||||
|
||||
void incRef(Job const* job) noexcept;
|
||||
void decRef(Job const* job) noexcept;
|
||||
|
||||
Job* create(Job* parent, JobFunc func) noexcept;
|
||||
Job* allocateJob() noexcept;
|
||||
JobSystem::ThreadState& getStateToStealFrom(JobSystem::ThreadState& state) noexcept;
|
||||
@@ -512,38 +537,6 @@ JobSystem::Job* parallel_for(JobSystem& js, JobSystem::Job* parent,
|
||||
return js.createJob<JobData, &JobData::parallelWithJobs>(parent, std::move(jobData));
|
||||
}
|
||||
|
||||
|
||||
// parallel jobs with start/count indices + sequential 'reduce'
|
||||
template<typename S, typename F, typename R>
|
||||
JobSystem::Job* parallel_for(JobSystem& js, JobSystem::Job* parent,
|
||||
uint32_t start, uint32_t count, F functor, const S& splitter, R finish) noexcept {
|
||||
using JobData = details::ParallelForJobData<S, F>;
|
||||
JobData jobData(start, count, 0, std::move(functor), splitter);
|
||||
auto wrapper = js.createJob(parent, [jobData, finish](JobSystem& js, JobSystem::Job* p) {
|
||||
auto parallelJob = js.createJob<JobData, &JobData::parallelWithJobs>(p, std::move(jobData));
|
||||
js.runAndWait(parallelJob);
|
||||
finish(js, parallelJob);
|
||||
});
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// parallel jobs with pointer/count + sequential 'reduce'
|
||||
template<typename T, typename S, typename F, typename R>
|
||||
JobSystem::Job* parallel_for(JobSystem& js, JobSystem::Job* parent,
|
||||
T* data, uint32_t count, F functor, const S& splitter, R finish) noexcept {
|
||||
auto user = [data, f = std::move(functor)](uint32_t s, uint32_t c) {
|
||||
f(data + s, c);
|
||||
};
|
||||
using JobData = details::ParallelForJobData<S, decltype(user)>;
|
||||
JobData jobData(0, count, 0, std::move(user), splitter);
|
||||
auto wrapper = js.createJob(parent, [jobData, finish](JobSystem& js, JobSystem::Job* p) {
|
||||
auto parallelJob = js.createJob<JobData, &JobData::parallelWithJobs>(p, std::move(jobData));
|
||||
js.runAndWait(parallelJob);
|
||||
finish(js, parallelJob);
|
||||
});
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// parallel jobs on a Slice<>
|
||||
template<typename T, typename S, typename F>
|
||||
JobSystem::Job* parallel_for(JobSystem& js, JobSystem::Job* parent,
|
||||
@@ -551,13 +544,6 @@ JobSystem::Job* parallel_for(JobSystem& js, JobSystem::Job* parent,
|
||||
return parallel_for(js, parent, slice.data(), slice.size(), functor, splitter);
|
||||
}
|
||||
|
||||
// parallel jobs on a Slice<> + sequential 'reduce'
|
||||
template<typename T, typename S, typename F, typename R>
|
||||
JobSystem::Job* parallel_for(JobSystem& js, JobSystem::Job* parent,
|
||||
utils::Slice<T> slice, F functor, const S& splitter, R finish) noexcept {
|
||||
return parallel_for(js, parent, slice.data(), slice.size(), functor, splitter, finish);
|
||||
}
|
||||
|
||||
|
||||
template <size_t COUNT, size_t MAX_SPLITS = 12>
|
||||
class CountSplitter {
|
||||
|
||||
Reference in New Issue
Block a user