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
  • 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 freed 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.