1. 程式人生 > >SQLite剖析(10):非同步IO模式、共享快取模式和解鎖通知

SQLite剖析(10):非同步IO模式、共享快取模式和解鎖通知

    下面介紹SQLite的一些擴充套件模組。本文整理自http://sqlite.org/docs.html。
    1、非同步I/O模式
    通常,當SQLite寫一個數據庫檔案時,會等待,直到寫操作完成,然後控制返回到呼叫程式。相比於CPU操作,寫檔案系統是非常耗時的,這是一個性能瓶頸。非同步I/O後端是SQLite的一個擴充套件模組,允許SQLite使用一個獨立的後臺執行緒來執行所有的寫請求。雖然這並不會減少整個系統的資源消耗(CPU,磁碟頻寬等),但它允許SQLite在正在寫資料庫時立刻返回到呼叫者,從使用者角度看,無疑提高了前端的響應速度。對非同步I/O,寫請求在一個獨立的後臺執行緒中被處理,這意味著啟動資料庫寫操作的執行緒不必等待磁碟I/O的發生。寫操作看起來似乎很快就發生了,但實際上速度跟通常是一樣的,只不過在後臺進行。
    非同步I/O似乎提供了更好的響應能力,但這是有代價的。你會失去ACID中的永續性(Durable)屬性。在SQLite的預設I/O後端中,一旦寫操作完成,你知道更改的資料已經安全地在磁碟上了。而非同步I/O卻不是這樣的情況。如果應用程式在資料寫操作之後,非同步寫執行緒完成之前發生崩潰或掉電,則資料庫更改可能根本沒有被寫到磁碟,下一次使用資料庫時就看不到更改。
    非同步I/O失去了永續性,但仍然保持ACID的其他三個屬性:原子性(Atomic)、一致性(Consistent)和隔離性(Isolated)。很多應用程式沒有永續性也能很好地工作。
    我們通過建立一個SQLite VFS物件並且用sqlite3_vfs_register()註冊它來使用非同步I/O模式。當用這個VFS開啟資料庫檔案並進行寫操作時(使用vfs的xWrite()方法),資料不會立刻寫到磁碟,而是放在由後臺執行緒維護的寫佇列中。當用非同步VFS開啟資料庫檔案並進行讀操作時(使用vfs的xRead()方法),資料從磁碟讀出,而寫佇列從vfs讀程序的角度看,其xWrite()已經完成了。非同步I/O的虛擬檔案系統(VFS)通過sqlite3async_initialize()來註冊,通過sqlite3async_shutdown()來關閉。
    為了積累經驗,非同步I/O的實現有意保持簡單。更多的功能會在將來的版本中新增。例如,在當前的實現中,如果寫操作正在一個穩定的流上發生,而這個流超過了後臺寫執行緒的I/O能力,則掛起的寫操作佇列將會無限地增長,可能會耗盡主機系統的記憶體。複雜一點的模組則可以跟蹤掛起的寫運算元量,在超過一定數目後停止接收新的寫請求。
    在單個程序中、使用非同步IO的多個連線可以併發地訪問單個數據庫。從使用者的角度看,如果所有連線都位於單個程序中,則正常SQLite和使用非同步IO的SQLite,其併發性並沒有什麼不同。如果檔案鎖是啟用的(預設是啟用的),來自多個程序的連線都要讀和寫資料庫檔案,則併發性在下面的情況下會減弱:
    (1)當使用非同步IO的連線啟動一個數據庫事務時,資料庫會立刻被鎖住。然而鎖只有在寫佇列中的所有操作已經重新整理到磁碟後才能釋放。這意味著有時即使在一個"COMMIT"或"ROLLBACK"執行完後,資料庫可能仍然處於鎖住狀態。
    (2)如果應用程式使用非同步IO連續地執行多個事務,其他資料庫使用者可能會因為資料庫一直被鎖住而不能使用資料庫。這是因為當一個BEGIN執行後,資料庫鎖會立刻建立起來。但當對應的COMMIT或ROLLBACK發生時,鎖不一定釋放了,要到後臺寫佇列全部重新整理到磁碟後才能釋放。如果後臺寫佇列還沒重新整理完,資料庫就一直處於鎖住狀態,其他程序不能訪問資料庫。
    檔案鎖可以在執行時通過sqlite3async_control()函式禁用。對NFS這可以提高效能,因為可以避免對伺服器的來回非同步操作建立檔案鎖。但是如果多個連線嘗試訪問同一個資料庫,而檔案鎖被禁用了,則應用程式崩潰和資料庫損壞就可能發生。
    非同步IO擴充套件模組由單個原始檔sqlite3async.c,和一個頭檔案sqlite3async.h組成,位於原始碼樹的ext/async/子目錄下。應用程式可以用其中定義的C API來啟用和控制這個模組的功能。為了使用非同步IO擴充套件,把sqlite3async.c編譯成使用SQLite的應用程式的一部分,然後使用sqlite3async.h中定義的API來初始化和配置這個模組。這些API在sqlite3async.h的註釋中有詳細說明,使用這些API通常有以下步驟:
    (1)呼叫sqlite3async_initialize()來給SQLite註冊非同步IO VFS(虛擬檔案系統)。
    (2)建立一個後臺執行緒來執行寫操作,並呼叫sqlite3async_run()。
    (3)通過非同步IO VFS,使用正常的SQLite API來讀寫資料庫。
    當前的非同步IO擴充套件相容win32系統和支援pthread介面的系統,包括Mac OS X, Linux和其他Unix變體。為了移植非同步IO擴充套件到其他的平臺,使用者必須在新平臺上實現互斥鎖和條件變數原語。當前並沒有外部可用介面來允許做這樣的控制,但是修改sqlite3async.c中的程式碼以包含新平臺的併發控制原語是相當容易的,更多細節可搜尋sqlite3async.c中的註釋串"PORTING FUNCTIONS"。然後實現下面這些函式的新版本:
    static void async_mutex_enter(int eMutex);
    static void async_mutex_leave(int eMutex);
    static void async_cond_wait(int eCond, int eMutex);
    static void async_cond_signal(int eCond);
    static void async_sched_yield(void);
    上面這些函式的功能在sqlite3async.c的註釋中有詳細描述。
    2、共享快取模式

    從3.3.0版開始,SQLite包含一個特別的“共享快取”模式(預設情況下禁用),主要用在嵌入式伺服器中。如果共享快取模式啟用,並且一個執行緒在同一個資料庫上建立多個連線,則這些連線共享一個數據和模式快取。這能夠顯著減少系統的記憶體和IO消耗。在3.5.0版中,共享快取模式被修改以便同一快取的共享可以跨越整個程序而不只是單個執行緒。在這個修改之前,線上程間傳遞資料連線是受限制的。從3.5.0版開始這個限制就消除了。

    從另一個程序或執行緒的角度看,使用共享快取的兩個或多個數據庫連線看起來就像是一個連線。鎖協議用來在多個共享快取或資料庫使用者之間進行仲裁。

