iOS數據庫的使用(三):sqlite多線程
引言
首先,sqlite支持多線程,但是是有條件的支持,也就是同一個連接不能在多線程中使用,不同連接才可以在多線程中使用。這個是最宏觀的sqlite多線程準則。
另外,sqlite的文件鎖是粗顆粒的,也就是以數據庫文件為維度加鎖,涉及到5種鎖狀態。5中鎖狀態可以使用一句話來總結:sqlite在普通情況(非普通情況就是shared-cache+wal模式)下支持并發讀取操作,但是不支持并發寫入操作,且不支持并發寫入讀取混合操作。說白了就是只能并發讀取sqlite。
再者有了sqlite3.3以后的shared-cache模式+WAL模式,這兩種模式使sqlite支持并發寫入讀取混合操作,也就是寫入和讀取都可以并發且不影響了。
本文不涉及SHARED-CACHE + WAL模式,只講sqlite中的多線程及其意義和使用;
sqlite3中的三種線程模式
Single-thread(單線程):值為0,所有的互斥所都被禁止。這種模式在極度要求速度的情況下被建議使用。因為沒有加鎖,所以在多線程中使用時是不安全的。該模式下性能最好,在性能優先的模式下選擇。
Multi-thread(多線程):值為2,在部分地方加鎖,部分地方禁止了互斥所。可以在多線程是使用多個連接,但是一個連接同時被多個線程使用時,是不安全的。
Serialized(串行):值為1,所有的互斥所都被開啟。這種模式無論是多個連接在多線程中使用,還是單個連接在多線程中使用,最終都被被強制成串行執行,所以是絕對線程安全的,但是性能最差,在安全性要求高的情況下選擇。
官方文檔:SQLITE_THREADSAFE=<0 or 1 or 2>
sqlite線程模式的設置
編譯階段:通過使用編譯指令配置相關的參數。例如iOS中的sqlite3.lib就是被編譯之后的庫,這個lib中sqlite3的線程模式被配置成了2,也就是串行模式。具體的指令如下:
gcc -DSQLITE_THREADSAFE=0 shell.c sqlite3.c -ldl
其意義是:編譯時設置SQLITE_THREADSAFE參數的值為0,編譯shell.c和sqlite3.c,生成命令行執行程序。sqlite3編譯設置
初始化階段:在調用sqlite3_initialize()之前使用sqlite3_config()函數設置。因為sqlite3_initialize()一般都被封裝在了open方法中,所以這個階段可以認為是在調用open方法之前使用sqlite3_config()來設置線程模式。
運行時:通過sqlite3_open_v2()中的第三個參數來設置,可選值為SQLITE_OPEN_NOMUTEX(無鎖即多線程模式),SQLITE_OPEN_FULLMUTEX(全鎖即串行模式)
sqlite3_threadsafe()方法
該方法包含以下幾個重點:
- 該方法返回編譯時期所設置的線程模式的值
- 該方法返回值不受其他階段重置線程模式的影響,也就是說即使在初始化或者runtime階段改變了線程模式,該函數的返回值不變。
sqlite3_config方法
#define SQLITE_CONFIG_SINGLETHREAD 1 /* nil */
#define SQLITE_CONFIG_MULTITHREAD 2 /* nil */
#define SQLITE_CONFIG_SERIALIZED 3 /* nil */
#define SQLITE_CONFIG_MALLOC 4 /* sqlite3_mem_methods* */
#define SQLITE_CONFIG_GETMALLOC 5 /* sqlite3_mem_methods* */
#define SQLITE_CONFIG_SCRATCH 6 /* No longer used */
#define SQLITE_CONFIG_PAGECACHE 7 /* void*, int sz, int N */
#define SQLITE_CONFIG_HEAP 8 /* void*, int nByte, int min */
#define SQLITE_CONFIG_MEMSTATUS 9 /* boolean */
#define SQLITE_CONFIG_MUTEX 10 /* sqlite3_mutex_methods* */
#define SQLITE_CONFIG_GETMUTEX 11 /* sqlite3_mutex_methods* */
/* previously SQLITE_CONFIG_CHUNKALLOC 12 which is now unused. */
#define SQLITE_CONFIG_LOOKASIDE 13 /* int int */
#define SQLITE_CONFIG_PCACHE 14 /* no-op */
#define SQLITE_CONFIG_GETPCACHE 15 /* no-op */
#define SQLITE_CONFIG_LOG 16 /* xFunc, void* */
#define SQLITE_CONFIG_URI 17 /* int */
#define SQLITE_CONFIG_PCACHE2 18 /* sqlite3_pcache_methods2* */
#define SQLITE_CONFIG_GETPCACHE2 19 /* sqlite3_pcache_methods2* */
#define SQLITE_CONFIG_COVERING_INDEX_SCAN 20 /* int */
#define SQLITE_CONFIG_SQLLOG 21 /* xSqllog, void* */
#define SQLITE_CONFIG_MMAP_SIZE 22 /* sqlite3_int64, sqlite3_int64 */
#define SQLITE_CONFIG_WIN32_HEAPSIZE 23 /* int nByte */
#define SQLITE_CONFIG_PCACHE_HDRSZ 24 /* int *psz */
#define SQLITE_CONFIG_PMASZ 25 /* unsigned int szPma */
#define SQLITE_CONFIG_STMTJRNL_SPILL 26 /* int nByte */
#define SQLITE_CONFIG_SMALL_MALLOC 27 /* boolean */
#define SQLITE_CONFIG_SORTERREF_SIZE 28 /* int nByte */
#define SQLITE_CONFIG_MEMDB_MAXSIZE 29 /* sqlite3_int64 */
各個值得意義:官方文檔
幾個重點
- sqlite3_config(int,...)可以設置多個值
- sqlite3_config不是線程安全的,當該方法在執行時,確保其他線程沒有調用該方法
- 這里的線程模式的值是1、2、3,而編譯階段設置的線程模式的值為0、1、2
Muti-thread模式下的并發
根據官方文檔,只要保證了多線程中不同時使用同一個connect即可,所以path使用同一個,也就意味著使用同一個數據庫,但是并發中取創建新的db,也就是open的是不同的db,也就是和同一個數據庫建立了多個不同的連接,代碼如下
主要并發邏輯:
- (void)mutiThreadTest {
// iOS中的sqlite3lib默認是2,也就是muti-thread。也就是說應用程序需要自己去保證不再多線程中同時使用同一個數據庫連接(database-connection)。也就是說,可以通過建立多個數據庫連接來實現并行訪問sqlite
dispatch_queue_t t = dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0);
dispatch_async(t, ^{
FMDatabase *db1 = [FMDatabase databaseWithPath:self.path];
[self addDataFrom:0 count:1000 withDB:db1 withFlag:@"1"];
});
dispatch_async(t, ^{
FMDatabase *db2 = [FMDatabase databaseWithPath:self.path];
[self addDataFrom:1000 count:1000 withDB:db2 withFlag:@"2"];
});
dispatch_async(t, ^{
FMDatabase *db3 = [FMDatabase databaseWithPath:self.path];
[self addDataFrom:2000 count:1000 withDB:db3 withFlag:@"3"];
});
dispatch_async(t, ^{
FMDatabase *db4 = [FMDatabase databaseWithPath:self.path];
[self addDataFrom:3000 count:1000 withDB:db4 withFlag:@"4"];
});
}
viewDidLoad方法:
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.tableView];
[self create];
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
[btn setTitle:@"查詢" forState:UIControlStateNormal];
btn.backgroundColor = [UIColor redColor];
[btn addTarget:self action:@selector(mutiThreadTest) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
btn.frame = CGRectMake(100,100, 40, 40);
}
創建數據庫方法:
- (void)create {
self.path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"test.db"];
self.db = [FMDatabase databaseWithPath:self.path];
self.queue = [FMDatabaseQueue databaseQueueWithPath:self.path];
if ([self.db open]) {
[self.db executeUpdate:@"CREATE table if not exists ClientTable (name text, no text, signature text,PRIMARY KEY(no));"];
[self.db executeUpdate:@"delete from ClientTable"];
}
NSLog(@"%@",self.path);
}
結果:
step方法報錯,錯誤碼為5,即SQLITE_BUSY;
所以,sqlite中的線程安全意味著什么呢?
sqlite的多線程模式中的條件的具體意義
sqlite的線程安全意味著數據的安全,如果錯誤的使用將會導致異常,比如崩潰、數據錯亂。
如官方文檔中鎖描述的,其本質是不要在多線程中同時使用同一個connect或者statement,這里的具體意思:
connect的代表是sqlite3對象,所以這里的維度不是以open-close為維度,而是以sqlite對象當前被哪個線程使用到為標準,也就是說可以在thread1中open,然后再thread2中step,然后在thread3中close,只要這3個步驟不是同時進行的就沒有問題。但是如果是同一個sqlite對象同時在兩個線程中被調用,哪怕只是sqlite3_errmsg(sqlite);這種函數,也會引發崩潰!
驗證代碼:
#import "ViewController.h"
#import <sqlite3.h>
#define DATABASEPATH [[NSTemporaryDirectory() stringByAppendingPathComponent:@"testSql01.db"] UTF8String]
@interface ViewController (){
sqlite3 *sqlite;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *btnx = [UIButton buttonWithType:UIButtonTypeSystem];
[self.view addSubview:btnx];
[btnx setTitle:@"多線程打開" forState:UIControlStateNormal];
btnx.frame = CGRectMake(0, 200, 80, 20);
btnx.backgroundColor = [UIColor redColor];
[btnx addTarget:self action:@selector(openUnSafe) forControlEvents:UIControlEventTouchUpInside];
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
[self.view addSubview:btn];
[btn setTitle:@"串行打開" forState:UIControlStateNormal];
btn.frame = CGRectMake(0, 300, 80, 20);
btn.backgroundColor = [UIColor redColor];
[btn addTarget:self action:@selector(openSafe) forControlEvents:UIControlEventTouchUpInside];
UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeSystem];
[self.view addSubview:btn2];
[btn2 setTitle:@"創建" forState:UIControlStateNormal];
btn2.frame = CGRectMake(100, 300, 80, 20);
btn2.backgroundColor = [UIColor redColor];
[btn2 addTarget:self action:@selector(createDB) forControlEvents:UIControlEventTouchUpInside];
UIButton *btn3 = [UIButton buttonWithType:UIButtonTypeSystem];
[self.view addSubview:btn3];
[btn3 setTitle:@"并發插入" forState:UIControlStateNormal];
btn3.frame = CGRectMake(200, 300, 80, 20);
btn3.backgroundColor = [UIColor redColor];
[btn3 addTarget:self action:@selector(asynStep) forControlEvents:UIControlEventTouchUpInside];
UIButton *btn4 = [UIButton buttonWithType:UIButtonTypeSystem];
[self.view addSubview:btn4];
[btn4 setTitle:@"關閉" forState:UIControlStateNormal];
btn4.frame = CGRectMake(300, 300, 80, 20);
btn4.backgroundColor = [UIColor redColor];
[btn4 addTarget:self action:@selector(close) forControlEvents:UIControlEventTouchUpInside];
}
- (void)asynStep {
NSString *name = [NSString stringWithFormat:@"%d",arc4random()/20];
dispatch_queue_t queue = dispatch_queue_create([name UTF8String], DISPATCH_QUEUE_CONCURRENT);
// 并發操作
dispatch_async(queue, ^{
[self testMethod];
});
dispatch_async(queue, ^{
[self testMethod];
});
}
- (void)openSafe {
int openFlage = sqlite3_open_v2(DATABASEPATH, &sqlite, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL);
NSLog(@"path:%@",[NSString stringWithUTF8String:DATABASEPATH]);
if (openFlage != SQLITE_OK) {
sqlite3_close(sqlite);
NSLog(@"數據庫打開失敗!");
return;
}
NSLog(@"數據庫打開成功!");
}
- (void)openUnSafe {
int openFlage = sqlite3_open_v2(DATABASEPATH, &sqlite, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX, NULL);
NSLog(@"path:%@",[NSString stringWithUTF8String:DATABASEPATH]);
NSLog(@"%@",[NSThread currentThread]);
if (openFlage != SQLITE_OK) {
sqlite3_close(sqlite);
NSLog(@"數據庫打開失敗!");
return;
}
NSLog(@"數據庫打開成功!");
}
- (void)createDB {
NSLog(@"%@",[NSThread currentThread]);
NSString *deleteSql = @"delete from ClientTable";
char *err = 0;
int deleteFlag = sqlite3_exec(sqlite, [deleteSql UTF8String], NULL, NULL, &err);
if (deleteFlag != SQLITE_OK) {
NSLog(@"數據庫表刪除失敗!");
NSLog(@"%s",err);
}
NSString *sql = @"CREATE table if not exists ClientTable (name text, no text, signature text,PRIMARY KEY(no));delete from ClientTable";
int createFlag = sqlite3_exec(sqlite, [sql UTF8String], NULL, NULL, NULL);
if (createFlag != SQLITE_OK) {
NSLog(@"數據庫表創建失敗!");
}
NSLog(@"數據庫表創建成功!");
sqlite3_free(err);
}
- (BOOL)testMethod {
NSLog(@"%@",[NSThread currentThread]);
// 這里即使只是調用sqlite3_errmsg方法,如果是多線程中同時使用,也會引起崩潰
sqlite3_errmsg(sqlite);
return YES;
}
- (void)close {
if (sqlite3_close(sqlite) == SQLITE_OK) {
NSLog(@"數據庫關閉成功");
}
}
@end
結果:

