/* This example uses the pthreads API */#include /*
** A pointer to an instance of this structure is passed as the user-context
** pointer when registering for an unlock-notify callback.
*/
typedef struct UnlockNotification UnlockNotification;
struct UnlockNotification 
  int fired;/* True after unlock event has occurred */
  pthread_cond_t cond;/* Condition variable to wait on */
  pthread_mutex_t mutex;/* Mutex to protect structure */
;/*
** This function is an unlock-notify callback registered with SQLite.
*/
static void unlock_notify_cb(void **apArg,int nArg)
  int i;for(i=0; i<nArg; i++)
    UnlockNotification *p =(UnlockNotification *)apArg[i];
    pthread_mutex_lock(&p->mutex);
    p->fired =1;
    pthread_cond_signal(&p->cond);
    pthread_mutex_unlock(&p->mutex);
  


/*
** This function assumes that an SQLite API call (either sqlite3_prepare_v2() 
** or sqlite3_step()) has just returned SQLITE_LOCKED. The argument is the
** associated database connection.
**
** This function calls sqlite3_unlock_notify() to register for an 
** unlock-notify callback, then blocks until that callback is delivered 
** and returns SQLITE_OK. The caller should then retry the failed operation.
**
** Or, if sqlite3_unlock_notify() indicates that to block would deadlock 
** the system, then this function returns SQLITE_LOCKED immediately. In 
** this case the caller should not retry the operation and should roll 
** back the current transaction (if any).
*/
static int wait_for_unlock_notify(sqlite3 *db)
  int rc;
  UnlockNotification un;/* Initialize the UnlockNotification structure. */
  un.fired =0;
  pthread_mutex_init(&un.mutex,0);
  pthread_cond_init(&un.cond,0);/* Register for an unlock-notify callback. */
  rc = sqlite3_unlock_notify(db, unlock_notify_cb,(void *)&un);
  assert( rc==SQLITE_LOCKED 

/*
** This function is a wrapper around the SQLite function sqlite3_step().
** It functions in the same way as step(), except that if a required
** shared-cache lock cannot be obtained, this function may block waiting for
** the lock to become available. In this scenario the normal API step()
** function always returns SQLITE_LOCKED.
**
** If this function returns SQLITE_LOCKED, the caller should rollback
** the current transaction (if any) and try again later. Otherwise, the
** system may become deadlocked.
*/int sqlite3_blocking_step(sqlite3_stmt *pStmt)
  int rc;while( SQLITE_LOCKED==(rc = sqlite3_step(pStmt)))
    rc = wait_for_unlock_notify(sqlite3_db_handle(pStmt));if( rc!=SQLITE_OK )break;
    sqlite3_reset(pStmt);
  
  return rc;


/*
** This function is a wrapper around the SQLite function sqlite3_prepare_v2().
** It functions in the same way as prepare_v2(), except that if a required
** shared-cache lock cannot be obtained, this function may block waiting for
** the lock to become available. In this scenario the normal API prepare_v2()
** function always returns SQLITE_LOCKED.
**
** If this function returns SQLITE_LOCKED, the caller should rollback
** the current transaction (if any) and try again later. Otherwise, the
** system may become deadlocked.
*/int sqlite3_blocking_prepare_v2(
  sqlite3 *db,/* Database handle. */
  const char*zSql,/* UTF-8 encoded SQL statement. */int nSql,/* Length of zSql in bytes. */
  sqlite3_stmt **ppStmt,/* OUT: A pointer to the prepared statement */
  const char**pz           /* OUT: End of parsed string */)
  int rc;while( SQLITE_LOCKED==(rc = sqlite3_prepare_v2(db, zSql, nSql, ppStmt, pz)))
    rc = wait_for_unlock_notify(db);if( rc!=SQLITE_OK )break;
  
  return rc;

Cuando dos o más conexiones acceden a la misma base de datos en modo de caché compartida, se utilizan bloqueos de lectura y escritura (compartidos y exclusivos) en tablas individuales para garantizar que las transacciones que se ejecutan simultáneamente se mantienen aisladas. Antes de escribir en una tabla, se debe obtener un bloqueo de escritura (exclusivo) en esa tabla. Antes de leer, se debe obtener un bloqueo de lectura (compartido). Una conexión libera todos los bloqueos de tabla retenidos cuando concluye su transacción. Si una conexión no puede obtener un bloqueo requerido, la llamada a sqlite3_step () devuelve SQLITE_LOCKED.

Aunque es menos común, una llamada a sqlite3_prepare () o sqlite3_prepare_v2 () también puede devolver SQLITE_LOCKED si no puede obtener un bloqueo de lectura en la tabla sqlite_schema de cada base de datos adjunta. Estas API necesitan leer los datos de esquema contenidos en la tabla sqlite_schema para compilar declaraciones SQL en objetos sqlite3_stmt *.

Este artículo presenta una técnica que utiliza la interfaz SQLite sqlite3_unlock_notify () de modo que las llamadas al bloque sqlite3_step () y sqlite3_prepare_v2 () hasta que los bloqueos necesarios estén disponibles en lugar de devolver SQLITE_LOCKED inmediatamente. Si las funciones sqlite3_blocking_step () o sqlite3_blocking_prepare_v2 () presentadas a la izquierda devuelven SQLITE_LOCKED, esto indica que bloquear haría un punto muerto en el sistema.

La API sqlite3_unlock_notify (), que solo está disponible si la biblioteca se compila con el símbolo de preprocesador SQLITE_ENABLE_UNLOCK_NOTIFY definido, se documenta aquí. ¡Este artículo no sustituye la lectura de la documentación completa de la API!

La interfaz sqlite3_unlock_notify () está diseñada para su uso en sistemas que tienen un subproceso separado asignado a cada conexión de base de datos. No hay nada en la implementación que impida que un solo hilo ejecute múltiples conexiones de base de datos. Sin embargo, la interfaz sqlite3_unlock_notify () solo funciona en una sola conexión a la vez, por lo que la lógica de resolución de bloqueo presentada aquí solo funcionará para una sola conexión de base de datos por hilo.

La API sqlite3_unlock_notify ()

Después de que una llamada a sqlite3_step () o sqlite3_prepare_v2 () devuelva SQLITE_LOCKED, se puede invocar la API sqlite3_unlock_notify () para registrarse para una devolución de llamada de desbloqueo-notificación. SQLite invoca la devolución de llamada de desbloqueo-notificación después de que la conexión de la base de datos que contiene el bloqueo de tabla que impidió que la llamada a sqlite3_step () o sqlite3_prepare_v2 () tuviera éxito haya finalizado su transacción y haya liberado todos los bloqueos. Por ejemplo, si una llamada a sqlite3_step () es un intento de leer desde la tabla X, y alguna otra conexión Y tiene un bloqueo de escritura en la tabla X, entonces sqlite3_step () devolverá SQLITE_LOCKED. Si luego se llama a sqlite3_unlock_notify (), la devolución de llamada de desbloqueo-notificación se invocará después de que concluya la transacción de la conexión Y. La conexión que está esperando la devolución de llamada de desbloqueo-notificación, en este caso la conexión Y, se conoce como “conexión de bloqueo”.

Si una llamada a sqlite3_step () que intenta escribir en una tabla de la base de datos devuelve SQLITE_LOCKED, entonces más de una conexión puede tener un bloqueo de lectura en la tabla de la base de datos en cuestión. En este caso, SQLite simplemente selecciona una de esas otras conexiones arbitrariamente y emite la devolución de llamada de desbloqueo-notificación cuando finaliza la transacción de esa conexión. Si la llamada a sqlite3_step () fue bloqueada por una o muchas conexiones, cuando se emite la devolución de llamada de desbloqueo-notificación correspondiente, no se garantiza que el bloqueo requerido esté disponible, solo que puede estarlo.

Cuando se emite la devolución de llamada de desbloqueo-notificación, se emite desde dentro de una llamada a sqlite3_step () (o sqlite3_close ()) asociada con la conexión de bloqueo. Es ilegal invocar cualquier función de la API sqlite3_XXX () desde una devolución de llamada de desbloqueo-notificación. El uso esperado es que la devolución de llamada de desbloqueo-notificación señalará algún otro hilo en espera o programará alguna acción para que se lleve a cabo más tarde.

El algoritmo utilizado por la función sqlite3_blocking_step () es el siguiente:

  1. Llame a sqlite3_step () en el identificador de instrucciones proporcionado. Si la llamada devuelve algo que no sea SQLITE_LOCKED, devuelva este valor a la persona que llama. De lo contrario, continúe.

  2. Invoque sqlite3_unlock_notify () en el identificador de conexión de la base de datos asociado con el identificador de instrucción proporcionado para registrarse para una devolución de llamada de desbloqueo-notificación. Si la llamada a unlock_notify () devuelve SQLITE_LOCKED, devuelva este valor a la persona que llama.

  3. Bloquear hasta que la devolución de llamada de desbloqueo-notificación sea invocada por otro hilo.

  4. Llame a sqlite3_reset () en el identificador de instrucciones. Dado que un error SQLITE_LOCKED solo puede ocurrir en la primera llamada a sqlite3_step () (no es posible que una llamada a sqlite3_step () devuelva SQLITE_ROW y luego el siguiente SQLITE_LOCKED), el identificador de la instrucción se puede restablecer en este punto sin afectar los resultados de la consulta desde el punto de vista de la persona que llama. Si no se llamó a sqlite3_reset () en este punto, la siguiente llamada a sqlite3_step () devolvería SQLITE_MISUSE.

  5. Regrese al paso 1.

El algoritmo utilizado por la función sqlite3_blocking_prepare_v2 () es similar, excepto que se omite el paso 4 (restablecer el identificador de declaración).

Escritor Hambruna

Varias conexiones pueden mantener un bloqueo de lectura simultáneamente. Si muchos subprocesos adquieren bloqueos de lectura superpuestos, puede darse el caso de que al menos un subproceso siempre mantenga un bloqueo de lectura. Entonces, una mesa esperando un bloqueo de escritura esperará eternamente. Este escenario se llama “hambre de escritor”.

SQLite ayuda a las aplicaciones a evitar la falta de escritor. Después de que falla cualquier intento de obtener un bloqueo de escritura en una tabla (porque una o más conexiones tienen bloqueos de lectura), todos los intentos de abrir nuevas transacciones en la caché compartida fallan hasta que se cumple una de las siguientes condiciones:

  • El escritor actual concluye su transacción, O
  • El número de transacciones de lectura abiertas en la caché compartida se reduce a cero.

Los intentos fallidos de abrir nuevas transacciones de lectura devuelven SQLITE_LOCKED a la persona que llama. Si la persona que llama luego llama a sqlite3_unlock_notify () para registrarse para una devolución de llamada de desbloqueo-notificación, la conexión de bloqueo es la conexión que actualmente tiene una transacción de escritura abierta en el caché compartido. Esto evita la falta de escritura, ya que si no se pueden abrir nuevas transacciones de lectura y asumiendo que todas las transacciones de lectura existentes se concluyen finalmente, el escritor finalmente tendrá la oportunidad de obtener el bloqueo de escritura requerido.

La API de pthreads

En el momento en que wait_for_unlock_notify () invoca sqlite3_unlock_notify (), es posible que la conexión de bloqueo que impidió que la llamada sqlite3_step () o sqlite3_prepare_v2 () tuviera éxito ya haya finalizado su transacción. En este caso, la devolución de llamada de desbloqueo-notificación se invoca inmediatamente, antes de que sqlite3_unlock_notify () regrese. O bien, es posible que la devolución de llamada de desbloqueo-notificación sea invocada por un segundo subproceso después de que se llame a sqlite3_unlock_notify () pero antes de que el subproceso comience a esperar a ser señalado de forma asincrónica.

Exactamente cómo se maneja tal condición de carrera potencial depende de los subprocesos y la interfaz de primitivas de sincronización utilizada por la aplicación. Este ejemplo utiliza pthreads, la interfaz proporcionada por los sistemas modernos similares a UNIX, incluido Linux.

La interfaz pthreads proporciona la función pthread_cond_wait (). Esta función permite a la persona que llama liberar simultáneamente un mutex y comenzar a esperar una señal asincrónica. Usando esta función, una bandera “activada” y un mutex, la condición de carrera descrita anteriormente se puede eliminar de la siguiente manera:

Cuando se invoca la devolución de llamada de desbloqueo-notificación, que puede ser antes de que el hilo que llamó sqlite3_unlock_notify () comience a esperar la señal asincrónica, hace lo siguiente:

  1. Obtiene el mutex.
  2. Establece el indicador “disparado” en verdadero.
  3. Intenta señalar un hilo en espera.
  4. Libera el mutex.

Cuando el hilo wait_for_unlock_notify () está listo para comenzar a esperar a que llegue la devolución de llamada de desbloqueo-notificación,:

  1. Obtiene el mutex.
  2. Comprueba si se ha establecido la bandera de “disparado”. Si es así, ya se ha invocado la devolución de llamada de desbloqueo-notificación. Suelta el mutex y continúa.
  3. Libera atómicamente el mutex y comienza a esperar la señal asincrónica. Cuando llegue la señal, continúe.

De esta manera, no importa si la devolución de llamada de desbloqueo-notificación ya se ha invocado, o se está invocando, cuando el hilo wait_for_unlock_notify () comienza a bloquearse.

Posibles mejoras

El código de este artículo podría mejorarse de al menos dos formas:

  • Podría gestionar las prioridades de los hilos.
  • Podría manejar un caso especial de SQLITE_LOCKED que puede ocurrir al soltar una tabla o índice.

Aunque la función sqlite3_unlock_notify () solo permite que la persona que llama especifique un único puntero de contexto de usuario, una devolución de llamada de desbloqueo-notificación se pasa a una matriz de dichos punteros de contexto. Esto se debe a que si cuando una conexión de bloqueo concluye su transacción, si hay más de una notificación de desbloqueo registrada para llamar a la misma función C, los punteros de contexto se agrupan en una matriz y se emite una única devolución de llamada. Si a cada subproceso se le asignara una prioridad, entonces, en lugar de simplemente señalar los subprocesos en orden arbitrario como lo hace esta implementación, los subprocesos de mayor prioridad se podrían señalar antes que los subprocesos de menor prioridad.

Si se ejecuta un comando SQL “DROP TABLE” o “DROP INDEX”, y la misma conexión de base de datos tiene actualmente una o más sentencias SELECT en ejecución activa, se devuelve SQLITE_LOCKED. Si se llama a sqlite3_unlock_notify () en este caso, la devolución de llamada especificada se invocará inmediatamente. Reintentar la instrucción “DROP TABLE” o “DROP INDEX” devolverá otro error SQLITE_LOCKED. En la implementación de sqlite3_blocking_step () que se muestra a la izquierda, esto podría causar un bucle infinito.

La persona que llama podría distinguir entre este caso especial “DROP TABLE | INDEX” y otros casos mediante el uso de códigos de error extendidos. Cuando es apropiado llamar a sqlite3_unlock_notify (), el código de error extendido es SQLITE_LOCKED_SHAREDCACHE. De lo contrario, en el caso “DROP TABLE | INDEX”, es simplemente SQLITE_LOCKED. Otra solución podría ser limitar el número de veces que se puede volver a intentar una sola consulta (por ejemplo, 100). Aunque esto podría ser menos eficiente de lo que uno desearía, es poco probable que la situación en cuestión ocurra con frecuencia.