FMDB 多線程訪問同一數(shù)據(jù)庫連接祷蝌、預(yù)處理語句崩潰

本文討論的 FMDB 版本為2.7.5,測試環(huán)境是?Xcode 10.1 & iOS 12.1

一沐批、問題記錄

最近在分析崩潰日志的時候發(fā)現(xiàn)一個 FMDB 的 crash 頻繁出現(xiàn)从铲,crash 堆棧如下:


在控制臺能看到報錯:

[logging] BUG IN CLIENT OF sqlite3.dylib: illegal multi-threaded access to database connection

Warning: there is at least one open result set around after performing [FMDatabaseQueue inDatabase:]

從日志中能大概猜到瘪校,這是多線程訪問數(shù)據(jù)庫導(dǎo)致的 crash。FMDB 提供了 FMDatabaseQueue 在多線程環(huán)境下操作數(shù)據(jù)庫名段,它內(nèi)部維護了一個串行隊列來保證線程安全阱扬。我檢查了所有操作數(shù)據(jù)庫的代碼,都是在 FMDatabaseQueue 隊列里執(zhí)行的伸辟,為啥還是會報多線程問題(一臉懵逼??)麻惶?

在網(wǎng)上找了一圈,發(fā)現(xiàn) github 上有人遇到了同樣的問題自娩, Issue 724 和 Issue 711用踩,Stack Overflow上有相關(guān)的討論。

項目里業(yè)務(wù)太復(fù)雜忙迁,很難排查問題,于是寫了一個簡化版的 Demo 來復(fù)現(xiàn)問題:

問題完美復(fù)現(xiàn)碎乃,接下來就可以排查問題了姊扔,有兩個問題亟待解決:

iOS 系統(tǒng)自帶的 SQLite 究竟是不是線程安全的?

為什么使用了線程安全隊列 FMDatabaseQueue梅誓, 還是出現(xiàn)了線程安全問題恰梢?

二、SQLite 線程安全

我們先來看第一個問題梗掰,iOS 系統(tǒng)自帶的 SQLite 究竟是不是線程安全的嵌言?

Google 了一下,發(fā)現(xiàn)了關(guān)于SQLite的官方文檔 - Using SQLite In Multi-Threaded Applications及穗。文檔寫的很清晰摧茴,有時間最好認真讀讀,這里簡單總結(jié)一下埂陆。

SQLite 有3種線程模式:

Single-thread:單線程模式苛白,編譯時所有互斥鎖代碼會被刪除掉娃豹,多線程環(huán)境下不安全。

Multi-thread:在大部分情況下多線程環(huán)境安全购裙,比如同一個數(shù)據(jù)庫懂版,開多個線程,每個線程都開一個連接同時訪問這個庫躏率,這種情況是安全的躯畴。但是也有不安全情況:多個線程同時使用同一個數(shù)據(jù)庫連接(或從該連接派生的任何預(yù)準備語句)

Serialized:完全線程安全。

有3個時間點可以配置 threading mode薇芝,編譯時(compile-time)蓬抄、初始化時(start-time)、運行時(run-time)恩掷。配置生效規(guī)則是 run-time 覆蓋 start-time 覆蓋 compile-time倡鲸,有一些特殊情況:

編譯時設(shè)置 Single-thread,用戶就不能再開啟多線程模式黄娘,因為線程安全代碼被優(yōu)化了峭状。

如果編譯時設(shè)置的多線程模式,在運行時不能降級為單線程模式逼争,只能在Multi-thread和Serialized間切換优床。

threading mode 編譯選項

SQLite threading mode 編譯選項的官方文檔

編譯時,通過配置項SQLITE_THREADSAFE可以配置 SQLite 在多線程環(huán)境下是否安全誓焦。有三個可選項:

????????0胆敞,對應(yīng) Single-thread ,編譯時所有互斥鎖代碼會被刪除掉杂伟,SQLite 在多線程環(huán)境下不安全移层。

????????1,對應(yīng) Serialized赫粥,在多線程環(huán)境下安全观话,如果不手動指定,這是默認選項越平。

????????2频蛔,對應(yīng) Multi-thread ,在大部分情況下多線程環(huán)境安全秦叛,不安全情況:有兩個線程同時嘗試使用相同數(shù)據(jù)庫連接(或從該數(shù)據(jù)庫連接派生的任何預(yù)處理語句 Prepared Statements)

