Lifetimes
Question 1.
In order to be able to place a reference inside a structure, we need to ensure
that the reference outlives the structure. In particular, the task
reference
in schedule_mut
must outlive the scheduler instance. In order to express
this constraint, we need to add a lifetime parameter to the Scheduler
trait:
# #![allow(unused_variables)] #fn main() { /// The lifetime parameter 'a indicates the scheduler's lifetime. Only tasks /// which live longer than this lifetime can be scheduled. trait Scheduler<'a>: 'a { /// By taking a `&'a mut` reference to the task, we ensure that it can be /// stored inside the scheduler. fn schedule_mut(&mut self, task: &'a mut dyn TaskMut); } impl<'r> Scheduler<'r> for Runtime<'r> { /// Schedule a task by registering it into the runtime's `ready` queue. fn schedule_mut(&mut self, task: &'r mut dyn TaskMut) { self.ready.push(task) } } #}
Don't forget to add the lifetime in the TaskMut
trait definition as well:
# #![allow(unused_variables)] #fn main() { trait TaskMut: 'static { fn execute_mut<'a>(&mut self, scheduler: &mut dyn Scheduler<'a>); } #}
Question 2.
The 'a
parameter we added in the execute_mut
function could have been added
to the TaskMut
trait directly instead. However, this would make the nodes
usable with a single scheduler: we would like to avoid that if possible.
Question 3.
The trait definitions for this will still work; however the execute
function
from the previous question won't. We need to change it to the following:
# #![allow(unused_variables)] #fn main() { fn execute<'r>(tasks: &'r mut Vec<Box<dyn TaskMut + 'r>>) { let mut runtime = Runtime::new(tasks.iter_mut().map(Box::as_mut).collect()); runtime.execute(); } #}
This is required because Box<dyn TaskMut>
is implicitely converted to
Box<dyn TaskMut + 'static>
, i.e. trait objects as generic parameters contain
an implicit static lifetime. This is because we could implement TaskMut
for
a type which contain a lifetime, such as &'a i32
.
Question 4.
The compiler error about this is relatively explicit: we take an &mut
reference to self
, but then we try to schedule it with a &'r mut
reference.
Since the 'r
lifetime outlives the local function lifetime, we can't do this
(see also the flatten_ref
question in the first part)! One solution is to
simply use a &'r mut
lifetime when calling execute_mut
:
# #![allow(unused_variables)] #fn main() { trait TaskMut { fn execute_mut<'r>(&'r mut self, scheduler: &mut dyn Scheduler<'r>); } struct Loop; impl TaskMut for Loop { fn execute_mut<'r>(&'r mut self, scheduler: &mut dyn Scheduler<'r>) { scheduler.schedule_mut(self) } } #}
However, we can't use this to execute a loop's body and re-schedule the loop itself. Indeed, the API allows any node to re-schedule itself -- and in particular, the body could do so! In the following scenario:
- Loop executes
- Body executes and re-schedules itself: there is an
&'r mut
to the body in the scheduler
- Body executes and re-schedules itself: there is an
- Loop re-schedules ifself: there is an
&'r mut
to the loop in the scheduler... but we can get an&'r mut
to the body from that!
we can create two mutable references to the loop body at once, so there is no way we will fix that issue with lifetime annotations.
Question 5.
The only valid implementations are:
# #![allow(unused_variables)] #fn main() { impl<T: Task> TaskBox for Loop<T> { fn execute_box<'r>(self: Box<Self>, scheduler: &mut Scheduler<'r>) { self.body.execute(scheduler); scheduler.schedule_box(self); } } impl<T: TaskMut> TaskBox for Loop<T> { fn execute_box<'r>(mut self: Box<Self>, scheduler: &mut Scheduler<'r>) { self.body.execute_mut(scheduler); scheduler.schedule_box(self); } } #}
Conceptually, there is no difference between the TaskBox
trait and the
TaskMut
trait with &'r mut
arguments. In general, Box<T>
and &'r mut T
restrict what we can do with T
in the same way; the only difference is that
when the Box<T>
goes out of scope the memory will be free
d but nothing will
happen when &'r mut T
goes out of scope.
Question 6.
If we allow &'r
arguments, we can also write:
# #![allow(unused_variables)] #fn main() { impl<T: Task> Task for Loop<T> { fn execute<'r>(&'r self, scheduler: &mut Scheduler<'r>) { self.body.execute(scheduler); scheduler.schedule(self); } } #}
This is because by using an immutable reference we solve the issue described in the previous question: immutable references can be cloned, and it is not a problem to have multiple ones of them.