Introduction : pourquoi un singleton global en Rust est-il un défi ?
\n\nSi vous venez du monde Python, JavaScript ou C++, vous êtes habitué à un simple objet global. Vous déclarez une variable au niveau du module – et elle est à vous. En Rust, cette astuce ne fonctionne pas. Le compilateur surveille strictement la durée de vie et l'accès aux données, surtout si elles peuvent être modifiées depuis différents threads. C'est pourquoi sur Stack Overflow, la question « How do I create a global, mutable singleton? » a recueilli des milliers de vues et des dizaines de réponses complexes.
\n\nMais ne vous inquiétez pas. Le singleton en Rust n'est pas une douleur, mais une discipline. Aujourd'hui, nous allons voir comment créer un singleton mutable global de manière sécurisée, efficace et en comprenant ce qui se passe. Vous apprendrez pourquoi static mut est un mal, et quels outils (lazy_static, OnceCell, Mutex) nous donnent le contrôle.
Cet article est écrit pour le site éducatif pythonlib.ru et adapte une question réelle de SO en leçon pratique. C'est parti !
\n\nLe problème : qu'est-ce qu'un singleton mutable global ?
\n\nSingleton est un modèle de conception où une classe ou une structure a exactement une instance dans tout le programme. Dans le contexte de Rust, nous parlons d'une variable globale accessible depuis n'importe où dans le code, que l'on peut modifier (mut).
\n\nExemples d'utilisation :
\n- \n
- Configuration de l'application, chargée une fois au démarrage. \n
- Logger global ou compteur d'événements. \n
- Pool de connexions à une base de données. \n
- Cache en mémoire (in-memory cache). \n
Le problème est que Rust interdit les variables globales mutables sans wrapper spécial. Pourquoi ? Parce qu'elles entraînent des courses de données (data races) et un comportement indéfini. Le compilateur ne peut pas prouver que deux threads n'écriront pas simultanément dans la même cellule mémoire.
\n\nLa solution naïve ressemble à ceci :
\n// MAUVAIS code — ne faites pas ça !\nstatic mut COUNTER: u32 = 0;\n\nfn increment() {\n unsafe {\n COUNTER += 1;\n }\n}\n\nCela fonctionne, mais nécessite unsafe et vous laisse seul face aux courses. Dans un environnement multi-thread, c'est une bombe à retardement. Nous avons besoin d'une approche sûre et idiomatique.
Outil 1 : lazy_static + Mutex — le classique du genre
\n\nLa façon la plus populaire de créer un singleton mutable global est la combinaison de lazy_static (initialisation paresseuse) et Mutex (exclusion mutuelle).
\n\nAjoutez les dépendances dans Cargo.toml :
[dependencies]\nlazy_static = "1.4"\n\n\nMaintenant le code :
\nuse lazy_static::lazy_static;\nuse std::sync::Mutex;\n\nlazy_static! {\n static ref GLOBAL_CONFIG: Mutex<Config> = Mutex::new(Config::default());\n}\n\n#[derive(Debug)]\nstruct Config {\n db_url: String,\n max_connections: u32,\n}\n\nimpl Default for Config {\n fn default() -> Self {\n Config {\n db_url: "localhost:5432".to_string(),\n max_connections: 10,\n }\n }\n}\n\nfn main() {\n // Lecture\n let config = GLOBAL_CONFIG.lock().unwrap();\n println!("URL actuelle : {}", config.db_url);\n\n // Écriture\n // drop(config); // libère explicitement le verrou\n let mut config = GLOBAL_CONFIG.lock().unwrap();\n config.db_url = "new-host:8888".to_string();\n println!("Nouvelle URL : {}", config.db_url);\n}\n\n\nComment cela fonctionne-t-il ?
\n- \n
lazy_static!garantit que l'initialisation n'a lieu qu'une seule fois lors de la première utilisation. \n Mutex<T>assure l'exclusion mutuelle : un seul thread peut accéder aux données à un moment donné. \n - La méthode
.lock()retourne unMutexGuardqui libère automatiquement le verrou en sortant de la portée. \n
Avantages : simplicité, éprouvé par le temps, sûr pour