Spec-Zone.ru › SQLite

Использование API sqlite3_unlock_notify()

/* This example uses the pthreads API */
#include <pthread.h>

/*
** 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 || rc==SQLITE_OK );

  /* The call to sqlite3_unlock_notify() always returns either SQLITE_LOCKED 
  ** or SQLITE_OK. 
  **
  ** If SQLITE_LOCKED was returned, then the system is deadlocked. In this
  ** case this function needs to return SQLITE_LOCKED to the caller so 
  ** that the current transaction can be rolled back. Otherwise, block
  ** until the unlock-notify callback is invoked, then return SQLITE_OK.
  */
  if( rc==SQLITE_OK ){
    pthread_mutex_lock(&un.mutex);
    if( !un.fired ){
      pthread_cond_wait(&un.cond, &un.mutex);
    }
    pthread_mutex_unlock(&un.mutex);
  }

  /* Destroy the mutex and condition variables. */
  pthread_cond_destroy(&un.cond);
  pthread_mutex_destroy(&un.mutex);

  return rc;
}

/*
** 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;
}

Когда два или более подключений обращаются к одной и той же базе данных в режиме совместного кэша, используются блокировки чтения и записи (совместные и эксклюзивные) на отдельных таблицах, чтобы гарантировать изоляцию одновременно выполняемых транзакций. Перед записью в таблицу необходимо получить блокировку записи (эксклюзивную) на эту таблицу. Перед чтением необходимо получить блокировку чтения (совместную). Подключение высвобождает все удерживаемые блокировки таблиц по завершении транзакции. Если подключение не может получить необходимую блокировку, то вызов sqlite3_step() возвращает SQLITE_LOCKED.

Хотя это встречается реже, вызов sqlite3_prepare() или sqlite3_prepare_v2() также может вернуть SQLITE_LOCKED, если не удаётся получить блокировку чтения на таблице sqlite_schema каждой подключенной базы данных. Эти API необходимо читать данные схемы, содержащиеся в таблице sqlite_schema, для компиляции SQL-запросов в объекты sqlite3_stmt*.

В этой статье представлен метод, использующий интерфейс SQLite sqlite3_unlock_notify(), при котором вызовы sqlite3_step() и sqlite3_prepare_v2() блокируются до тех пор, пока необходимые блокировки не станут доступны, вместо немедленного возвращения SQLITE_LOCKED. Если функции sqlite3_blocking_step() или sqlite3_blocking_prepare_v2(), представленные слева, возвращают SQLITE_LOCKED, это указывает на то, что блокировка приведет к тупику системы.

API sqlite3_unlock_notify(), доступное только при компиляции библиотеки с определённым препроцессором символом SQLITE_ENABLE_UNLOCK_NOTIFY, документировано здесь. Данная статья не является заменой для прочтения полной документации API!

Интерфейс sqlite3_unlock_notify() предназначен для использования в системах, где каждому подключению к базе данных назначен отдельный поток. В реализации нет ничего, что препятствует выполнению нескольких подключений к базе данных одним потоком. Однако интерфейс sqlite3_unlock_notify() работает только с одним подключением за раз, поэтому логика разрешения блокировок, представленная здесь, будет работать только для одного подключения к базе данных на поток.

API sqlite3_unlock_notify()

После того, как вызов sqlite3_step() или sqlite3_prepare_v2() вернёт SQLITE_LOCKED, можно вызвать API sqlite3_unlock_notify() для регистрации обратного вызова unlock-notify. Обратный вызов unlock-notify вызывается SQLite после того, как подключение к базе данных, удерживающее блокировку таблицы, которая препятствовала успешному вызову sqlite3_step() или sqlite3_prepare_v2(), завершило свою транзакцию и освободило все блокировки. Например, если вызов sqlite3_step() является попыткой чтения из таблицы X, а какое-то другое подключение Y удерживает блокировку записи на таблице X, то sqlite3_step() вернёт SQLITE_LOCKED. Если затем вызывается sqlite3_unlock_notify(), обратный вызов unlock-notify будет вызван после завершения транзакции подключения Y. Подключение, на которое ожидает обратный вызов unlock-notify, в данном случае подключение Y, известно как "блокирующее подключение".

Если вызов sqlite3_step(), который пытается записать в таблицу базы данных, возвращает SQLITE_LOCKED, то может быть более одного другого подключения, удерживающего блокировку чтения на таблице базы данных. В этом случае SQLite просто произвольно выбирает одно из этих других подключений и вызывает обратный вызов unlock-notify, когда транзакция этого подключения завершается. Не гарантируется, что необходимая блокировка будет доступна, когда вызывается соответствующий обратный вызов unlock-notify, независимо от того, было ли подключение заблокировано одним или несколькими подключениями, только что она может быть доступна.

Когда вызывается обратный вызов unlock-notify, он вызывается из вызова sqlite3_step() (или sqlite3_close()), связанного с блокирующим подключением. Запрещено вызывать любые функции sqlite3_XXX() из обратного вызова unlock-notify. Ожидаемое использование заключается в том, что обратный вызов unlock-notify будет сигнализировать другому ожидающему потоку или планировать какое-либо действие на более поздний срок.

Алгоритм, используемый функцией sqlite3_blocking_step(), следующий:

  1. Вызов sqlite3_step() для заданного обработчика оператора. Если вызов возвращает что-либо, кроме SQLITE_LOCKED, то верните это значение вызывающей стороне. В противном случае продолжайте.

  2. Вызов sqlite3_unlock_notify() для обработчика базы данных, связанного с заданным обработчиком оператора, для регистрации обратного вызова unlock-notify. Если вызов unlock_notify() возвращает SQLITE_LOCKED, верните это значение вызывающей стороне.

  3. Заблокироваться до тех пор, пока обратный вызов unlock-notify не будет вызван другим потоком.

  4. Вызов sqlite3_reset() для обработчика оператора. Поскольку ошибка SQLITE_LOCKED может возникнуть только при первом вызове sqlite3_step() (невозможно, чтобы один вызов sqlite3_step() возвращал SQLITE_ROW, а затем следующий — SQLITE_LOCKED), обработчик оператора может быть сброшен на этом этапе без влияния на результаты запроса с точки зрения вызывающей стороны. Если бы sqlite3_reset() не был вызван на этом этапе, следующий вызов sqlite3_step() вернул бы SQLITE_MISUSE.

  5. Возврат к шагу 1.

Алгоритм, используемый функцией sqlite3_blocking_prepare_v2(), аналогичен, за исключением того, что шаг 4 (сброс обработчика оператора) пропущен.

Голод писателя

Несколько подключений могут одновременно удерживать блокировку чтения. Если много потоков приобретают перекрывающиеся блокировки чтения, может быть так, что по крайней мере один поток всегда удерживает блокировку чтения. Тогда таблица, ожидающая блокировки записи, будет ждать вечно. Эта ситуация называется "голод писателя".

SQLite помогает приложениям избежать голода писателя. После любой неудачной попытки получить блокировку записи для таблицы (потому что одно или несколько других подключений удерживают блокировки чтения), все попытки открыть новые транзакции в общем кэше терпят неудачу, пока одно из следующих условий не будет истинно:

  • Текущий писатель завершает свою транзакцию, ИЛИ
  • Число открытых транзакций чтения в общем кэше становится нулевым.

Неудачные попытки открытия новых транзакций чтения возвращают вызывающей стороне SQLITE_LOCKED. Если вызывающая сторона затем вызывает sqlite3_unlock_notify() для регистрации обратного вызова unlock-notify, блокирующим подключением является подключение, которое в настоящее время имеет открытую транзакцию записи в общем кэше. Это предотвращает голод писателя, так как если новые транзакции чтения не могут быть открыты, и предполагая, что все существующие транзакции чтения в конечном итоге завершатся, у писателя в конечном итоге будет возможность получить необходимую блокировку записи.

API pthreads

К тому времени, когда sqlite3_unlock_notify() вызывается wait_for_unlock_notify(), возможно, что блокирующее подключение, которое препятствовало вызову sqlite3_step() или sqlite3_prepare_v2(), уже завершило свою транзакцию. В этом случае обратный вызов unlock-notify вызывается немедленно, прежде чем sqlite3_unlock_notify() вернёт значение. Или, возможно, обратный вызов unlock-notify вызывается вторым потоком после вызова sqlite3_unlock_notify(), но до того, как поток начнёт ожидать асинхронного сигнала.

Точно то, как обрабатывается потенциальное состояние гонки, зависит от используемого приложением интерфейса потоков и синхронизирующих примитивов. В этом примере используется pthreads, интерфейс, предоставляемый современными системами типа UNIX, включая Linux.

Интерфейс pthreads предоставляет функцию pthread_cond_wait(). Эта функция позволяет вызывающей стороне одновременно высвободить мьютекс и начать ожидание асинхронного сигнала. Используя эту функцию, "флаг fired" и мьютекс, состояние гонки, описанное выше, можно устранить следующим образом:

Когда вызывается обратный вызов unlock-notify, который может произойти до того, как поток, вызвавший sqlite3_unlock_notify(), начнёт ожидать асинхронного сигнала, он выполняет следующие действия:

  1. Получение мьютекса.
  2. Установка флага "fired" в true.
  3. Попытка послать сигнал ожидающему потоку.
  4. Высвобождение мьютекса.

Когда поток wait_for_unlock_notify() готов начать ожидание прихода обратного вызова unlock-notify, он:

  1. Получение мьютекса.
  2. Проверка, установлен ли флаг "fired". Если да, то обратный вызов unlock-notify уже был вызван. Высвободите мьютекс и продолжайте.
  3. Атомарно высвобождает мьютекс и начинает ожидать асинхронного сигнала. Когда сигнал приходит, продолжайте.

Таким образом, не имеет значения, был ли уже вызван обратный вызов unlock-notify или он вызывается, когда поток wait_for_unlock_notify() начинает блокировку.

Возможные улучшения

Код в этой статье можно улучшить как минимум двумя способами:

  • Он может управлять приоритетами потоков.
  • Он может обрабатывать особый случай SQLITE_LOCKED, который может возникнуть при удалении таблицы или индекса.

Несмотря на то, что функция sqlite3_unlock_notify() позволяет вызывающей стороне указать только один указатель контекста пользователя, обратному вызову unlock-notify передаётся массив таких указателей контекста. Это потому, что если при завершении транзакции блокирующего подключения существует более одного зарегистрированного обратного вызова unlock-notify для вызова одной и той же C-функции, указатели контекста собираются в массив, и вызывается единственный обратный вызов. Если каждому потоку был бы назначен приоритет, то вместо простого сигнализации потоков в произвольном порядке, как это делает данная реализация, потоки с более высоким приоритетом могли бы сигнализироваться перед потоками с более низким приоритетом.

Если выполняется SQL-команда "DROP TABLE" или "DROP INDEX", и в настоящее время у того же подключения к базе данных есть один или несколько активно выполняемых запросов SELECT, то возвращается SQLITE_LOCKED. Если в этом случае вызывается sqlite3_unlock_notify(), то указанный обратный вызов будет вызван немедленно. Повторная попытка выполнить операцию "DROP TABLE" или "DROP INDEX" вернёт другую ошибку SQLITE_LOCKED. В представленной слева реализации sqlite3_blocking_step() это может привести к бесконечному циклу.

Вызывающая сторона может отличить этот особый случай "DROP TABLE|INDEX" от других случаев, используя расширенные коды ошибок. Когда уместно вызвать sqlite3_unlock_notify(), расширенный код ошибки — SQLITE_LOCKED_SHAREDCACHE. В противном случае, в случае "DROP TABLE|INDEX", это просто обычная ошибка SQLITE_LOCKED. Другим решением может быть ограничение количества раз, когда какой-либо отдельный запрос может быть повторён (скажем, до 100). Хотя это может быть менее эффективным, чем хотелось бы, подобная ситуация, скорее всего, не часто встречается.

Последнее изменение этой страницы состоялось 2022-01-08 05:02:57 UTC

SQLite is in the Public Domain.
https://sqlite.org/unlock_notify.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API