Small. Fast. Reliable.
Choose any three.

使用sqlite3_unlock_notify()API

/ *此示例使用pthreads API * /
#include <pthread.h>

/ * 
**
在注册解锁通知回调时,指向
此结构实例的指针作为用户上下文**指针传递。* /
typedef struct UnlockNotification UnlockNotification;
struct UnlockNotification {
  被解雇                         / *发生解锁事件后为真* / 
  pthread_cond_t cond;               / *要在* / 
  pthread_mutex_t互斥锁上等待的条件变量/ *互斥保护结构* /
};

/ * 
**此函数是向SQLite注册的解锁通知回调。
* /
静态void unlock_notify_cb(void ** apArg,int nArg){
  我
  for(i = 0; i <nArg; i ++){
    UnlockNotification * p =(UnlockNotification *)apArg [i];
    pthread_mutex_lock(&p-> mutex);
    p->解雇= 1;
    pthread_cond_signal(&p-> cond);
    pthread_mutex_unlock(&p-> mutex);
  }
}

/ * 
**此函数假定SQLite API调用(sqlite3_prepare_v2()
**或sqlite3_step())刚刚返回了SQLITE_LOCKED。参数是
**关联的数据库连接。
** 
**该函数调用sqlite3_unlock_notify()来注册
**解锁通知回调,然后阻塞直到该回调被传递
**并返回SQLITE_OK。然后,调用方应重试失败的操作。
** 
**或者,如果sqlite3_unlock_notify()表示要阻塞将使
系统死锁,则此函数立即返回SQLITE_LOCKED。在
**在这种情况下,调用方不应重试该操作,而应回滚
当前事务(如果有)。
* / 
static int wait_for_unlock_notify(sqlite3 * db){
  int rc;
  UnlockNotification un;

  / *初始化UnlockNotification结构。* /
  未解雇= 0;
  pthread_mutex_init(&un.mutex,0);
  pthread_cond_init(&un.cond,0);

  / *注册一个解锁通知回调。* / 
  rc = sqlite3_unlock_notify(db,unlock_notify_cb,(void *)&un);
  assert(rc == SQLITE_LOCKED || rc == SQLITE_OK);

  / *调用sqlite3_unlock_notify()始终返回SQLITE_LOCKED 
  **或SQLITE_OK。
  ** 
  **如果返回SQLITE_LOCKED,则系统处于死锁状态。在这种
  情况下,此函数需要将SQLITE_LOCKED返回给调用方,以便
  可以回滚当前事务。否则,阻塞
  **直到调用unlock-notify回调,然后返回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);
  }

  / *销毁互斥量和条件变量。* /
  pthread_cond_destroy(&un.cond);
  pthread_mutex_destroy(&un.mutex);

  返回rc;
}

/ * 
**此函数是SQLite函数sqlite3_step()的包装。
**它的功能与step()相同,不同之处在于,如果
无法获得
所需的**共享缓存锁,则此函数可能会阻止等待**锁可用。在这种情况下,正常的API step()
**函数始终返回SQLITE_LOCKED。
** 
**如果此函数返回SQLITE_LOCKED,则调用者应回滚
**当前事务(如果有),然后稍后重试。否则,
**系统可能会陷入僵局。
* / 
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)中断;
    sqlite3_reset(pStmt);
  }
  返回rc;
}

