SQLite剖析之異步IO模式、共享緩存模式和解鎖通知
原文轉(zhuǎn)載于:https://www.cnblogs.com/5211314jackrose/p/5816066.html
1、異步I/O模式
?? ?通常,當(dāng)SQLite寫一個數(shù)據(jù)庫文件時,會等待,直到寫操作完成,然后控制返回到調(diào)用程序。相比于CPU操作,寫文件系統(tǒng)是非常耗時的,這是一個性能瓶頸。異步I/O后端是SQLite的一個擴(kuò)展模塊,允許SQLite使用一個獨(dú)立的后臺線程來執(zhí)行所有的寫請求。雖然這并不會減少整個系統(tǒng)的資源消耗(CPU、磁盤帶寬等),但它允許SQLite在正在寫數(shù)據(jù)庫時立刻返回到調(diào)用者,從用戶角度看,無疑提高了前端的響應(yīng)速度。對異步I/O,寫請求在一個獨(dú)立的后臺線程中被處理,這意味著啟動數(shù)據(jù)庫寫操作的線程不必等待磁盤I/O的發(fā)生。寫操作看起來似乎很快就發(fā)生了,但實(shí)際上速度跟通常是一樣的,只不過在后臺進(jìn)行。
?? ?異步I/O似乎提供了更好的響應(yīng)能力,但這是有代價的。你會失去ACID中的持久性(Durable)屬性。在SQLite的缺省I/O后端中,一旦寫操作完成,你知道更改的數(shù)據(jù)已經(jīng)安全地在磁盤上了。而異步I/O卻不是這樣的情況。如果應(yīng)用程序在數(shù)據(jù)寫操作之后,異步寫線程完成之前發(fā)生崩潰或掉電,則數(shù)據(jù)庫更改可能根本沒有被寫到磁盤,下一次使用數(shù)據(jù)庫時就看不到更改。
?? ?異步I/O失去了持久性,但仍然保持ACID的其他三個屬性:原子性(Atomic)、一致性(Consistent)和隔離性(Isolated)。很多應(yīng)用程序沒有持久性也能很好地工作。
?? ?我們通過創(chuàng)建一個SQLite VFS對象并且用sqlite3_vfs_register()注冊它來使用異步I/O模式。當(dāng)用這個VFS打開數(shù)據(jù)庫文件并進(jìn)行寫操作時(使用vfs的xWrite()方法),數(shù)據(jù)不會立刻寫到磁盤,而是放在由后臺線程維護(hù)的寫隊(duì)列中。當(dāng)用異步VFS打開數(shù)據(jù)庫文件并進(jìn)行讀操作時(使用vfs的xRead()方法),數(shù)據(jù)從磁盤讀出,而寫隊(duì)列從vfs讀進(jìn)程的角度看,其xWrite()已經(jīng)完成了。異步I/O的虛擬文件系統(tǒng)(VFS)通過sqlite3async_initialize()來注冊,通過sqlite3async_shutdown()來關(guān)閉。
?? ?為了積累經(jīng)驗(yàn),異步I/O的實(shí)現(xiàn)有意保持簡單。更多的功能會在將來的版本中添加。例如,在當(dāng)前的實(shí)現(xiàn)中,如果寫操作正在一個穩(wěn)定的流上發(fā)生,而這個流超過了后臺寫線程的I/O能力,則掛起的寫操作隊(duì)列將會無限地增長,可能會耗盡主機(jī)系統(tǒng)的內(nèi)存。復(fù)雜一點(diǎn)的模塊則可以跟蹤掛起的寫操作數(shù)量,在超過一定數(shù)目后停止接收新的寫請求。
?? ?在單個進(jìn)程中、使用異步IO的多個連接可以并發(fā)地訪問單個數(shù)據(jù)庫。從用戶的角度看,如果所有連接都位于單個進(jìn)程中,則正常SQLite和使用異步IO的SQLite,其并發(fā)性并沒有什么不同。如果文件鎖是激活的(缺省是激活的),來自多個進(jìn)程的連接都要讀和寫數(shù)據(jù)庫文件,則并發(fā)性在下面的情況下會減弱:
?? ?(1)當(dāng)使用異步IO的連接啟動一個數(shù)據(jù)庫事務(wù)時,數(shù)據(jù)庫會立刻被鎖住。然而鎖只有在寫隊(duì)列中的所有操作已經(jīng)刷新到磁盤后才能釋放。這意味著有時即使在一個"COMMIT"或"ROLLBACK"執(zhí)行完后,數(shù)據(jù)庫可能仍然處于鎖住狀態(tài)。
?? ?(2)如果應(yīng)用程序使用異步IO連續(xù)地執(zhí)行多個事務(wù),其他數(shù)據(jù)庫用戶可能會因?yàn)閿?shù)據(jù)庫一直被鎖住而不能使用數(shù)據(jù)庫。這是因?yàn)楫?dāng)一個BEGIN執(zhí)行后,數(shù)據(jù)庫鎖會立刻建立起來。但當(dāng)對應(yīng)的COMMIT或ROLLBACK發(fā)生時,鎖不一定釋放了,要到后臺寫隊(duì)列全部刷新到磁盤后才能釋放。如果后臺寫隊(duì)列還沒刷新完,數(shù)據(jù)庫就一直處于鎖住狀態(tài),其他進(jìn)程不能訪問數(shù)據(jù)庫。
?? ?文件鎖可以在運(yùn)行時通過sqlite3async_control()函數(shù)禁用。對NFS這可以提高性能,因?yàn)榭梢员苊鈱Ψ?wù)器的來回異步操作建立文件鎖。但是如果多個連接嘗試訪問同一個數(shù)據(jù)庫,而文件鎖被禁用了,則應(yīng)用程序崩潰和數(shù)據(jù)庫損壞就可能發(fā)生。
?? ?異步IO擴(kuò)展模塊由單個源文件sqlite3async.c,和一個頭文件sqlite3async.h組成,位于源碼樹的ext/async/子目錄下。應(yīng)用程序可以用其中定義的C API來激活和控制這個模塊的功能。為了使用異步IO擴(kuò)展,把sqlite3async.c編譯成使用SQLite的應(yīng)用程序的一部分,然后使用sqlite3async.h中定義的API來初始化和配置這個模塊。這些API在sqlite3async.h的注釋中有詳細(xì)說明,使用這些API通常有以下步驟:
?? ?(1)調(diào)用sqlite3async_initialize()來給SQLite注冊異步IO VFS(虛擬文件系統(tǒng))。
?? ?(2)創(chuàng)建一個后臺線程來執(zhí)行寫操作,并調(diào)用sqlite3async_run()。
?? ?(3)通過異步IO VFS,使用正常的SQLite API來讀寫數(shù)據(jù)庫。
?? ?當(dāng)前的異步IO擴(kuò)展兼容win32系統(tǒng)和支持pthread接口的系統(tǒng),包括Mac OS X, Linux和其他Unix變體。為了移植異步IO擴(kuò)展到其他的平臺,用戶必須在新平臺上實(shí)現(xiàn)互斥鎖和條件變量原語。當(dāng)前并沒有外部可用接口來允許做這樣的控制,但是修改sqlite3async.c中的代碼以包含新平臺的并發(fā)控制原語是相當(dāng)容易的,更多細(xì)節(jié)可搜索sqlite3async.c中的注釋串"PORTING FUNCTIONS"。然后實(shí)現(xiàn)下面這些函數(shù)的新版本:
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);
?? ?上面這些函數(shù)的功能在sqlite3async.c的注釋中有詳細(xì)描述。
2、共享緩存模式
?? ?從3.3.0版開始,SQLite包含一個特別的“共享緩存”模式(缺省情況下禁用),主要用在嵌入式服務(wù)器中。如果共享緩存模式激活,并且一個線程在同一個數(shù)據(jù)庫上建立多個連接,則這些連接共享一個數(shù)據(jù)和模式緩存。這能夠顯著減少系統(tǒng)的內(nèi)存和IO消耗。在3.5.0版中,共享緩存模式被修改以便同一緩存的共享可以跨越整個進(jìn)程而不只是單個線程。在這個修改之前,在線程間傳遞數(shù)據(jù)連接是受限制的。從3.5.0版開始這個限制就消除了。
?? ?從另一個進(jìn)程或線程的角度看,使用共享緩存的兩個或多個數(shù)據(jù)庫連接看起來就像是一個連接。鎖協(xié)議用來在多個共享緩存或數(shù)據(jù)庫用戶之間進(jìn)行仲裁。
圖1 共享緩存模式
??? 圖1描述一個運(yùn)行時配置的例子,有三個數(shù)據(jù)庫連接。連接1是一個正常的SQLite數(shù)據(jù)庫連接,連接2和3共享一個緩存。正常的鎖協(xié)議用來在連接1和共享緩存之間串行化數(shù)據(jù)庫訪問。而連接2和連接3對共享緩存訪問的串行化則有專門的內(nèi)部協(xié)議。見下面的描述。
?? ?有三個級別的共享緩存加鎖模型,事務(wù)級別的加鎖,表級別的加鎖和模式級別的加鎖。
?? ?(1)事務(wù)級別的加鎖
?? ?SQLite連接可能打開兩種類型的事務(wù),讀事務(wù)和寫事務(wù)。這不是顯式完成的,一個事務(wù)隱式地含有一個讀事務(wù),直到它首次寫一個數(shù)據(jù)庫文件,這時成為一個寫事務(wù)。在任何時候共享緩存上最多只能有一個連接打開一個寫事務(wù),這個寫事務(wù)可以和任何數(shù)量的讀事務(wù)共存。這與非共享緩存模式不同,非共享緩存模式下有讀操作時不允許有寫操作。
?? ?(2)表級別的加鎖
?? ?當(dāng)兩個或更多的連接使用一個共享緩存,用鎖來串行化每個表格的并發(fā)訪問。表支持兩種類型的鎖,讀鎖和寫鎖。鎖被授予連接,任何時候每個數(shù)據(jù)庫連接上的每個表格可以有讀鎖、寫鎖或沒有鎖。一個表格上可以任何數(shù)量的讀鎖,但只能有一個寫鎖。讀數(shù)據(jù)庫表格時必須首先獲得一個讀鎖。寫表格時必須獲得一個寫鎖。如果不能獲取需要的鎖,查詢失敗并返回SQLITE_LOCKED給調(diào)用者。表級別的鎖在獲取之后,要到當(dāng)前事務(wù)(讀或?qū)懀┙Y(jié)束時才釋放。
?? ?如果使用read_uncommitted pragma指令把事務(wù)隔離模式從串行(serialized,缺省模式,即查詢數(shù)據(jù)時會加上共享瑣,阻塞其他事務(wù)修改真實(shí)數(shù)據(jù))改成允許臟讀(read-uncommitted,即SELECT會讀取其他事務(wù)修改而還沒有提交的數(shù)據(jù)),則上面描述的行為會有稍許的變化。事務(wù)隔離模式還有另外兩種,無法重復(fù)讀read-comitted是同一個事務(wù)中兩次執(zhí)行同樣的查詢語句,若在第一次與第二次查詢之間時間段,其他事務(wù)又剛好修改了其查詢的數(shù)據(jù)且提交了,則兩次讀到的數(shù)據(jù)不一致。可以重復(fù)讀read-repeatable是指同一個事務(wù)中兩次執(zhí)行同樣的查詢語句,得到的數(shù)據(jù)始終都是一致的。
/* Set the value of the read-uncommitted flag: ** ** True -> Set the connection to read-uncommitted mode. ** False -> Set the connection to serialized (the default) mode. */ PRAGMA read_uncommitted =; /* Retrieve the current value of the read-uncommitted flag */ PRAGMA read_uncommitted;
? ? 允許臟讀模式的數(shù)據(jù)庫連接在讀數(shù)據(jù)庫表時不會獲取讀鎖,如果這時另外一個數(shù)據(jù)庫連接修改了正在被讀的表數(shù)據(jù),則可能導(dǎo)致查詢結(jié)果不一致,因?yàn)樵试S臟讀模式的讀事務(wù)不會被打斷。允許臟讀模式不會影響寫事務(wù),它必須獲取寫鎖,因此數(shù)據(jù)庫寫操作可以被阻塞。允許臟讀模式也不會影響sqlite_master級別的鎖。
?? ?(3)模式(sqlite_master)級別的加鎖
?? ?sqlite_master表支持與其他數(shù)據(jù)庫表相同的共享緩存讀鎖和寫鎖。還會使用下面的特殊規(guī)則:
?? ?* 在訪問任何數(shù)據(jù)庫表格或者獲取任何其他的讀鎖和寫鎖之前,連接必須先獲取一個sqlite_master表上的讀鎖。
?? ?* 在執(zhí)行修改數(shù)據(jù)庫模式的語句(例如CREATE TABLE或DROP TABLE)之前,連接必須先獲取一個sqlite_master表上的寫鎖。
?? ?* 如果任何其他的連接持有關(guān)聯(lián)數(shù)據(jù)庫(包括缺省的主數(shù)據(jù)庫)的sqlite_master表上的寫鎖,則連接不可以編譯一個SQL語句。
?? ?在SQLite 3.3.0到3.4.2之間,數(shù)據(jù)庫連接只能被調(diào)用sqlite3_open()創(chuàng)建它的線程使用,一個連接只能與同一線程中的其他連接共享緩存。從SQLite 3.5.0開始,這個限制消除了。在老版本的SQLite上,共享緩存模式不能使用在虛擬表上,從SQLite 3.6.17開始,這個限制消除了。
?? ?共享緩存模式在每個進(jìn)程級別上激活。C接口int sqlite3_enable_shared_cache(int)用來全局地激活或禁用共享緩存模式。每次調(diào)用sqlite3_enable_shared_cache()影響后續(xù)的使用sqlite3_open(), sqlite3_open16()或sqlite3_open_v2()創(chuàng)建的數(shù)據(jù)庫連接,已經(jīng)存在的數(shù)據(jù)庫連接則不受影響。每次sqlite3_enable_shared_cache()的調(diào)用覆蓋進(jìn)程上的前面各次調(diào)用。
?? ?使用sqlite3_open_v2()創(chuàng)建的單個數(shù)據(jù)庫連接,通過在第三個參數(shù)上使用SQLITE_OPEN_SHAREDCACHE或SQLITE_OPEN_PRIVATECACHE標(biāo)志,可能選擇參與或不參與共享緩存模式。在該數(shù)據(jù)庫連接上這些標(biāo)志會覆蓋全局的sqlite3_enable_shared_cache()設(shè)置。如果同時使用這兩個標(biāo)志,則行為是未定義的。
?? ?當(dāng)使用URI文件名時,"cache"查詢參數(shù)可以用來指定連接是否使用共享緩存模式。"cache=shared"激活共享緩存,"cache=private"禁用共享緩存。例如:
?? ?ATTACH 'file:aux.db?cache=shared' AS aux;
?? ?從SQLite 3.7.13開始,倘若數(shù)據(jù)庫使用URI文件名創(chuàng)建,共享緩存模式可以在內(nèi)存數(shù)據(jù)庫上使用。為了向后兼容,使用未修飾的":memory:"名稱打開內(nèi)存數(shù)據(jù)庫時缺省是禁用共享緩存的。而在SQLite 3.7.13之前,無論使用的內(nèi)存數(shù)據(jù)庫名、當(dāng)前系統(tǒng)的共享緩存設(shè)置、以及查詢參數(shù)或標(biāo)志是什么,內(nèi)存數(shù)據(jù)庫上共享緩存總是被禁用的。
?? ?在內(nèi)存數(shù)據(jù)庫上激活共享緩存,會允許同一進(jìn)程上的兩個或更多數(shù)據(jù)庫連接訪問同一段內(nèi)存。當(dāng)最后一個連接關(guān)閉時,內(nèi)存數(shù)據(jù)庫會自動刪除,這段內(nèi)存也會被重置。
?? ?
3、解鎖通知
?? ?當(dāng)多個連接在共享緩存模式下訪問同一個數(shù)據(jù)庫時,單個表上的讀鎖和寫鎖(即共享鎖和排他鎖)用來確保并發(fā)執(zhí)行的事務(wù)是隔離的。如果連接不能獲取到需要的鎖,sqlite3_step()調(diào)用返回SQLITE_LOCKED。如果不能獲取到每個關(guān)聯(lián)數(shù)據(jù)庫的sqlite_master表上的讀鎖(雖然這種情況并不常見),sqlite3_prepare()或sqlite3_prepare_v2()調(diào)用也會返回SQLITE_LOCKED。
?? ?通過使用SQLite的sqlite3_unlock_notify()接口,我們可以讓sqlite3_step()或sqlite3_prepare_v2()調(diào)用阻塞直到獲得需要的鎖,而不是立刻返回SQLITE_LOCKED。下面的例子展示解鎖通知的使用。
/* 本例子使用pthreads API */ #include/* ** 當(dāng)注冊一個解鎖通知時,傳遞本結(jié)構(gòu)實(shí)例的指針,以作為用戶上下文中的實(shí)例 */ typedef struct UnlockNotification UnlockNotification; struct UnlockNotification { int fired; /* 在解鎖事件發(fā)生后為True */ pthread_cond_t cond; /* 要等待的條件變量 */ pthread_mutex_t mutex; /* 保護(hù)本結(jié)構(gòu)的互斥量 */ }; /* ** 解鎖通知回調(diào)函數(shù) */ static void unlock_notify_cb(void **apArg, int nArg){ int i; for(i=0; i mutex); /* 對臨界區(qū)加鎖 */ p->fired = 1; /* 觸發(fā)解鎖事件,本變量只能互斥訪問 */ pthread_cond_signal(&p->cond); pthread_mutex_unlock(&p->mutex); } } /* ** 本函數(shù)假設(shè)SQLite API調(diào)用(sqlite3_prepare_v2()或sqlite3_step())返回SQLITE_LOCKED。 ** 參數(shù)為關(guān)聯(lián)的數(shù)據(jù)庫連接。 ** 本函數(shù)調(diào)用sqlite3_unlock_notify()注冊一個解鎖通知回調(diào)函數(shù),然后阻塞直到 ** 回調(diào)函數(shù)執(zhí)行完并返回SQLITE_OK。調(diào)用者應(yīng)該重試失敗的操作。 ** 或者,如果sqlite3_unlock_notify()指示阻塞將會導(dǎo)致系統(tǒng)死鎖,則本函數(shù)立刻 ** 返回SQLITE_LOCKED。調(diào)用者不應(yīng)該重試失敗的操作,而是回滾當(dāng)前事務(wù) */ static int wait_for_unlock_notify(sqlite3 *db){ int rc; UnlockNotification un; /* 初始化UnlockNotification結(jié)構(gòu) */ un.fired = 0; pthread_mutex_init(&un.mutex, 0); pthread_cond_init(&un.cond, 0); /* 注冊一個解鎖通知回調(diào)函數(shù) */ rc = sqlite3_unlock_notify(db, unlock_notify_cb, (void *)&un); assert( rc==SQLITE_LOCKED || rc==SQLITE_OK ); /* sqlite3_unlock_notify()調(diào)用總是返回SQLITE_LOCKED或SQLITE_OK。 ** 如果返回SQLITE_LOCKED,則系統(tǒng)死鎖。本函數(shù)需要返回SQLITE_LOCKED給調(diào)用者以 ** 便當(dāng)前事務(wù)能夠回滾。否則阻塞直到解鎖通知回調(diào)函數(shù)執(zhí)行,然后返回SQLITE_OK */ if( rc==SQLITE_OK ){ pthread_mutex_lock(&un.mutex); if( !un.fired ){ /* 如果解鎖事件沒有發(fā)生,則阻塞 */ pthread_cond_wait(&un.cond, &un.mutex); } pthread_mutex_unlock(&un.mutex); } /* 銷毀互斥量和條件變量 */ pthread_cond_destroy(&un.cond); pthread_mutex_destroy(&un.mutex); return rc; } /* ** 本函數(shù)是SQLite函數(shù)sqlite3_step()的包裝,它的工作方式與sqlite3_step()相同。 ** 但如果沒有獲得共享緩存鎖,則本函數(shù)阻塞以等待鎖可用。 ** 如果本函數(shù)返回SQLITE_LOCKED,調(diào)用者應(yīng)該回滾當(dāng)前事務(wù),之后再嘗試。否則系統(tǒng)可能死鎖了 */ 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; } /* ** 本函數(shù)是SQLite函數(shù)sqlite3_prepare_v2()的包裝,它的工作方式與sqlite3_prepare_v2()相同。 ** 但如果沒有獲得共享緩存鎖,則本函數(shù)阻塞以等待鎖可用。 ** 如果本函數(shù)返回SQLITE_LOCKED,調(diào)用者應(yīng)該回滾當(dāng)前事務(wù),之后再嘗試。否則系統(tǒng)可能死鎖了 */ int sqlite3_blocking_prepare_v2( sqlite3 *db, /* 數(shù)據(jù)庫句柄 */ const char *zSql, /* UTF-8編碼的SQL語句 */ int nSql, /* zSql的字節(jié)數(shù) */ sqlite3_stmt **ppStmt, /* OUT: 指向預(yù)處理語句的指針 */ 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 ) break; } return rc; }
? ? 如果例子中的sqlite3_blocking_step()或sqlite3_blocking_prepare_v2()函數(shù)返回SQLITE_LOCKED,則表明阻塞將導(dǎo)致系統(tǒng)死鎖。
?? ?只有在編譯時定義預(yù)處理宏SQLITE_ENABLE_UNLOCK_NOTIFY,才能使用sqlite3_unlock_notify()接口。該接口被設(shè)計(jì)成用在這樣的系統(tǒng)中:每個數(shù)據(jù)庫連接分配單獨(dú)的線程。如果在一個線程中運(yùn)行多個數(shù)據(jù)庫連接,則不能使用該接口。sqlite3_unlock_notify()接口一次只在一個線程上工作,因此上面的鎖控制邏輯只能工作于一個線程的單個數(shù)據(jù)庫連接上。
?? ?上面的例子中,在sqlite3_step()或sqlite3_prepare_v2()返回SQLITE_LOCKED后,sqlite3_unlock_notify()被調(diào)用以注冊一個解鎖通知回調(diào)函數(shù)。在數(shù)據(jù)庫連接持有表級別的鎖后,解鎖通知函數(shù)被執(zhí)行以防止sqlite3_step()或sqlite3_prepare_v2()隨后完成事務(wù)并釋放所有鎖。例如,如果sqlite3_step()嘗試讀表格X,而其他某個連接Y正持有表格X的寫鎖,sqlite3_step()將返回SQLITE_LOCKED。如果隨后調(diào)用sqlite3_unlock_notify(),解鎖通知函數(shù)將在連接Y的事務(wù)結(jié)束后被調(diào)用。解鎖通知函數(shù)正在等待的連接(這里的Y),被稱為“阻塞式連接”。
?? ?如果sqlite3_step()嘗試寫一個數(shù)據(jù)庫,但返回SQLITE_LOCKED,則可能有多個進(jìn)程持有當(dāng)前數(shù)據(jù)庫表格的讀鎖。這時SQLite隨意地選擇其中的一個連接,當(dāng)這個連接的事務(wù)完成時執(zhí)行解鎖通知函數(shù)。解鎖通知函數(shù)從sqlite3_step()(或sqlite3_close())里執(zhí)行,它關(guān)聯(lián)有一個阻塞式進(jìn)程。解鎖通知函數(shù)里面可以調(diào)用任何的sqlite3_XXX()函數(shù),可以向其他等待線程發(fā)信號,或者安排一些在以后要發(fā)生的行為。
?? ?sqlite3_blocking_step()函數(shù)使用的算法描述如下:
?? ?(1)在指定的SQL語句對象上調(diào)用sqlite3_step(),如果返回除SQLITE_LOCKED之外的值,則直接返回這個值給調(diào)用者。如果返回SQLITE_LOCKED則繼續(xù)。
?? ?(2)調(diào)用sqlite3_unlock_notify()注冊一個解鎖通知回調(diào)函數(shù)。如果sqlite3_unlock_notify()返回SQLITE_LOCKED,說明系統(tǒng)死鎖,返回這個值給調(diào)用者以便回滾。否則繼續(xù)。
?? ?(3)阻塞,直到解鎖通知函數(shù)被另外一個線程執(zhí)行。
?? ?(4)在SQL語句對象上調(diào)用sqlite3_reset()。因?yàn)镾QLITE_LOCKED錯誤可能只發(fā)生在第一次調(diào)用sqlite3_step()時(不可能有sqlite3_step()先返回SQLITE_ROW而下一次卻返回SQLITE_LOCKED的情況)。這時SQL語句對象會被重置,從而不會影響查詢結(jié)果。如果不調(diào)用sqlite3_reset(),下一次調(diào)用sqlite3_step()將返回SQLITE_MISUSE。
?? ?(5)轉(zhuǎn)向步驟(1)。
?? ?sqlite3_blocking_prepare_v2()使用的算法也類似,只不過第4步(重置SQL語句對象)忽略。
?? ?對于“寫?zhàn)囸I”現(xiàn)象,SQLite能幫助應(yīng)用程序避免出現(xiàn)寫?zhàn)囸I的情況。當(dāng)在一個表上獲取寫鎖的任何嘗試失敗后(因?yàn)橛羞B接一直持有讀鎖),共享緩存上啟動新事務(wù)的所有嘗試都會失敗,直到下面有一種情況變成true為止:
?? ?* 當(dāng)前寫事務(wù)完成,或者
?? ?* 共享緩存上打開的讀事務(wù)數(shù)量減為0。
?? ?啟動新的讀事務(wù)失敗會返回SQLITE_LOCKED給調(diào)用者。如果調(diào)用者然后調(diào)用sqlite3_unlock_notify()注冊一個解鎖通知函數(shù),阻塞式連接當(dāng)前在共享緩存上會有一個寫事務(wù)。這就避免了寫?zhàn)囸I,因?yàn)闆]有新的讀鎖可以打開了。當(dāng)所有存在的讀鎖完成時,寫操作最終能有機(jī)會獲得需要的寫鎖。
?? ?在wait_for_unlock_notify()調(diào)用sqlite3_unlock_notify()時,有可能阻塞式線程已經(jīng)完成它的事務(wù),這樣在sqlite3_unlock_notify()返回前解鎖通知函數(shù)會立刻被調(diào)用。解鎖通知函數(shù)也有可能被另一個線程調(diào)用,正好發(fā)生在sqlite3_unlock_notify()調(diào)用之后,而在這個線程開始等待異步信號之前。這樣的競爭條件怎么處理,取決于應(yīng)用程序使用的線程和同步原語。本例子中使用pthread,這是現(xiàn)代Unix風(fēng)格的系統(tǒng)(包括Linux)提供的接口。
?? ?pthread提供pthread_cond_wait()函數(shù),它允許調(diào)用者同時釋放一個互斥量并開始等待一個異步信號。使用這個函數(shù)、一個"fired"標(biāo)志和一個互斥量,競爭狀態(tài)可以消除,如下:
?? ?當(dāng)解鎖通知函數(shù)被調(diào)用時,這可能發(fā)生在調(diào)用sqlite3_unlock_notify()的線程開始等待一個異步信號之前,它做下面的工作:
?? ?(1)獲取互斥量。
?? ?(2)設(shè)置"fired"標(biāo)志為true。
?? ?(3)向等待線程發(fā)信號。
?? ?(4)釋放互斥量。
?? ?當(dāng)wait_for_unlock_notify()線程開始等待解鎖通知函數(shù)到達(dá)時,它:
?? ?(1)獲取互斥量。
?? ?(2)檢查"fired"標(biāo)志是否設(shè)置。如果已設(shè)置,解鎖通知函數(shù)已經(jīng)被調(diào)用,直接釋放互斥量,然后繼續(xù)。
?? ?(3)如果沒設(shè)置,原子性地釋放互斥量,并開始等待異步信號。當(dāng)信號到達(dá)時,繼續(xù)。
?? ?通過這種方式,當(dāng)wait_for_unlock_notify()開始阻塞時,解鎖通知函數(shù)不管是已經(jīng)被調(diào)用,還是正在被調(diào)用,都沒有問題。
?? ?本文例子中的代碼至少在以下兩個方面可以改進(jìn):
?? ?* 能管理線程優(yōu)先級。
?? ?* 能處理SQLITE_LOCKED的特殊情形,這可能發(fā)生在刪除一個表或索引時。
?? ?雖然sqlite3_unlock_notify()只允許調(diào)用者指定單個的用戶上下文指針,但一個解鎖通知回調(diào)是傳給這種上下文指針數(shù)組的。這是因?yàn)楫?dāng)一個阻塞式線程完成它的事務(wù)時,如果有多個解鎖通知被注冊用于調(diào)用同一個C函數(shù),則上下文指針就要排列成一個數(shù)組。如果每個線程分配一個優(yōu)先級,則高優(yōu)先級的線程就會比低優(yōu)先級的線程先得到信號通知,而不是以任意的順序來通知線程。
?? ?如果執(zhí)行一個"DROP TABLE"或"DROP INDEX"命令,而當(dāng)前數(shù)據(jù)庫連接上有一個或多個正在執(zhí)行的SELECT語句,則會返回SQLITE_LOCKED。如果調(diào)用了sqlite3_unlock_notify(),指定的回調(diào)函數(shù)立刻會被調(diào)用。重新嘗試"DROP TABLE"或"DROP INDEX"將返回另外一個SQLITE_LOCKED錯誤。在上面的sqlite3_blocking_step()實(shí)現(xiàn)中,這會導(dǎo)致死循環(huán)。
?? ?調(diào)用者可以使用擴(kuò)展錯誤碼來區(qū)別這種特殊的"DROP TABLE|INDEX"情形和其他情形。當(dāng)它正常調(diào)用sqlite3_unlock_notify()時,擴(kuò)展錯誤碼是SQLITE_LOCKED_SHAREDCACHE。在"DROP TABLE|INDEX"情形中,是普通的SQLITE_LOCKED。另外一種解決方法是限制重試單個查詢的次數(shù)(如100次)。雖然這會導(dǎo)致效率低一點(diǎn),但我們這里討論的情況并不是經(jīng)常發(fā)生的。