我会考虑 rust 异步基础设施,API 的核心是 Future 特征,即:
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
根据文档
未来的核心方法,
poll
,试图将未来解析为最终值。如果值未就绪,则此方法不会阻止。相反,当前任务计划在可以通过再次轮询取得进一步进展时唤醒。传递给轮询方法的上下文可以提供Waker
,这是唤醒当前任务的句柄。
这意味着传递给.poll()
(特别是唤醒器)的Context
值需要某种方式来引用固定的未来&mut self
才能唤醒它。但是&mut
意味着引用没有锯齿。我是否误解了什么,或者这是允许混叠&mut
的"特殊情况"?如果是后者,除了UnsafeCell
之外,还有其他这样的特殊情况吗?
需要某种方法来引用固定的未来
&mut self
才能唤醒它。
不,它没有。要理解的关键是,"任务"不是未来:它是未来以及执行者对它的了解。唤醒者究竟变异了什么取决于执行者,但它一定不是Future
的东西。我说"必须"不仅仅是因为 Rust 可变性规则,还因为Future
不包含任何说明它们是否已被唤醒的状态。所以,那里没有什么可以有用的变异;未来内存的 100% 字节专用于特定的Future
实现,与执行者无关。
那么在下一页,如果这本书你会注意到该任务包含一个盒装的未来,并且唤醒器是从对任务的引用创建的。因此,从任务中提到了未来,尽管是间接的。
好的,让我们看一下这些数据结构。浓缩:
struct Executor {
ready_queue: Receiver<Arc<Task>>,
}
struct Task {
future: Mutex<Option<BoxFuture<'static, ()>>>,
task_sender: SyncSender<Arc<Task>>,
}
对任务的引用是一个Arc<Task>
,未来本身在任务的一个Mutex
(内部可变性)中。因此
- 从
Arc<Task>
获得&mut Task
,因为Arc
不允许这样做。 future
位于一个Mutex
中,该执行运行时检查是否最多有一个可变引用。
您唯一能用Arc<Task>
做的事情是
- 克隆并发送
&
访问Mutex
中的future
(允许请求对Future
进行运行时检查的突变访问)- 获得对
task_sender
&
的访问权限(允许将内容发送到ready_queue
)。
因此,在这种情况下,当唤醒器被调用时,它甚至根本不会改变任何特定于Task
的东西:它会克隆Arc<Task>
(增加存储在Task
旁边的原子引用计数)并将其放在ready_queue
上(这会改变Sender
和Receiver
之间共享的存储)。
另一个执行程序可能确实在Task
中具有特定于任务的状态,该状态已发生突变,例如标记任务已唤醒且不需要再次唤醒的标志。该标志可能存储在任务的AtomicBoolean
字段中。但是,它仍然没有与Future
的任何&mut
混叠,因为它不是Future
的一部分,而是任务。
综上所述,Future
s和noalias实际上有一些特别之处 - 但这不是关于执行者,而是关于Pin
。固定显式允许固定类型包含指向自身的"自引用"指针,因此 Rust 不会为Pin<&mut T>
声明 noalias。然而,围绕这一点的语言规则究竟是什么仍然没有严格规定;我认为,目前的情况只是被认为是一个难题,以便可以正确编译async
函数。
没有这样的特殊情况。从你对 Rust 执行器的评论来看,你误解了内部可变性是如何工作的。
Rust 书中的示例使用Arc
包装的Task
结构,未来包含在Mutex
中。运行任务时,它会锁定互斥锁并获取允许存在的单数&mut
引用。
现在看看该示例如何实现wake_by_ref
,并注意它如何从不触及future
。事实上,该功能根本无法锁定未来,因为上层已经锁定了。它将无法安全地获取&mut
引用,因此它会阻止您这样做,因此没有问题。
UnsafeCell
及其包装器的限制是,在任何时间点,一个对象只能存在一个&mut
引用。但是,可能存在多个&
不可变的引用,指向包含它的UnsafeCell
或结构 - 这就是内部可变性的重点。