Introducción: ¿por qué un singleton global en Rust es un desafío?
\n\nSi vienes del mundo de Python, JavaScript o C++, estás acostumbrado a un objeto global simple. Declaras una variable a nivel de módulo y es tuya. En Rust, ese truco no funciona. El compilador supervisa estrictamente el tiempo de vida y el acceso a los datos, especialmente si pueden cambiar desde diferentes hilos. Por eso, en Stack Overflow, la pregunta «How do I create a global, mutable singleton?» ha acumulado miles de visitas y decenas de respuestas complejas.
\n\nPero no te asustes. Un singleton en Rust no es un dolor, es disciplina. Hoy analizaremos cómo crear un singleton mutable global de forma segura, eficiente y con comprensión de lo que sucede. Aprenderás por qué static mut es malo y qué herramientas (lazy_static, OnceCell, Mutex) nos brindan control.
Este artículo está escrito para el sitio educativo pythonlib.ru y adapta una pregunta real de SO a una lección práctica. ¡Vamos allá!
\n\nEl problema: ¿qué es un singleton mutable global?
\n\nUn singleton es un patrón en el que una clase o estructura tiene exactamente una instancia en todo el programa. En el contexto de Rust, hablamos de una variable global accesible desde cualquier lugar del código, que se puede modificar (mut).
\n\nEjemplos de uso:
\n- \n
- Configuración de la aplicación, cargada una vez al inicio. \n
- Un registrador global o contador de eventos. \n
- Un pool de conexiones a la base de datos. \n
- Un caché en memoria (in-memory cache). \n
El problema es que Rust prohíbe las variables globales mutables sin un envoltorio especial. ¿Por qué? Porque conducen a condiciones de carrera (data races) y comportamiento indefinido. El compilador no puede demostrar que dos hilos no escribirán simultáneamente en la misma celda de memoria.
\n\nLa solución ingenua se ve así:
\n// Código MALO — ¡no lo hagas!\nstatic mut COUNTER: u32 = 0;\n\nfn increment() {\n unsafe {\n COUNTER += 1;\n }\n}\n\nEsto funciona, pero requiere unsafe y te deja solo frente a las condiciones de carrera. En un entorno multihilo, es una bomba de tiempo. Necesitamos un enfoque seguro e idiomático.
Herramienta 1: lazy_static + Mutex — el clásico del género
\n\nLa forma más popular de crear un singleton mutable global es la combinación de lazy_static (inicialización perezosa) y Mutex (exclusión mutua).
\n\nAñade las dependencias en Cargo.toml:
[dependencies]\nlazy_static = "1.4"\n\n\nAhora el código:
\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 // Lectura\n let config = GLOBAL_CONFIG.lock().unwrap();\n println!("URL actual: {}", config.db_url);\n\n // Escritura\n // drop(config); // liberamos explícitamente el bloqueo\n let mut config = GLOBAL_CONFIG.lock().unwrap();\n config.db_url = "new-host:8888".to_string();\n println!("Nueva URL: {}", config.db_url);\n}\n\n\n¿Cómo funciona?
\n- \n
lazy_static!garantiza que la inicialización ocurra solo una vez en el primer acceso. \n Mutex<T>proporciona exclusión mutua: solo un hilo puede acceder a los datos en un momento dado. \n - El método
.lock()devuelve unMutexGuard, que libera automáticamente el bloqueo al salir del ámbito. \n
Ventajas: simplicidad, probado en el tiempo, seguro para