圖1 共享快取模式

    圖1描述一個執行時配置的例子,有三個資料庫連線。連線1是一個正常的SQLite資料庫連線,連線2和3共享一個快取。正常的鎖協議用來在連線1和共享快取之間序列化資料庫訪問。而連線2和連線3對共享快取訪問的序列化則有專門的內部協議。見下面的描述。
    有三個級別的共享快取加鎖模型,事務級別的加鎖,表級別的加鎖和模式級別的加鎖。
    (1)事務級別的加鎖


    SQLite連線可能開啟兩種型別的事務,讀事務和寫事務。這不是顯式完成的,一個事務隱式地含有一個讀事務,直到它首次寫一個數據庫檔案,這時成為一個寫事務。在任何時候共享快取上最多隻能有一個連線開啟一個寫事務,這個寫事務可以和任何數量的讀事務共存。這與非共享快取模式不同,非共享快取模式下有讀操作時不允許有寫操作。
    (2)表級別的加鎖
    當兩個或更多的連線使用一個共享快取,用鎖來序列化每個表格的併發訪問。表支援兩種型別的鎖,讀鎖和寫鎖。鎖被授予連線,任何時候每個資料庫連線上的每個表格可以有讀鎖、寫鎖或沒有鎖。一個表格上可以任何數量的讀鎖,但只能有一個寫鎖。讀資料庫表格時必須首先獲得一個讀鎖。寫表格時必須獲得一個寫鎖。如果不能獲取需要的鎖,查詢失敗並返回SQLITE_LOCKED給呼叫者。表級別的鎖在獲取之後,要到當前事務(讀或寫)結束時才釋放。
    如果使用read_uncommitted pragma指令把事務隔離模式從序列(serialized,預設模式,即查詢資料時會加上共享瑣,阻塞其他事務修改真實資料)改成允許髒讀(read-uncommitted,即SELECT會讀取其他事務修改而還沒有提交的資料),則上面描述的行為會有稍許的變化。事務隔離模式還有另外兩種,無法重複讀read-comitted是同一個事務中兩次執行同樣的查詢語句,若在第一次與第二次查詢之間時間段,其他事務又剛好修改了其查詢的資料且提交了,則兩次讀到的資料不一致。可以重複讀read-repeatable是指同一個事務中兩次執行同樣的查詢語句,得到的資料始終都是一致的。

  1. /* Set the value of the read-uncommitted flag: 
  2.   ** 
  3.   **   True  -> Set the connection to read-uncommitted mode. 
  4.   **   False -> Set the connection to serialized (the default) mode. 
  5.   */
  6.   PRAGMA read_uncommitted = <boolean>;  
  7.   /* Retrieve the current value of the read-uncommitted flag */
  8.   PRAGMA read_uncommitted;  
    允許髒讀模式的資料庫連線在讀資料庫表時不會獲取讀鎖,如果這時另外一個數據庫連線修改了正在被讀的表資料,則可能導致查詢結果不一致,因為允許髒讀模式的讀事務不會被打斷。允許髒讀模式不會影響寫事務,它必須獲取寫鎖,因此資料庫寫操作可以被阻塞。允許髒讀模式也不會影響sqlite_master級別的鎖。
    (3)模式(sqlite_master)級別的加鎖
    sqlite_master表支援與其他資料庫表相同的共享快取讀鎖和寫鎖。還會使用下面的特殊規則:
    * 在訪問任何資料庫表格或者獲取任何其他的讀鎖和寫鎖之前,連線必須先獲取一個sqlite_master表上的讀鎖。
    * 在執行修改資料庫模式的語句(例如CREATE TABLE或DROP TABLE)之前,連線必須先獲取一個sqlite_master表上的寫鎖。
    * 如果任何其他的連線持有關聯資料庫(包括預設的主資料庫)的sqlite_master表上的寫鎖,則連線不可以編譯一個SQL語句。
    在SQLite 3.3.0到3.4.2之間,資料庫連線只能被呼叫sqlite3_open()建立它的執行緒使用,一個連線只能與同一執行緒中的其他連線共享快取。從SQLite 3.5.0開始,這個限制消除了。在老版本的SQLite上,共享快取模式不能使用在虛擬表上,從SQLite 3.6.17開始,這個限制消除了。
    共享快取模式在每個程序級別上啟用。C介面int sqlite3_enable_shared_cache(int)用來全域性地啟用或禁用共享快取模式。每次呼叫sqlite3_enable_shared_cache()影響後續的使用sqlite3_open(), sqlite3_open16()或sqlite3_open_v2()建立的資料庫連線,已經存在的資料庫連線則不受影響。每次sqlite3_enable_shared_cache()的呼叫覆蓋程序上的前面各次呼叫。
    使用sqlite3_open_v2()建立的單個數據庫連線,通過在第三個引數上使用SQLITE_OPEN_SHAREDCACHE或SQLITE_OPEN_PRIVATECACHE標誌,可能選擇參與或不參與共享快取模式。在該資料庫連線上這些標誌會覆蓋全域性的sqlite3_enable_shared_cache()設定。如果同時使用這兩個標誌,則行為是未定義的。
    當使用URI檔名時,"cache"查詢引數可以用來指定連線是否使用共享快取模式。"cache=shared"啟用共享快取,"cache=private"禁用共享快取。例如:
    ATTACH 'file:aux.db?cache=shared' AS aux;
    從SQLite 3.7.13開始,倘若資料庫使用URI檔名建立,共享快取模式可以在記憶體資料庫上使用。為了向後相容,使用未修飾的":memory:"名稱開啟記憶體資料庫時預設是禁用共享快取的。而在SQLite 3.7.13之前,無論使用的記憶體資料庫名、當前系統的共享快取設定、以及查詢引數或標誌是什麼,記憶體資料庫上共享快取總是被禁用的。
    在記憶體資料庫上啟用共享快取,會允許同一程序上的兩個或更多資料庫連線訪問同一段記憶體。當最後一個連線關閉時,記憶體資料庫會自動刪除,這段記憶體也會被重置。
    3、解鎖通知
    當多個連線在共享快取模式下訪問同一個資料庫時,單個表上的讀和寫鎖(即共享和排他鎖)用來確保併發執行的事務是隔離的。如果連線不能獲取到需要的鎖,sqlite3_step()呼叫返回SQLITE_LOCKED。如果不能獲取到每個關聯資料庫的sqlite_master表上的讀鎖(雖然這種情況並不常見),sqlite3_prepare()或sqlite3_prepare_v2()呼叫也會返回SQLITE_LOCKED。
    通過使用SQLite的sqlite3_unlock_notify()介面,我們可以讓sqlite3_step()或sqlite3_prepare_v2()呼叫阻塞直到獲得需要的鎖,而不是立刻返回SQLITE_LOCKED。下面的例子展示解鎖通知的使用。
  1. /* 本例子使用pthreads API */
  2. #include <pthread.h>
  3. /* 
  4. ** 當註冊一個解鎖通知時,傳遞本結構例項的指標,以作為使用者上下文中的例項 
  5. */
  6. typedefstruct UnlockNotification UnlockNotification;  
  7. struct UnlockNotification {  
  8.   int fired;                         /* 在解鎖事件發生後為True */
  9.   pthread_cond_t cond;               /* 要等待的條件變數 */
  10.   pthread_mutex_t mutex;             /* 保護本結構的互斥量 */
  11. };  
  12. /* 
  13. ** 解鎖通知回撥函式 
  14. */
  15. staticvoid unlock_notify_cb(void **apArg, int nArg){  
  16.   int i;  
  17.   for(i=0; i<nArg; i++){  
  18.     UnlockNotification *p = (UnlockNotification *)apArg[i];  
  19.     pthread_mutex_lock(&p->mutex);  /* 對臨界區加鎖 */
  20.     p->fired = 1;  /* 觸發解鎖事件,本變數只能互斥訪問 */
  21.     pthread_cond_signal(&p->cond);  
  22.     pthread_mutex_unlock(&p->mutex);  
  23.   }  
  24. }  
  25. /* 
  26. ** 本函式假設SQLite API呼叫(sqlite3_prepare_v2()或sqlite3_step())返回SQLITE_LOCKED。 
  27. ** 引數為關聯的資料庫連線。 
  28. ** 本函式呼叫sqlite3_unlock_notify()註冊一個解鎖通知回撥函式,然後阻塞直到 
  29. ** 回撥函式執行完並返回SQLITE_OK。呼叫者應該重試失敗的操作。 
  30. ** 或者,如果sqlite3_unlock_notify()指示阻塞將會導致系統死鎖,則本函式立刻 
  31. ** 返回SQLITE_LOCKED。呼叫者不應該重試失敗的操作,而是回滾當前事務 
  32. */
  33. staticint wait_for_unlock_notify(sqlite3 *db){  
  34.   int rc;  
  35.   UnlockNotification un;  
  36.   /* 初始化UnlockNotification結構 */
  37.   un.fired = 0;  
  38.   pthread_mutex_init(&un.mutex, 0);  
  39.   pthread_cond_init(&un.cond, 0);  
  40.   /* 註冊一個解鎖通知回撥函式 */
  41.   rc = sqlite3_unlock_notify(db, unlock_notify_cb, (void *)&un);  
  42.   assert( rc==SQLITE_LOCKED || rc==SQLITE_OK );  
  43.   /* sqlite3_unlock_notify()呼叫總是返回SQLITE_LOCKED或SQLITE_OK。 
  44.   ** 如果返回SQLITE_LOCKED,則系統死鎖。本函式需要返回SQLITE_LOCKED給呼叫者以 
  45.   ** 便當前事務能夠回滾。否則阻塞直到解鎖通知回撥函式執行,然後返回SQLITE_OK 
  46.   */
  47.   if( rc==SQLITE_OK ){  
  48.     pthread_mutex_lock(&un.mutex);  
  49.     if( !un.fired ){ /* 如果解鎖事件沒有發生,則阻塞 */
  50.       pthread_cond_wait(&un.cond, &un.mutex);  
  51.     }  
  52.     pthread_mutex_unlock(&un.mutex);  
  53.   }  
  54.   /* 銷燬互斥量和條件變數 */
  55.   pthread_cond_destroy(&un.cond);  
  56.   pthread_mutex_destroy(&un.mutex);  
  57.   return rc;  
  58. }  
  59. /* 
  60. ** 本函式是SQLite函式sqlite3_step()的包裝,它的工作方式與sqlite3_step()相同。 
  61. ** 但如果沒有獲得共享快取鎖,則本函式阻塞以等待鎖可用。 
  62. ** 如果本函式返回SQLITE_LOCKED,呼叫者應該回滾當前事務,之後再嘗試。否則系統可能死鎖了 
  63. */
  64. int sqlite3_blocking_step(sqlite3_stmt *pStmt){  
  65.   int rc;  
  66.   while( SQLITE_LOCKED==(rc = sqlite3_step(pStmt)) ){  
  67.     rc = wait_for_unlock_notify(sqlite3_db_handle(pStmt));  
  68.     if( rc!=SQLITE_OK ) break;  
  69.     sqlite3_reset(pStmt);  
  70.   }  
  71.   return rc;  
  72. }  
  73. /* 
  74. ** 本函式是SQLite函式sqlite3_prepare_v2()的包裝,它的工作方式與sqlite3_prepare_v2()相同。 
  75. ** 但如果沒有獲得共享快取鎖,則本函式阻塞以等待鎖可用。 
  76. ** 如果本函式返回SQLITE_LOCKED,呼叫者應該回滾當前事務,之後再嘗試。否則系統可能死鎖了 
  77. */
  78. int sqlite3_blocking_prepare_v2(  
  79.   sqlite3 *db,              /* 資料庫控制代碼 */
  80.   constchar *zSql,         /* UTF-8編碼的SQL語句 */
  81.   int nSql,                 /* zSql的位元組數 */
  82.   sqlite3_stmt **ppStmt,    /* OUT: 指向預處理語句的指標 */
  83.   constchar **pz           /* OUT: 解析過的字串尾部位置 */
  84. ){  
  85.   int rc;  
  86.   while( SQLITE_LOCKED==(rc = sqlite3_prepare_v2(db, zSql, nSql, ppStmt, pz)) ){  
  87.     rc = wait_for_unlock_notify(db);  
  88.     if( rc!=SQLITE_OK ) break;  
  89.   }  
  90.   return rc;  
  91. }  
    如果例子中的sqlite3_blocking_step()或sqlite3_blocking_prepare_v2()函式返回SQLITE_LOCKED,則表明阻塞將導致系統死鎖。
    只有在編譯時定義預處理巨集SQLITE_ENABLE_UNLOCK_NOTIFY,才能使用sqlite3_unlock_notify()介面。該介面被設計成用在這樣的系統中:每個資料庫連線分配單獨的執行緒。如果在一個執行緒中執行多個數據庫連線,則不能使用該介面。sqlite3_unlock_notify()介面一次只在一個執行緒上工作,因此上面的鎖控制邏輯只能工作於一個執行緒的單個數據庫連線上。
    上面的例子中,在sqlite3_step()或sqlite3_prepare_v2()返回SQLITE_LOCKED後,sqlite3_unlock_notify()被呼叫以註冊一個解鎖通知回撥函式。在資料庫連線持有表級別的鎖後,解鎖通知函式被執行以防止sqlite3_step()或sqlite3_prepare_v2()隨後完成事務並釋放所有鎖。例如,如果sqlite3_step()嘗試讀表格X,而其他某個連線Y正持有表格X的寫鎖,sqlite3_step()將返回SQLITE_LOCKED。如果隨後呼叫sqlite3_unlock_notify(),解鎖通知函式將在連線Y的事務結束後被呼叫。解鎖通知函式正在等待的連線(這裡的Y),被稱為“阻塞式連線”。
    如果sqlite3_step()嘗試寫一個數據庫,但返回SQLITE_LOCKED,則可能有多個程序持有當前資料庫表格的讀鎖。這時SQLite隨意地選擇其中的一個連線,當這個連線的事務完成時執行解鎖通知函式。解鎖通知函式從sqlite3_step()(或sqlite3_close())裡執行,它關聯有一個阻塞式程序。解鎖通知函式裡面可以呼叫任何的sqlite3_XXX()函式,可以向其他等待執行緒發訊號,或者安排一些在以後要發生的行為。
    sqlite3_blocking_step()函式使用的演算法描述如下:
    (1)在指定的SQL語句物件上呼叫sqlite3_step(),如果返回除SQLITE_LOCKED之外的值,則直接返回這個值給呼叫者。如果返回SQLITE_LOCKED則繼續。
    (2)呼叫sqlite3_unlock_notify()註冊一個解鎖通知回撥函式。如果sqlite3_unlock_notify()返回SQLITE_LOCKED,說明系統死鎖,返回這個值給呼叫者以便回滾。否則繼續。
    (3)阻塞,直到解鎖通知函式被另外一個執行緒執行。
    (4)在SQL語句物件上呼叫sqlite3_reset()。因為SQLITE_LOCKED錯誤可能只發生在第一次呼叫sqlite3_step()時(不可能有sqlite3_step()先返回SQLITE_ROW而下一次卻返回SQLITE_LOCKED的情況)。這時SQL語句物件會被重置,從而不會影響查詢結果。如果不呼叫sqlite3_reset(),下一次呼叫sqlite3_step()將返回SQLITE_MISUSE。
    (5)轉向步驟(1)。
    sqlite3_blocking_prepare_v2()使用的演算法也類似,只不過第4步(重置SQL語句物件)忽略。
    對於“寫飢餓”現象,SQLite能幫助應用程式避免出現寫飢餓的情況。當在一個表上獲取寫鎖的任何嘗試失敗後(因為有連線一直持有讀鎖),共享快取上啟動新事務的所有嘗試都會失敗,直到下面有一種情況變成True為止:
    * 當前寫事務完成,或者
    * 共享快取上開啟的讀事務數量減為0。
    啟動新的讀事務失敗會返回SQLITE_LOCKED給呼叫者。如果呼叫者然後呼叫sqlite3_unlock_notify()註冊一個解鎖通知函式,阻塞式連線當前在共享快取上會有一個寫事務。這就避免了寫飢餓,因為沒有新的讀鎖可以打開了。當所有存在的讀鎖完成時,寫操作最終能有機會獲得需要的寫鎖。
    在wait_for_unlock_notify()呼叫sqlite3_unlock_notify()時,有可能阻塞式執行緒已經完成它的事務,這樣在sqlite3_unlock_notify()返回前解鎖通知函式會立刻被呼叫。解鎖通知函式也有可能被另一個執行緒呼叫,正好發生在sqlite3_unlock_notify()呼叫之後,而在這個執行緒開始等待非同步訊號之前。這樣的競爭條件怎麼處理,取決於應用程式使用的執行緒和同步原語。本例子中使用pthread,這是現代UNIX風格的系統(包括Linux)提供的介面。
    pthread提供pthread_cond_wait()函式,它允許呼叫者同時釋放一個互斥量並開始等待一個非同步訊號。使用這個函式、一個"fired"標誌和一個互斥量,競爭狀態可以消除,如下:
    當解鎖通知函式被呼叫時,這可能發生在呼叫sqlite3_unlock_notify()的執行緒開始等待一個非同步訊號之前,它做下面的工作:
    (1)獲取互斥量。
    (2)設定"fired"標誌為true。
    (3)向等待執行緒發訊號。
    (4)釋放互斥量。
    當wait_for_unlock_notify()執行緒開始等待解鎖通知函式到達時,它:
    (1)獲取互斥量。
    (2)檢查"fired"標誌是否設定。如果已設定,解鎖通知函式已經被呼叫,直接釋放互斥量,然後繼續。
    (3)如果沒設定,原子性地釋放互斥量,並開始等待非同步訊號。當訊號到達時,繼續。
    通過這種方式,當wait_for_unlock_notify()開始阻塞時,解鎖通知函式不管是已經被呼叫,還是正在被呼叫,都沒有問題。
    本文例子中的程式碼至少在以下兩個方面可以改進:
    * 能管理執行緒優先順序。
    * 能處理SQLITE_LOCKED的特殊情形,這可能發生在刪除一個表或索引時。
    雖然sqlite3_unlock_notify()只允許呼叫者指定單個的使用者上下文指標,但一個解鎖通知回撥是傳給這種上下文指標陣列的。這是因為當一個阻塞式執行緒完成它的事務時,如果有多個解鎖通知被註冊用於呼叫同一個C函式,則上下文指標就要排列成一個數組。如果每個執行緒分配一個優先順序,則高優先順序的執行緒就會比低優先順序的執行緒先得到訊號通知,而不是以任意的順序來通知執行緒。
    如果執行一個"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次)。雖然這會導致效率低一點,但我們這裡討論的情況並不是經常發生的。