Expand description
Lockless Work Stealing Job System.
§Features
- Lockless work stealing queue
- 0 runtime allocations
- Jobs are executed from closures
- Chaining and grouping of jobs
- Parallel for_each abstraction
§General Notes
This crate provides job system with 0 allocation overhead at runtime. What this means in practice is, that once the instance is created, there are no more memory allocations required to execute jobs.
Jobs are allocated from a Job Pool with a fixed capacity. Each thread gets their own pool and queue. The system uses a fixed storage space where it stores the closure for each job. The default storage is set to 64 bytes. This can be extended to 128 by enabling the feature “job_storage_128”.
When a Job finishes execution there is a possibility to start other jobs. See JobScope::chain() for more details.
Additionally, it’s recommended that, in order to avoid running out of jobs during execution, you regularly ensure that you all your previous jobs have finished executing.
Finally, this crate runs on rust stable and has been tested with rust 1.50.0 with the 2018 edition.
§Safety
Due to the implementation performing type erasure to store the closures in the pre-allocated space, I have not been able to figure out a way to ensure the compiler is aware of this so it can enforce lifetime and ownership checks. Until that is addressed JobScope::create, JobScope::create_with_parent and JobScope::run are marked as unsafe and is up to caller to guarantee that they can enforce their safty requirements.
§Panics
Due to the queues having a fixed size, if we start filling all the queues up to full capacity there is a small chance that a worker thread may not be able to queue chained jobs. When this happens the system will panic as there is no way to recover from this. The queues come with debug asserts which will detect this situation.
§Examples
§Safe
These example provide safe variants that uphold the safety requirements.
§Parallel
See for_each() and for_each_with_result() for more details.
let mut job_sys = jobsys::JobSystem::new(4, 512).unwrap();
let job_scope = jobsys::JobScope::new_from_system(&job_sys);
let mut array = [0_u32; 100];
job_scope.for_each(&mut array, |slice: &mut [u32], start, _end| {
for i in 0..slice.len() {
slice[i] = (start + i) as u32;
}
}).expect("Failed to start jobs");
§Single Work Item
See JobInstance for more details.
let job_sys = jobsys::JobSystem::new(2, 128).expect("Failed to init job system");
let job_scope = jobsys::JobScope::new_from_system(&job_sys);
let job_instance = jobsys::JobInstance::create(&job_scope, || {
println!("Hello from Job Instance");
}).unwrap();
job_instance.wait_with(|| println!("Waiting on Job to Finish")).expect("Failed to wait on job");
§Unsafe
The following example are unsafe due to certain requirements that need to be upheld by the caller but can be used as building block for safer alternatives.
§Start and Wait
let job_sys = jobsys::JobSystem::new(4, 512).unwrap();
let job_scope = jobsys::JobScope::new_from_system(&job_sys);
let mut handle = unsafe{job_scope.create(|| { println!("Hello World!");}).unwrap()};
unsafe{job_scope.run(&mut handle).expect("Failed to run job");}
job_scope.wait(&mut handle);
§Grouping
Ensure one job does not complete until other jobs have finished as well.
let job_sys= jobsys::JobSystem::new(4, 512).unwrap();
let job_scope = jobsys::JobScope::new_from_system(&job_sys);
let mut parent = job_scope.create_noop().unwrap(); // Create a job that does nothing
let mut child = unsafe{job_scope.create_with_parent(&mut parent, || { println!("Hello World!");}).unwrap()};
unsafe{job_scope.run(&child).expect("Failed to start child");}
unsafe{job_scope.run(&parent).expect("Failed to start parent");}
job_scope.wait(&parent); // Parent will only finish when both it and its child have finished
§Chaining
Launch new jobs as soon as one job completes.
let job_sys = jobsys::JobSystem::new(4, 512).unwrap();
let job_scope = jobsys::JobScope::new_from_system(&job_sys);
let mut first = unsafe{job_scope.create(|| { println!("Hello World Chained!");})}.unwrap();
let mut second = unsafe{job_scope.create(|| { println!("Hello World Chained!");})}.unwrap();
job_scope.chain(&mut first, &second).expect("Failed to chain job, maximum chain count exceeded");
unsafe{job_scope.run(&first).expect("Failed to start job");}
job_scope.wait(&second); // Second will only be executed after first completes
§Creating jobs on job threads
let job_sys = jobsys::JobSystem::new(4, 512).unwrap();
let job_scope = jobsys::JobScope::new_from_system(&job_sys);
let mut handle = unsafe{job_scope.create(|| {
let thread_job_scope = jobsys::JobScope::new_from_thread().unwrap();
let thread_job_handle = thread_job_scope.create(|| {
println!("Created on job thread");
}).unwrap();
thread_job_scope.run(&thread_job_handle).expect("Failed to run job");
}).unwrap()};
unsafe{job_scope.run(&mut handle).expect("Failed to run job");}
job_scope.wait(&mut handle);
Structs§
- JobInstance provides a safe interface to schedule a Job and ensures waits until the job has finished execution before it goes out of scope.
- Manages the creation and scheduling of jobs. This type can either be created from a JobSystem instance or retrieved from a worker thread. If you tried to use this with a thread that is not being tracked by the job system, all the functions will fail with
Error::InvalidThread
. - Work Stealing JobSystem.
- Handle which represents an allocated job.