Obligé d'utiliser Mutex lorsqu'il n'est pas nécessaire
J'écris un jeu et j'ai une liste de joueurs définie comme suit:
pub struct PlayerList {
by_name: HashMap<String, Arc<Mutex<Player>>>,
by_uuid: HashMap<Uuid, Arc<Mutex<Player>>>,
}
Cette structure a des méthodes pour ajouter, supprimer, obtenir des joueurs et obtenir le nombre de joueurs.
Le NetworkServer
et Server
partage cette liste comme suit:
NetworkServer {
...
player_list: Arc<Mutex<PlayerList>>,
...
}
Server {
...
player_list: Arc<Mutex<PlayerList>>,
...
}
C'est à l'intérieur d'un Arc<Mutex>
car NetworkServer
accède à la liste dans un thread différent (boucle réseau).
Lorsqu'un joueur se joint, un fil de discussion est créé pour lui et il est ajouté à la player_list.
Bien que la seule opération que je fais soit l'ajout player_list
, je suis obligé d'utiliser Arc<Mutex<Player>>
au lieu de la plus naturelle Rc<RefCell<Player>>
dans le HashMap
s parce Mutex<PlayerList>
que cela l'exige. Je n'accède pas aux joueurs à partir du thread réseau (ou de tout autre thread), donc cela n'a aucun sens de les mettre sous un Mutex
. Seuls les HashMap
s doivent être verrouillés, ce que je fais en utilisant Mutex<PlayerList>
. Mais Rust est pédant et veut se protéger de tous les abus.
Comme je n'accède qu'aux Player
s dans le thread principal, verrouiller à chaque fois pour le faire est à la fois ennuyeux et moins performant. Existe-t-il une solution de contournement au lieu d'utiliser unsafe
ou quelque chose?
Voici un exemple:
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();
}
Comme je
Players
n'accède que dans le fil principal, verrouiller à chaque fois pour le faire est à la fois ennuyeux et moins performant.
Vous voulez dire, comme pour le moment vous n'accédez que Players
dans le thread principal, mais à tout moment plus tard, vous pouvez accidentellement y introduire un accès dans un autre thread?
Du point de vue de la langue, si vous pouvez obtenir une référence à une valeur, vous pouvez utiliser la valeur. Par conséquent, si plusieurs threads ont une référence à une valeur, cette valeur doit pouvoir être utilisée en toute sécurité à partir de plusieurs threads. Il n'y a aucun moyen d'imposer, au moment de la compilation, qu'une valeur particulière, bien qu'accessible, ne soit en fait jamais utilisée.
Cela soulève cependant la question:
Si la valeur n'est jamais utilisée par un thread donné, pourquoi ce thread y a-t-il accès en premier lieu?
Il me semble que vous avez un problème de conception . Si vous parvenez à repenser votre programme pour que seul le thread principal ait accès au PlayerList
, vous pourrez immédiatement l'utiliser Rc<RefCell<...>>
.
Par exemple, vous pouvez demander au thread réseau d'envoyer un message au thread principal annonçant qu'un nouveau joueur est connecté.
Pour le moment, vous "Communiquez en partageant", et vous pourriez passer à "Partager en communiquant" à la place. Le premier a généralement des primitives de synchronisation (comme les mutex, les atomes, ...) partout, et peut faire face à des problèmes de contention / dead-lock, tandis que le second a généralement des files d'attente de communication (canaux) et nécessite un style "asynchrone" de programmation.