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
#[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(())}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 awaitQué 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
// 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.
// 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.