我正在编写游戏并且玩家列表定义如下:
pub struct PlayerList {
by_name: HashMap<String, Arc<Mutex<Player>>>,
by_uuid: HashMap<Uuid, Arc<Mutex<Player>>>,
}
此结构包含添加,删除,获取玩家和获取玩家数量的方法。
NetworkServer
和Server
分享此列表如下:
NetworkServer {
...
player_list: Arc<Mutex<PlayerList>>,
...
}
Server {
...
player_list: Arc<Mutex<PlayerList>>,
...
}
这是在Arc<Mutex>
内部,因为NetworkServer
访问不同线程中的列表(网络循环)。
当玩家加入时,会为他们生成一个线程并将它们添加到player_list中。
虽然我唯一的操作是添加到player_list
,但我强制使用Arc<Mutex<Player>>
代替{{1}中更自然的Rc<RefCell<Player>>
因为HashMap
需要它。我不是从网络线程(或任何其他线程)访问玩家,因此将它们放在Mutex<PlayerList>
下是没有意义的。只需要锁定Mutex
,我正在使用HashMap
进行锁定。但鲁斯特很迂腐,想要防止所有的误用。
由于我只访问主线程中的Mutex<PlayerList>
,因此每次执行此操作时锁定既烦人且性能较差。是否有解决方法而不是使用Player
或其他东西?
以下是一个例子:
unsafe
答案 0 :(得分:7)
因为我只是在主线程中访问
Players
,所以每次锁定都会令人烦恼且性能不佳。
你的意思是,现在你只是在主线程中访问Players
,但是在以后的任何时候你可能会意外地在另一个线程中引入对它们的访问权限?
从语言的角度来看,如果您可以获得对值的引用,则可以使用该值。因此,如果多个线程具有对值的引用,则应该可以安全地从多个线程使用此值。在编译时,没有办法强制执行特定值,尽管可以访问,但实际上从未使用过。
然而,这提出了一个问题:如果给定线程从不使用该值,为什么该线程首先可以访问它?
在我看来,您有设计问题。如果您可以设法重新设计程序,以便只有主线程可以访问PlayerList
,那么您将立即能够使用Rc<RefCell<...>>
。
例如,您可以让网络线程向主线程发送消息,宣布新玩家已连接。
目前,您正在“通过共享进行通信”,您可以转而使用“通过通信共享”。前者通常在整个地方都有同步原语(如互斥,原子,......),可能会遇到争用/死锁问题,而后者通常有通信队列(通道),需要“异步”风格编程。
答案 1 :(得分:1)
Send
是一种标记特征,用于管理哪些对象可以跨线程边界传输所有权。对于完全由Send
类型组成的任何类型,它都会自动实现。它也是一个不安全的特性,因为手动实现这个特性会导致编译器不能强制执行我们喜欢Rust的并发安全性。
问题在于Rc<RefCell<Player>>
不是Send
因此PlayerList
不是Send
因此无法成为发送到另一个线程,即使包裹在Arc<Mutex<>>
中。对于unsafe
结构,unsafe impl Send
解决方法是PlayerList
。
将此代码放入您的游乐场示例中,可以使用与Arc<Mutex<Player>>
struct PlayerList {
by_name: HashMap<String, Rc<RefCell<Player>>>,
by_uuid: HashMap<Uuid, Rc<RefCell<Player>>>,
}
unsafe impl Send for PlayerList {}
impl PlayerList {
fn add_player(&mut self, p: Player) {
let name = p.name.clone();
let uuid = p.uuid;
let p = Rc::new(RefCell::new(p));
self.by_name.insert(name, Rc::clone(&p));
self.by_uuid.insert(uuid, p);
}
}
Nomicon遗憾地解释了当程序员为包含Send
的类型不安全地实施Rc
时必须强制执行哪些规则,但只能访问一个线程似乎足够安全......
答案 2 :(得分:1)
我建议使用多发送器 - 单接收器通道解决此线程问题。网络线程获得Sender<Player>
并且无法直接访问播放器列表。
Receiver<Player>
存储在PlayerList
内。访问PlayerList
的唯一线程是主线程,因此您可以删除它周围的Mutex
。而是在用于锁定mutexit的主线程将Receiver<Player>
中的所有待处理玩家队列化的位置,将它们包装在Rc<RefCell<>>
中并将它们添加到适当的集合中。
虽然看着更大的设计,但我不会首先使用每个玩家的线程。相反,我会使用某种单线程基于事件循环的设计。 (我没看过哪个Rust库在那个区域很好,但是tokio似乎很受欢迎)