RussianLDP Рейтинг@Mail.ru
WebMoney: 
WMZ Z294115950220 
WMR R409981405661 
WME E134003968233 
Visa 
4274 3200 2453 6495 

Small. Fast. Reliable.
Choose any three.

Применение sqlite3_unlock_notify() API


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

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

Хотя это менее распространено, вызов sqlite3_prepare() или sqlite3_prepare_v2() тоже может вернуть SQLITE_LOCKED, если это не может получить блокировку read на таблице 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, это указывает, что блокировка завела бы в тупик систему.

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

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

sqlite3_unlock_notify() API

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

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

Когда отзыв unlock-notify выпущен, это выпущено из вызова sqlite3_step() (или sqlite3_close()), связанного со связью блокирования. Незаконно вызвать любую функцию API 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 callback вызван другим потоком.

  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 (перезагрузка дескриптора запроса) пропущен.

Ожидание записи

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

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

  • Действующий писатель завершает его транзакцию или
  • Количество открытых транзакций чтения на общем кэше опускается до нуля.

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

pthreads API

Когда 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(). Эта функция позволяет одновременно выпускать mutex и начинать ждать асинхронного сигнала. Используя эту функцию, флаг fired и mutex, состояние состязания, описанное выше, может быть устранено следующим образом:

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

  1. Получает mutex.
  2. Устанавливает флаг "fired" = true.
  3. Пытается сигнализировать об ожидании потока.
  4. Выпускает mutex.

Когда поток wait_for_unlock_notify() готов начать ждать отзыв unlock-notify:

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

Таким образом, не имеет значения, если 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). Хотя это могло бы быть менее эффективно, чем можно было бы пожелать, рассматриваемая ситуация вряд ли будет часто происходить.