Obligé d'utiliser Mutex lorsqu'il n'est pas nécessaire


Ailier Sendon

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 NetworkServeret Serverpartage 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 NetworkServeraccè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 HashMaps 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 HashMaps 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 Players 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 unsafeou 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();
}
Matthieu M.

Comme je Playersn'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 Playersdans 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.

Articles connexes


Y a-t-il un mal à utiliser Super lorsqu'il n'est pas nécessaire?

Rythme: Ce message concerne strictement Java. Si une méthode est dans une superclasse, la méthode peut être appelée de deux manières: foo(); super.foo(); Y a-t-il un mal à toujours faire ce dernier? En tant que style de codage, je préfère ce dernier car il es

LiveData Observer se déclenche lorsqu'il n'est pas nécessaire

Qrampus J'ai un modèle de vue qui reçoit le flux sous forme de données en direct du scénario val state get () = syncScenario.state.asLiveData () Dans l'activité, nous souscrivons à ces données en direct, une certaine logique se produit et utilise l'activitéRe