GIF:

解釋:
1、第一次點擊并未崩潰,這里只是概率事件,因為sqlite3_errmsg執行的速度較快的話可能會不崩潰;
2、演示過程中,數據庫在多個線程進行了操作,但是只要不是同時的,就不會崩潰;
sqlite多線程的實現
根據源代碼可知,多線程模式主要是由bCoreMutex和bFullMutex來控制,具體原理和實現步驟本文不作深究。

智能推薦
SQLite數據庫基礎(三)
基于JAVA實現查詢功能的兩種方法以及比較(分別用.txt和.db文件存儲數據) 基于JAVA實現查詢功能的兩種方法以及比較(分別用.txt和.db文件存儲數據) 1) 將碼表放在一個文本文件中實現鄭碼查詢功能 2) 將碼表放在一個SQLite中實現鄭碼查詢功能 3)兩種方法的比較 編程實現一個命令窗程序,使得輸入“a”則在屏上回顯“一再”,使得能進...
Android學習(24)SQLite_數據庫的使用
Android學習(24)SQLite_數據庫的使用 SQLite數據庫介紹: 輕量級:SQLite是進程內的數據庫引擎,因此不存在數據庫的客戶端和服務器。使用SQLite一般只需要帶上它的一個動態庫,就可以享受它的全部功能。而且動態庫的尺寸也相當小。 獨立性:SQLite數據庫的核心引擎本身不依賴第三方軟件,使用它也不需要“安裝”,所以在使用的時候能夠省去不少麻煩 隔離性...
Android開發——SQLite的使用及數據庫基礎
一.基礎知識補充: 數據庫概述: 1.數據庫(DataBase,簡稱DB):指長期保存在計算機的存儲設備上,按照一定規則組織起來,可以被各種用戶或應用共享的數據集合。(屬于文件系統) 2.數據庫管理系統(DataBase Management System,簡稱DBMS):指一種操作和管理數據庫的大型軟件,用于建立、使用和維護數據庫,對數據庫進行統一管理和控制,以保證數據庫的安全性和完整性。用戶通...
猜你喜歡
freemarker + ItextRender 根據模板生成PDF文件
1. 制作模板 2. 獲取模板,并將所獲取的數據加載生成html文件 2. 生成PDF文件 其中由兩個地方需要注意,都是關于獲取文件路徑的問題,由于項目部署的時候是打包成jar包形式,所以在開發過程中時直接安照傳統的獲取方法沒有一點文件,但是當打包后部署,總是出錯。于是參考網上文章,先將文件讀出來到項目的臨時目錄下,然后再按正常方式加載該臨時文件; 還有一個問題至今沒有解決,就是關于生成PDF文件...
電腦空間不夠了?教你一個小秒招快速清理 Docker 占用的磁盤空間!
Docker 很占用空間,每當我們運行容器、拉取鏡像、部署應用、構建自己的鏡像時,我們的磁盤空間會被大量占用。 如果你也被這個問題所困擾,咱們就一起看一下 Docker 是如何使用磁盤空間的,以及如何回收。 docker 占用的空間可以通過下面的命令查看: TYPE 列出了docker 使用磁盤的 4 種類型: Images:所有鏡像占用的空間,包括拉取下來的鏡像,和本地構建的。 Con...
requests實現全自動PPT模板
http://www.1ppt.com/moban/ 可以免費的下載PPT模板,當然如果要人工一個個下,還是挺麻煩的,我們可以利用requests輕松下載 訪問這個主頁,我們可以看到下面的樣式 點每一個PPT模板的圖片,我們可以進入到詳細的信息頁面,翻到下面,我們可以看到對應的下載地址 點擊這個下載的按鈕,我們便可以下載對應的PPT壓縮包 那我們就開始做吧 首先,查看網頁的源代碼,我們可以看到每一...
Linux C系統編程-線程互斥鎖(四)
互斥鎖 互斥鎖也是屬于線程之間處理同步互斥方式,有上鎖/解鎖兩種狀態。 互斥鎖函數接口 1)初始化互斥鎖 pthread_mutex_init() man 3 pthread_mutex_init (找不到的情況下首先 sudo apt-get install glibc-doc sudo apt-get install manpages-posix-dev) 動態初始化 int pthread_...