SQLite Unlock-Notify API
Using the 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;
}
当两个或更多连接以共享高速缓存模式访问同一数据库时,将使用对各个表的读和写(共享和排它)锁来确保并发执行的事务处于隔离状态。在写入表之前,必须在该表上获得写(独占)锁。在阅读之前,必须获得读取(共享)锁定。连接在结束其事务时释放所有持有的表锁。如果连接无法获得所需的锁定,则对sqlite3_step()的调用将返回SQLITE_LOCKED。
虽然它不太常见,但如果sqlite3_prepare()或sqlite3_prepare_v2()的调用无法在每个附加数据库的sqlite_master表上获得读锁,则也可能会返回SQLITE_LOCKED。这些API需要读取包含在sqlite_master表中的模式数据,以便将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()接口一次只能在单个连接上工作,因此此处介绍的锁解析逻辑仅适用于每个线程的单个数据库连接。
The sqlite3
_
unlock
_
notify() API
在调用sqlite3_step()或sqlite3_prepare_v2()返回SQLITE_LOCKED之后,可调用sqlite3_unlock_notify()API来注册解锁通知回调。在数据库连接持有表锁并阻止从成功调用sqlite3_step()或sqlite3_prepare_v2()完成其事务并释放所有锁之后,SQLite将调用unlock-notify回调。例如,如果对sqlite3_step()的调用是尝试从表X中读取数据,并且某个其他连接Y在表X上持有写锁定,则sqlite3_step()将返回SQLITE_LOCKED。如果然后调用sqlite3_unlock_notify(),则在连接Y的事务结束后,将调用unlock-notify回调。解锁通知回调正在等待的连接(在本例中为连接Y)被称为“
如果试图写入数据库表的sqlite3_step()调用返回SQLITE_LOCKED,那么多个其他连接可能正在对正在讨论的数据库表进行读锁。在这种情况下,SQLite只是任意选择其中一个连接,并在连接的事务完成时发出unlock-notify回调。无论对sqlite3_step()的调用是否被一个或多个连接阻止,当发出相应的unlock-notify回调时,都不能保证所需的锁可用,而只有它可能使用。
当发出unlock-notify回调时,它会在与阻塞连接关联的sqlite3_step()(或sqlite3_close())的调用中发出。从unlock-notify回调中调用任何sqlite3_XXX()API函数是非法的。预期的用途是解锁通知回调将发出一些其他等待线程的信号或安排稍后发生的某些操作。
sqlite3_blocking_step()函数使用的算法如下所示:
- 在提供的语句句柄上调用sqlite3_step()。如果调用返回SQLITE_LOCKED以外的任何内容,则将该值返回给调用者。否则,继续。
- 调用与提供的语句句柄关联的数据库连接句柄上的sqlite3_unlock_notify()以注册解锁通知回调。如果对unlock_notify()的调用返回SQLITE_LOCKED,则将此值返回给调用者。
- 阻塞直到unlock-notify回调被另一个线程调用。
- 在语句句柄上调用sqlite3_reset()。由于SQLITE_LOCKED错误可能只发生在第一次调用sqlite3_step()时(一次调用sqlite3_step()不可能返回SQLITE_ROW,然后是下一次SQLITE_LOCKED),语句句柄可能会在此时被重置,而不会影响结果从调用者的角度来看的查询。如果此时未调用sqlite3_reset(),则下一次调用sqlite3_step()将返回SQLITE_MISUSE。
- 返回到第1步。
sqlite3_blocking_prepare_v2()函数使用的算法类似,但省略了步骤4(重置语句句柄)。
Writer Starvation
多个连接可能同时持有读锁。如果许多线程正在获取重叠的读锁,则可能是至少有一个线程始终持有读锁。然后等待写锁的表将永远等待。这种情况被称为“作家饥饿”。
SQLite帮助应用程序避免编写器匮乏。任何试图在表上获取写入锁的尝试都失败(因为一个或多个其他连接持有读取锁),则在共享缓存中打开新事务的所有尝试都会失败,直到满足以下条件之一为止:
- 现任作家结束交易,或者其他方法
- 共享缓存中的开放读取事务数量降为零。
无法尝试打开新的读事务将SQLITE_LOCKED返回给调用者。如果调用者然后调用sqlite3_unlock_notify()注册一个unlock-notify回调,则阻塞连接是当前在共享缓存上具有开放写入事务的连接。这可以防止编写器匮乏,因为如果没有新的读取事务可以打开并且假设所有现有的读取事务最终结束,那么作者最终将有机会获得所需的写入锁定。
The pthreads API
在wait_for_unlock_notify()调用sqlite3_unlock_notify()时,阻止sqlite3_step()或sqlite3_prepare_v2()调用成功的阻塞连接可能已完成其事务。在这种情况下,在sqlite3_unlock_notify()返回之前立即调用unlock-notify回调。或者,在调用sqlite3_unlock_notify()之后但在线程开始等待异步发送信号之前,可能会由第二个线程调用unlock-notify回调。
具体如何处理这种潜在的竞争条件取决于应用程序使用的线程和同步原语接口。这个例子使用pthreads,这是由类似UNIX的系统(包括Linux)提供的接口。
pthreads接口提供了pthread_cond_wait()函数。这个函数允许调用者同时释放一个互斥并开始等待一个异步信号。使用这个函数,一个“被触发的”标记和一个互斥体,上面描述的竞态条件可以被消除如下:
当调用unlock-notify回调时,可能会在调用sqlite3_unlock_notify()的线程开始等待异步信号之前执行以下操作:
- 获得互斥体。
- 将“已触发”标志设置为true。
- 尝试发信号通知等待的线程。
- 释放互斥量。
当wait_for_unlock_notify线程准备好开始等待unlock-notify回调到达时,它会:
- 获得互斥体。
- 检查是否已设置“已触发”标志。如果是这样,则解锁通知回调已被调用。释放互斥并继续。
- 从原理上释放互斥并开始等待异步信号。信号到达时,继续。
这样,当wait_for_unlock_notify()线程开始阻塞时,unlock-notify回调是否已经被调用或正在被调用并不重要。
Possible Enhancements
本文中的代码至少可以通过以下两种方式进行改进:
- 它可以管理线程优先级。
- 它可以处理删除表或索引时可能发生的SQLITE_LOCKED特殊情况。
尽管sqlite3_unlock_notify()函数只允许调用者指定单个用户上下文指针,但是一个unlock-notify回调函数会传递一个这样的上下文指针数组。这是因为如果阻塞连接结束其事务时,如果有多个unlock-notify被注册以调用相同的C函数,则将上下文指针编组为一个数组并发出一个回调函数。如果每个线程都被分配了一个优先级,那么不是像这个实现那样只是以任意顺序发信号通知线程,而是可以在较低优先级的线程之前发送更高优先级的线程。
如果执行“DROP TABLE”或“DROP INDEX”SQL命令,并且同一个数据库连接当前有一个或多个主动执行的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)。虽然这可能不如人们想象的那么有效,但这种情况不太可能经常发生。
SQLite在公共领域。