TL;DR:如何创建一个请求-响应系统,该系统具有多个结构,仅用一个线程以可变方式相互通信?
我正在为业余爱好项目开发 NES 模拟器。我想在项目的层次结构和类型中表示硬件结构。所以我有代表每个芯片的结构。
在真实的硬件中,它们将通过电线进行通信并对信号做出反应,从而改变它们自己的状态。然而,这意味着共享可变状态。
目前的struct层次结构是这样的。
NES
拥有以下所有内容:
NES:
- CPU6502
- SystemBus
- PPU
- Memory
- CatridgeConnector
- some other chips
使用
Rc<RefCell<T>>
共享可变引用是第一种方法,它确实有效。但它引入了如此多的样板代码,我认为这是瓶颈的一部分(尽管只是预感)。这并不能很好地反映硬件结构。每个芯片不应该关心它连接到什么。我想避免在结构字段中提及其他芯片的类型。
所以想法是这样的:例如,当
CPU6502
想要与Memory
通信时,它会返回消息,表示它想要访问特定位置的内存。然而,这需要大量的重构。
在当前层次结构中,NES
将调用CPU6502
中的函数来执行循环。假设它返回消息给 NES
,那么 NES
就会命令 Memory
做它的事情,依此类推。
但是
CPU6502
可以在单个函数调用中多次从内存中读取。每次需要与其他芯片通信时都返回NES
是不可能的。这种行为在真实硬件中并不是 100% 准确,但现在就是这样。
我当前尝试的方法是使用bi Direction_channel。如果
CPU6502
向 Memory
发送请求,它应该以 u8
进行响应。但是我只有一个线程,没有任何东西可以处理请求。我可以使用多个线程,但这似乎有点矫枉过正。它只是从字节缓冲区读取特定索引处的单个值。处理多个线程,即使它像 tokio 一样轻量级,与仅执行此操作的小型操作相比也会更昂贵:self.buffer[address - start]
。
那只是阅读。
CPU6502
也需要写入Memory
,因此可变性也是必要的。
以上只是示例。每个芯片之间的所有通信都需要非常小的操作,例如设置单个u16,对u8进行按位操作。所以我认为多线程不是一个好主意。
您可以通过回调拥有类似通道的功能:通过类型擦除,您可以避免让每个组件了解其他组件,并且通过将组件访问集中在
Nes
下,您可以避免 Rc
。 RefCell
更难摆脱。
use std::cell::RefCell;
pub struct Requester<Params, Ret> {
responder: fn(&Nes, Params) -> Ret,
}
impl<Params, Ret> Requester<Params, Ret> {
pub fn new(responder: fn(&Nes, Params) -> Ret) -> Self {
Self { responder }
}
pub fn request(&self, nes: &Nes, params: Params) -> Ret {
(self.responder)(nes, params)
}
}
pub struct Address(pub u8);
pub struct Cpu6502 {
memory_requester: Requester<Address, u8>,
}
impl Cpu6502 {
pub fn new(memory_requester: Requester<Address, u8>) -> Self {
Self { memory_requester }
}
pub fn execute_cycle(&mut self, nes: &Nes) {
let data = self.memory_requester.request(nes, Address(0));
}
}
pub struct Memory {}
impl Memory {
pub fn new() -> Self {
Self {}
}
pub fn request_address(&self, nes: &Nes, addr: Address) -> u8 {
unimplemented!()
}
}
pub struct Nes {
cpu6502: RefCell<Cpu6502>,
memory: RefCell<Memory>,
}
impl Nes {
pub fn new() -> Self {
let cpu6502 = RefCell::new(Cpu6502::new(Requester::new(|nes, addr| {
nes.memory.borrow().request_address(nes, addr)
})));
let memory = RefCell::new(Memory::new());
Self { cpu6502, memory }
}
pub fn execute_cycle(&self) {
self.cpu6502.borrow_mut().execute_cycle(self);
}
}
如果组件仅需要对其所有操作共享引用,则可以避免将其包装在
RefCell
中。