Saltar al contenido

SQLite no es asíncrona

Su entrada y salida es bloqueante, y ninguna librería lo cambia. Qué significa eso para un comando asíncrono y cuál es el patrón que no finge.

Por qué bloquea

No es una carencia de la librería: es consecuencia de lo que SQLite es.

El fallo

Y es sutil porque el código parece correcto:

Rust
// mal: el comando es asíncrono, pero la consulta bloquea el hilo del ejecutor#[tauri::command]async fn buscar(db: State<'_, Db>, texto: String) -> Result<Vec<Fila>, Error> {    let conn = db.0.lock().await;    consulta_pesada(&conn, &texto)          // bloquea aquí, dentro del ejecutor asíncrono}

Qué pasa de verdad: el ejecutor asíncrono tiene unos pocos hilos para muchas tareas, y bloquear uno le impide atender a las demás. Con una consulta lenta y varios comandos en curso, el ejecutor se queda sin hilos disponibles y tareas que no tienen nada que ver dejan de avanzar.

El síntoma es desconcertante: una búsqueda lenta hace que se congele algo que no la usa —una animación, otra petición, un evento— y el fallo no aparece en desarrollo porque ahí hay una operación cada vez.

El patrón

Mover el trabajo bloqueante a un hilo pensado para eso:

Rust
#[tauri::command]async fn buscar(app: AppHandle, texto: String) -> Result<Vec<Fila>, Error> {    tauri::async_runtime::spawn_blocking(move || {        let db = app.state::<Db>();        let conn = db.0.blocking_lock();     // aquí sí se puede bloquear        consulta_pesada(&conn, &texto)    })    .await?}

Tres detalles:

  • El cerrojo se toma dentro del hilo bloqueante, con su variante bloqueante. Tomarlo fuera y llevarlo dentro complica la vida al compilador y no aporta nada.
  • Lo que entra al hilo tiene que poder moverse entre hilos, lo que a veces obliga a copiar unos cuantos datos. Es un precio pequeño.
  • Y el resultado vuelve por el await, con lo que el comando sigue siendo asíncrono desde fuera.

Cuándo no importa

Ser honesto con el coste, porque envolver todo en hilos bloqueantes también tiene precio:

La última fila es el argumento decisivo: lo que en tu máquina de desarrollo con doscientas filas tarda un microsegundo, en la máquina de un usuario con trescientas mil puede tardar un segundo. Ante la duda, hilo bloqueante — el coste es despreciable comparado con una interfaz congelada.