Saltar al contenido

Las envolturas, en serio

Cerrojo síncrono, cerrojo asíncrono, cerrojo de lectura y escritura, celda atómica. Cuál corresponde a cada forma de acceso, cómo se mide la contención, y los dos interbloqueos que se escriben sin querer.

Según el patrón de acceso

La última fila merece insistencia: mucho de lo que se mete en un cerrojo no cambia. Las rutas resueltas al arrancar, la configuración cargada, un cliente HTTP construido una vez. Si no muta, no necesita envoltura y todo el nivel deja de aplicar.

La contención, y cómo se nota

Contención es que dos comandos quieran el mismo cerrojo a la vez. Con poco tráfico no ocurre; cuando ocurre, el síntoma es un tiempo de respuesta que empeora cuando hay más actividad y no cuando el trabajo es mayor.

Se mide con un contador y un reloj alrededor de la toma:

Rust
let t = std::time::Instant::now();let g = estado.0.lock().unwrap();let espera = t.elapsed();if espera > std::time::Duration::from_millis(5) {    log::warn!("esperé {espera:?} por el cerrojo de Cache");}

Dejarlo unos días en desarrollo dice más que cualquier suposición. Si nunca salta, el cerrojo no es el problema y cambiarlo por uno más elaborado sería trabajo desperdiciado.

Los dos interbloqueos

El compilador caza el del nivel 16.5 y no caza estos dos:

1 · Tomar dos cerrojos en órdenes distintos. Un comando toma A y luego B; otro toma B y luego A. Si coinciden, los dos esperan para siempre y la aplicación se queda quieta sin ningún error.

La defensa: un orden global. Si hay que tomar dos, siempre en el mismo orden, y escrito donde se vea. Mejor todavía: no tener dos, agrupando el estado que se toca junto en un solo tipo.

2 · Volver a tomar el mismo cerrojo sin haberlo soltado. Una función toma el cerrojo y llama a otra que también lo toma. Con un cerrojo no reentrante —que es el habitual— eso se bloquea a sí mismo.

Rust
// Se escribe sin querer cuando una función auxiliar también toma.fn recalcular(estado: &Cache) {    let g = estado.0.lock().unwrap();    let n = contar(estado);        // ← y contar() vuelve a tomar: bloqueo}

La defensa: que las funciones auxiliares reciban el dato ya prestado en vez de tomar el cerrojo ellas. Es una regla simple con un efecto grande: solo toma el cerrojo la capa de arriba.

Cuando la respuesta no es una envoltura

Hay una señal clara de que el problema no se arregla eligiendo mejor:

  • Necesitas tomar dos o tres cerrojos para una operación.
  • Las secciones críticas son largas porque hay que hacer varias cosas de forma consistente.
  • Te ves poniendo un cerrojo asíncrono para poder esperar dentro.

En los tres, lo que hace falta es cambiar de modelo: que el estado lo posea una tarea y se le hable por mensajes. Sin cerrojos, sin órdenes, sin reentrada. Es el nivel 22.4.