/ * 
**此函数是SQLite函数sqlite3_prepare_v2()的包装。
**它的功能与prepare_v2()相同,不同之处在于,如果
无法获得
所需的**共享缓存锁,则此函数可能会阻止等待**锁可用。在这种情况下,常规API prepare_v2()
**函数始终返回SQLITE_LOCKED。
** 
**如果此函数返回SQLITE_LOCKED,则调用者应回滚
**当前事务(如果有),然后稍后重试。否则,
**系统可能会陷入僵局。
* /
int sqlite3_blocking_prepare_v2(
  sqlite3 * db,               / *数据库句柄。* / 
  const char * zSql,          / * UTF-8编码的SQL语句。* / 
  int nSql,                  / * zSql的长度(以字节为单位)。* / 
  sqlite3_stmt ** ppStmt,     / * OUT:指向准备好的语句的指针* / 
  const char ** pz            / * OUT:解析字符串的结尾* /
){
  int rc;
  while(SQLITE_LOCKED ==((rc = sqlite3_prepare_v2(db,zSql,nSql,ppStmt,pz))){
    rc = wait_for_unlock_notify(db);
    if(rc!= SQLITE_OK)中断;
  }
  返回rc;
}

当两个或多个连接以共享缓存模式访问同一数据库时,将使用单个表上的读写锁(共享和互斥)来确保并发执行的事务保持隔离。在写表之前,必须在该表上获得写(独占)锁。在读取之前,必须获得读取(共享)锁。连接在结束事务时将释放所有保留的表锁。如果连接无法获得所需的锁定,则对sqlite3_step()的调用将返回SQLITE_LOCKED。

尽管不太常见,但如果调用sqlite3_prepare()sqlite3_prepare_v2()不能在每个附加数据库的sqlite_schema表上获得读取锁,则也可能返回SQLITE_LOCKED 。这些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来注册解锁通知回调。持有表锁的数据库连接阻止了成功调用sqlite3_step()sqlite3_prepare_v2()的事务完成并释放了所有锁之后,SQLite会调用SQLite调用unlock-notify回调。例如,如果对sqlite3_step()的调用是尝试从表X读取的,并且某个其他连接Y对表X持有写锁定,则sqlite3_step()将返回SQLITE_LOCKED。如果sqlite3_unlock_notify()然后调用,在连接Y的事务结束后将调用unlock-notify回调。解锁通知回调正在等待的连接(在这种情况下为连接Y)被称为“阻塞连接”。

如果对sqlite3_step()的尝试尝试写入数据库表的调用返回SQLITE_LOCKED,则可能有一个以上的其他连接对该数据库表持有读锁定。在这种情况下,SQLite可以简单地任意选择其他连接之一,并在该连接的事务完成时发出解锁通知回调。无论是否通过一个或多个连接阻止了对sqlite3_step()的调用,当发出相应的unlock-notify回调时,不能保证所需的锁可用,只有这样可能。

发出解锁通知回调时,它是在对与阻塞连接关联的sqlite3_step()(或sqlite3_close())的调用中发出的。从解锁通知回调中调用任何sqlite3_XXX()API函数都是非法的。预期的用途是,unify-notify回调将发出信号通知其他正在等待的线程或安排某些操作在以后进行。

sqlite3_blocking_step()函数使用的算法如下:

  1. 在提供的语句句柄上调用sqlite3_step()。如果调用返回的不是SQLITE_LOCKED,则将此值返回给调用方。否则,请继续。

  2. 在与提供的语句句柄关联的数据库连接句柄上调用sqlite3_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()注册进行解锁通知回调,则阻塞连接是当前在共享缓存上具有打开的写事务的连接。因为如果没有新的读取事务可能被打开并且假设所有现有的读取事务最终都结束了,那么写入程序将最终有机会获得所需的写入锁定,从而避免了写入程序的匮乏。

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()函数。此功能允许调用者同时释放互斥锁并开始等待异步信号。使用此功能,“触发”标志和互斥锁,可以消除上述竞争条件,如下所示:

当调用解锁通知回调时(可能在调用sqlite3_unlock_notify()的线程开始等待异步信号之前),它将执行以下操作:

  1. 获取互斥量。
  2. 将“已触发”标志设置为true。
  3. 尝试发信号通知正在等待的线程。
  4. 释放互斥锁。

当wait_for_unlock_notify()线程准备开始等待解锁通知回调到达时,它:

  1. 获取互斥量。
  2. 检查是否已设置“已触发”标志。如果是这样,则已经调用了解锁通知回调。释放互斥并继续。
  3. 以原子方式释放互斥锁,并开始等待异步信号。信号到达后,继续。

这样,当wait_for_unlock_notify()线程开始阻塞时,是否已经调用或正在调用unlock-notify回调并不重要。

可能的增强

本文中的代码至少可以通过两种方式进行改进:

即使sqlite3_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)。尽管这样做的效率可能不如人们希望的那样,但是所讨论的情况不太可能经常发生。