不需要时强制使用 Mutex

Forced to use of Mutex when it's not required

我正在编写一个游戏,玩家列表定义如下:

pub struct PlayerList {
    by_name: HashMap<String, Arc<Mutex<Player>>>,
    by_uuid: HashMap<Uuid, Arc<Mutex<Player>>>,
}

这个结构有添加、删除、获取玩家和获取玩家数量的方法。

NetworkServerServer 共享此列表如下:

NetworkServer {
    ...
    player_list: Arc<Mutex<PlayerList>>,
    ...
}

Server {
    ...
    player_list: Arc<Mutex<PlayerList>>,
    ...
}

这是在 Arc<Mutex> 中,因为 NetworkServer 在不同的线程(网络循环)中访问列表。
当玩家加入时,会为他们生成一个线程并将他们添加到 player_list。

虽然我正在做的唯一操作是添加到 player_list,但我不得不使用 Arc<Mutex<Player>> 而不是 HashMap 中更自然的 Rc<RefCell<Player>>这是因为 Mutex<PlayerList> 需要它。我没有从网络线程(或任何其他线程)访问播放器,因此将它们放在 Mutex 下是没有意义的。只有 HashMap 需要锁定,我正在使用 Mutex<PlayerList> 进行锁定。但是 Rust 很迂腐,想要防止所有的误用。

因为我只在主线程中访问 Players,所以每次都进行锁定既烦人又性能低下。有没有解决方法而不是使用 unsafe 或其他方法?

这是一个例子:

use std::cell::Cell;
use std::collections::HashMap;
use std::ffi::CString;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::thread;

#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct Uuid([u8; 16]);

struct Player {
    pub name: String,
    pub uuid: Uuid,
}

struct PlayerList {
    by_name: HashMap<String, Arc<Mutex<Player>>>,
    by_uuid: HashMap<Uuid, Arc<Mutex<Player>>>,
}

impl PlayerList {
    fn add_player(&mut self, p: Player) {
        let name = p.name.clone();
        let uuid = p.uuid;

        let p = Arc::new(Mutex::new(p));
        self.by_name.insert(name, Arc::clone(&p));
        self.by_uuid.insert(uuid, p);
    }
}

struct NetworkServer {
    player_list: Arc<Mutex<PlayerList>>,
}

impl NetworkServer {
    fn start(&mut self) {
        let player_list = Arc::clone(&self.player_list);
        thread::spawn(move || {
            loop {
                // fake network loop
                // listen for incoming connections, accept player and add them to player_list.
                player_list.lock().unwrap().add_player(Player {
                    name: "blahblah".into(),
                    uuid: Uuid([0; 16]),
                });
            }
        });
    }
}

struct Server {
    player_list: Arc<Mutex<PlayerList>>,
    network_server: NetworkServer,
}

impl Server {
    fn start(&mut self) {
        self.network_server.start();
        // main game loop
        loop {
            // I am only accessing players in this loop in this thread. (main thread)
            // so Mutex for individual player is not needed although rust requires it.
        }
    }
}

fn main() {
    let player_list = Arc::new(Mutex::new(PlayerList {
        by_name: HashMap::new(),
        by_uuid: HashMap::new(),
    }));
    let network_server = NetworkServer {
        player_list: Arc::clone(&player_list),
    };
    let mut server = Server {
        player_list,
        network_server,
    };
    server.start();
}

Send 是一个标记特征,它控制哪些对象可以跨线程边界转移所有权。对于完全由 Send 类型组成的任何类型,它会自动实现。这也是一个不安全的特征,因为手动实现这个特征会导致编译器不强制执行我们喜欢 Rust 的并发安全。

问题是 Rc<RefCell<Player>> 不是 Send,因此您的 PlayerList 不是 Send,因此无法 发送 到另一个线程,即使包裹在 Arc<Mutex<>> 中也是如此。 unsafe 解决方法是 unsafe impl Send 用于您的 PlayerList 结构。

将此代码放入您的 playground 示例中可以使其编译方式与使用 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);
    }
}

Playground

遗憾的是,Nomicon 很少解释程序员在不安全地为包含 Rc 的类型实现 Send 时必须执行哪些规则,但在只有一个线程似乎足够安全...

为了完整性,here's TRPL's bit on Send and Sync

As I'm only accessing Players in the main thread, locking everytime to do that is both annoying and less performant.

你的意思是,现在你只在主线程中访问 Players,但在以后的任何时候你可能会不小心在另一个线程中引入对它们的访问?

从语言的角度来看,如果你能得到一个值的引用,你就可以使用这个值。因此,如果多个线程引用了一个值,那么这个值应该可以安全地从多个线程使用。无法在编译时强制执行某个特定值,尽管可以访问,但实际上从未使用过。

然而,这提出了问题:

如果给定线程从未使用过该值,为什么该线程首先可以访问它?

在我看来,您遇到了 设计 问题。如果您可以设法重新设计您的程序,以便只有主线程可以访问 PlayerList,那么您将立即能够使用 Rc<RefCell<...>>.

例如,您可以改为让网络线程向主线程发送一条消息,宣布新玩家已连接。

目前,您是 "Communicating by Sharing",您可以转向 "Sharing by Communicating"。前者通常到处都有同步原语(例如互斥体、原子、...),并且可能面临 contention/dead-lock 问题,而后者通常有通信队列(通道)并需要 "asynchronous"编程风格。

我建议使用多发送者单接收者通道来解决这个线程问题。网络线程获得 Sender<Player> 并且无法直接访问播放器列表。

Receiver<Player> 存储在 PlayerList 中。访问 PlayerList 的唯一线程是主线程,因此您可以删除它周围的 Mutex。相反,在主线程用于锁定互斥锁的地方,所有待处理的玩家从 Receiver<Player> 中出队,将它们包装在 Rc<RefCell<>> 中并将它们添加到适当的集合中。


尽管着眼于更大的设计,但我一开始不会使用针对每个玩家的线程。相反,我会使用某种基于单线程事件循环的设计。 (我没有研究哪些 Rust 库在那个领域很好,但 tokio 似乎很受欢迎)