Saltar al contenido

La trampa del cerrojo a través de un await

El error más frecuente de la fase III: sostener un cerrojo síncrono mientras esperas. Qué dice el compilador y por qué no menciona cerrojos, las tres formas de arreglarlo, y por qué la que compila sola es la peor.

El error

Rust
#[tauri::command]async fn refrescar(estado: tauri::State<'_, Cache>) -> Result<(), ErrorPayload> {    let mut c = estado.0.lock().unwrap();   // se toma el cerrojo    c.datos = descargar().await?;           // …y aquí se espera, con él tomado    Ok(())}
TXT
error: future cannot be sent between threads safely   |   = help: within `impl Future`, the trait `Send` is not implemented for           `std::sync::MutexGuard<'_, Datos>`note: future is not `Send` as this value is used across an await

Qué está diciendo

Se traduce en tres pasos:

Y hay una razón más profunda que el compilador no dice y conviene saber: aunque compilara, sería una mala idea. Sostener un cerrojo mientras esperas una descarga significa que todos los demás comandos que necesiten ese estado se quedan bloqueados durante toda la descarga. El compilador te está salvando de un problema de diseño, no solo de uno de tipos.

Las tres soluciones

Las dos primeras, escritas

Rust
// 1 · soltar antes de esperar#[tauri::command]async fn refrescar(estado: tauri::State<'_, Cache>) -> Result<(), ErrorPayload> {    let url = {        let c = estado.0.lock().unwrap();        c.url.clone()                       // copiar lo que hace falta…    };                                      // …y soltar aquí, cerrando el bloque    let datos = descargar(&url).await?;     // esperar SIN el cerrojo    estado.0.lock().unwrap().datos = datos; // y volver a tomarlo un instante    Ok(())}

El bloque con llaves no es cosmético: es lo que hace que el guardián muera antes del punto de espera. Es el idioma que hay que reconocer.

Rust
// 2 · esperar primero, tomar después — más simple cuando aplica#[tauri::command]async fn refrescar(estado: tauri::State<'_, Cache>) -> Result<(), ErrorPayload> {    let datos = descargar(URL).await?;      // nada de cerrojos aquí    estado.0.lock().unwrap().datos = datos; // el cerrojo, solo para escribir    Ok(())}

Lo que hay que asumir al soltar

Soltar y volver a tomar tiene una consecuencia que no se puede ignorar: entre las dos tomas, otro comando pudo cambiar el estado. Tres formas de tratarlo, según lo que importe:

  • Si el último gana, no hagas nada. Es lo correcto para una caché: sobrescribir es la operación.
  • Si hay que detectar el cambio, guarda una versión o una marca de tiempo al leer y compruébala al escribir. Si cambió, o descartas tu resultado o repites.
  • Si el cambio no puede ocurrir, es porque solo hay un sitio que escribe, y entonces conviene que el diseño lo diga: un único punto de escritura, o el patrón de la tarea dueña del estado del nivel 22.