Reputation: 6856
I'llI was thinking about the rust async infrastructure and at the heart of the API lies the Future trait which is:
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
According to the docs
The core method of future,
poll
, attempts to resolve the future into a final value. This method does not block if the value is not ready. Instead, the current task is scheduled to be woken up when it’s possible to make further progress by polling again. The context passed to the poll method can provide aWaker
, which is a handle for waking up the current task.
This implies that the Context
value passed to .poll()
(particularly the waker) needs some way to refer to the pinned future &mut self
in order to wake it up. However &mut
implies that the reference is not aliased. Am I misunderstanding something or is this a "special case" where aliasing &mut
is allowed? If the latter, are there other such special cases besides UnsafeCell
?
Upvotes: 1
Views: 491
Reputation: 43842
needs some way to refer to the pinned future
&mut self
in order to wake it up.
No, it doesn't. The key thing to understand is that a “task” is not the future: it is the future and what the executor knows about it. What exactly the waker mutates is up to the executor, but it must be something that isn't the Future
. I say “must” not just because of Rust mutability rules, but because Future
s don't contain any state that says whether they have been woken. So, there isn't anything there to usefully mutate; 100% of the bytes of the future's memory are dedicated to the specific Future
implementation and none of the executor's business.
Well on the very next page if the book you will notice that task contains a boxed future and the waker is created from a reference to task. So there is a reference to future held from task albeit indirect.
OK, let's look at those data structures. Condensed:
struct Executor {
ready_queue: Receiver<Arc<Task>>,
}
struct Task {
future: Mutex<Option<BoxFuture<'static, ()>>>,
task_sender: SyncSender<Arc<Task>>,
}
The reference to the task is an Arc<Task>
, and the future itself is inside a Mutex
(interior mutability) in the task. Therefore,
&mut Task
from the Arc<Task>
, because Arc
doesn't allow that.future
is in a Mutex
which does run-time checking that there is at most one mutable reference to it.The only things you can do with an Arc<Task>
are
&
access to the future
in a Mutex
(which allows requesting run-time-checked mutation access to the Future
)&
access to the task_sender
(which allows sending things to ready_queue
).So, in this case, when the waker is called, it sort-of doesn't even mutate anything specific to the Task
at all: it makes a clone of the Arc<Task>
(which increments an atomic reference count stored next to the Task
) and puts it on the ready_queue
(which mutates storage shared between the Sender
and Receiver
).
Another executor might indeed have task-specific state in the Task
that is mutated, such as a flag marking that the task is already woken and doesn't need to be woken again. That flag might be stored in an AtomicBoolean
field in the task. But still, it does not alias with any &mut
of the Future
because it's not part of the Future
, but the task.
All that said, there actually is something special about Future
s and noalias — but it's not about executors, it's about Pin
. Pinning explicitly allows the pinned type to contain “self-referential” pointers into itself, so Rust does not declare noalias for Pin<&mut T>
. However, exactly what the language rules around this are is still not quite rigorously specified; the current situation is just considered a kludge so that async
functions can be correctly compiled, I think.
Upvotes: 3
Reputation: 26589
There is no such special case. Judging from your comment about the Rust executor, you are misunderstanding how interior mutability works.
The example in the Rust book uses an Arc
wrapped Task
structure, with the future contained in a Mutex
. When the task is run, it locks the mutex and obtains the singular &mut
reference that's allowed to exist.
Now look at how the example implements wake_by_ref
, and notice how it never touches the future
at all. Indeed, that function would not be able to lock the future at all, as the upper level already has the lock. It would not be able to safely get a &mut
reference, and so it prevents you from doing so, therefore, no issue.
The restriction for UnsafeCell
and its wrappers is that only one &mut
reference may exist for an object at any point in time. However, multiple &
immutable references may exist to the UnsafeCell
or structures containing it just fine - that is the point of interior mutability.
Upvotes: 2