除了編譯時可以指定 threading mode 晦溪,還可以通過函數(shù) sqlite3_config() (start-time )改變?nèi)值?threading mode 或者通過sqlite3_open_v2() (run-time)改變某個數(shù)據(jù)庫連接的 threading mode。

但是如果編譯時配置了SQLITE_THREADSAFE = 0挣跋,編譯時所有線程安全代碼都被優(yōu)化掉了三圆,就不能再切換到多線程模式了。

有了前面的知識,我們就可以分析問題一了嫌术。調(diào)用函數(shù) sqlite3_threadsafe() 可以獲取編譯時的配置項哀澈,我們可以用這個函數(shù)獲取系統(tǒng)自帶的 SQLite 在編譯時的配置,結(jié)論是2(Multi-thread)度气。

也就是說割按,系統(tǒng)自帶的 SQLite 在不做任何配置的情況下不是完全線程安全的。當(dāng)然可以手動將模式切換到 Serialized 就可以實現(xiàn)完全線程安全了磷籍。

// 方案一:全局設(shè)置模式

sqlite3_config(SQLITE_CONFIG_SERIALIZED);

// 方案二:設(shè)置 connecting 模式适荣,調(diào)用 sqlite3_open_v2 時 flag 加上 SQLITE_OPEN_FULLMUTEX

sqlite3_open_v2(path, &db, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX, nil)

經(jīng)過測試,通過上面兩種方案改造之后院领,Demo 中的 crash 問題完美解決弛矛。但是我認為這不是最優(yōu)的解決方案,蘋果為啥不直接將編譯選項設(shè)置為 Serialized比然,這篇文章就永遠不會出現(xiàn)了??丈氓,勞民傷財讓大家折騰半天,去手動設(shè)置模式强法。我認為性能是一個重要因素万俗,Multi-thread 性能優(yōu)于 Serialized, 用戶只要保證一個連接不在多線程同時訪問就沒問題了,其實能滿足大部分需求饮怯。

比如 FMDB 的 FMDatabaseQueue 就是為了解決該問題闰歪。

三、FMDatabaseQueue 其實并不安全

FMDB 的官方文檔寫到:

FMDatabaseQueue will run the blocks on a serialized queue (hence the name of the class). So if you call FMDatabaseQueue's methods from multiple threads at the same time, they will be executed in the order they are received. This way queries and updates won't step on each other's toes, and every one is happy.

在多線程使用 FMDatabaseQueue 的確很安全蓖墅,通過 GCD 的串行隊列來保證所有讀寫操作都是串行執(zhí)行的库倘。它的核心代碼如下:

_queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);

- (void)inDatabase:(__attribute__((noescape)) void (^)(FMDatabase *db))block {

? ? // ...省略部分代碼

? ? dispatch_sync(_queue, ^() {

? ? ? ? FMDatabase *db = [self database];

? ? ? ? block(db);

? ? });

? ? // ...省略部分代碼

}

但是分析第一節(jié) Demo 的 crash 堆棧,可以看到崩潰發(fā)生在線程3的函數(shù) [FMResultSet reset]论矾,函數(shù)定義如下:

- (void)reset {

? ? if (_statement) {

? ? ? ? // 釋放預(yù)處理語句(Reset A Prepared Statement Object)

? ? ? ? sqlite3_reset(_statement);

? ? }

? ? _inUse = NO;

}

這個函數(shù)的調(diào)用棧如下:

- [FMStatement reset]

- [FMResultSet close]

- [FMResultSet dealloc]

順著調(diào)用堆棧教翩,我們來看看 FMResultSet 的 dealloc 和 close 方法:

- (void)dealloc {

? ? [self close];

? ? FMDBRelease(_query);

? ? _query = nil;

? ? FMDBRelease(_columnNameToIndexMap);

? ? _columnNameToIndexMap = nil;

}

- (void)close {

? ? [_statement reset];

? ? FMDBRelease(_statement);

? ? _statement = nil;

? ? [_parentDB resultSetDidClose:self];

? ? [self setParentDB:nil];

}

這里可以得出結(jié)論,在 FMResultSet dealloc 時會調(diào)用 close 方法贪壳,來關(guān)閉預(yù)處理語句迂曲。再回到第一節(jié)的 crash 堆棧,不難發(fā)現(xiàn)線程7在用同一個數(shù)據(jù)庫連接讀數(shù)據(jù)庫寥袭,結(jié)合官方文檔中的一段話,我們就可以得出結(jié)論了关霸。

When compiled with SQLITE_THREADSAFE=2, SQLite can be used in a multithreaded program so long as no two threads attempt to use the same database connection (or any prepared statements derived from that database connection) at the same time.

使用 FMDatabaseQueue 還是發(fā)生了多線程使用同一個數(shù)據(jù)庫連接传黄、預(yù)處理語句的情況,于是就崩潰了队寇。

解決方案

問題找到了膘掰,接下來聊聊怎么避免問題。

FMDB的正確打開方式

如果用 while 循環(huán)遍歷 FMResultSet 就不存在該問題,因為 [FMResultSet next] 遍歷到最后會調(diào)用 [FMResultSet close]识埋。

[_queue inDatabase:^(FMDatabase * _Nonnull db) {

? ? FMResultSet *result = [db executeQuery:@"select * from test where a = '1'"];

? ? // 安全

? ? while ([result next]) {

? ? }

? ? // 安全

? ? if ([result next]) {

? ? }

? ? [result close];

}];

如果一定要用 if ([result next]) 凡伊,手動加上 [FMResultSet close] 也沒有問題。

寫在最后

記得看過一位大牛說遇到這個問題窒舟,是被官方文檔的一句話誤導(dǎo)了系忙。

Typically, there's no need to -close an FMResultSet yourself, since that happens when either the result set is deallocated, or the parent database is closed.

且提了一個?Pull requests ,和兩種解決方案:

修改文檔惠豺,在文檔中強調(diào)银还,用戶需要手動調(diào)用 close。

在 [FMDatabaseQueue inDatabase:] 函數(shù)的最后洁墙,調(diào)用 [FMDatabase closeOpenResultSets] 幫助調(diào)用者關(guān)閉所有 FMResultSet蛹疯。

FMDB 的作者 ccgus 采用了第一種方案,在最新的一次?commit?修改了文檔热监,加上了相關(guān)說明捺弦。

Typically, there's no need to -close an FMResultSet yourself, since that happens when either the result set is exhausted. However, if you only pull out a single request or any other number of requests which don't exhaust the result set, you will need to call the -close method on the FMResultSet.

---------------------

參考

1.Using SQLite In Multi-Threaded Applications

2.sqlite3.dylib: illegal multi-threaded access to database connection

3.FMDB

4.SQLite編譯選項官方文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市孝扛,隨后出現(xiàn)的幾起案子列吼,更是在濱河造成了極大的恐慌,老刑警劉巖疗琉,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冈欢,死亡現(xiàn)場離奇詭異,居然都是意外死亡盈简,警方通過查閱死者的電腦和手機凑耻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來柠贤,“玉大人香浩,你說我怎么就攤上這事【拭悖” “怎么了邻吭?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長宴霸。 經(jīng)常有香客問我囱晴,道長,這世上最難降的妖魔是什么瓢谢? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任畸写,我火速辦了婚禮,結(jié)果婚禮上氓扛,老公的妹妹穿的比我還像新娘枯芬。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布千所。 她就那樣靜靜地躺著狂魔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪淫痰。 梳的紋絲不亂的頭發(fā)上最楷,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天,我揣著相機與錄音黑界,去河邊找鬼管嬉。 笑死,一個胖子當(dāng)著我的面吹牛朗鸠,可吹牛的內(nèi)容都是我干的蚯撩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼烛占,長吁一口氣:“原來是場噩夢啊……” “哼胎挎!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起忆家,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤犹菇,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后芽卿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體揭芍,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年卸例,在試婚紗的時候發(fā)現(xiàn)自己被綠了称杨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡筷转,死狀恐怖姑原,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情呜舒,我是刑警寧澤锭汛,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站袭蝗,受9級特大地震影響唤殴,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜到腥,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一眨八、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧左电,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至栈拖,卻和暖如春连舍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背涩哟。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工索赏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贴彼。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓潜腻,卻偏偏與公主長得像,于是被迫代替她去往敵國和親器仗。 傳聞我的和親對象是個殘疾皇子融涣,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,486評論 2 348