iOS數(shù)據(jù)庫的使用(三):sqlite多線程

1. 引言

首先,sqlite支持多線程,但是是有條件的支持,也就是:

  • 同一個連接不能在多線程中使用,不同連接才可以在多線程中使用。

這個是最宏觀的sqlite多線程準(zhǔn)則。

另外,sqlite 的文件鎖是粗顆粒的,也就是以數(shù)據(jù)庫文件為維度加鎖,涉及到5種鎖狀態(tài)。

5中鎖狀態(tài)可以使用一句話來總結(jié):sqlite 在普通情況(非普通情況就是shared-cache+wal模式)下支持并發(fā)讀取操作,但是不支持并發(fā)寫入操作,且不支持并發(fā)寫入讀取混合操作。說白了就是只能并發(fā)讀取sqlite。

再者有了sqlite3.3以后的 shared-cache 模式 + WAL 模式,這兩種模式使 sqlite 支持并發(fā)寫入讀取混合操作,也就是寫入和讀取都可以并發(fā)且不影響了。

本文不涉及 SHARED-CACHE + WAL 模式,只講sqlite中的多線程及其意義和使用;

2. sqlite3中的三種線程模式

  • Single-thread(單線程)

編譯時設(shè)置 -DSQLITE_THREADSAFE 值為0,所有的互斥鎖都被禁止。

這種模式在極度要求速度的情況下被建議使用。因為沒有加鎖,所以在多線程中使用時是不安全的。該模式下性能最好,在性能優(yōu)先的模式下選擇。此時,一個數(shù)據(jù)庫只能在一個線程使用,即使是多個線程使用多個連接來訪問數(shù)據(jù)庫也不行;

  • Multi-thread(多線程)

編譯時設(shè)置 -DSQLITE_THREADSAFE 值為2,在部分地方加鎖,部分地方禁止了互斥鎖。

可以在多線程是使用多個連接,但是一個連接同時被多個線程使用時,是不安全的。

  • Serialized(串行)

編譯時設(shè)置 -DSQLITE_THREADSAFE 值為1,所有的互斥鎖都被開啟。

這種模式無論是多個連接在多線程中使用,還是單個連接在多線程中使用,最終都被被強制成串行執(zhí)行,所以是絕對線程安全的,但是性能最差,在安全性要求高的情況下選擇。

官方文檔:SQLITE_THREADSAFE=<0 or 1 or 2>

3. sqlite線程模式的設(shè)置

  • 編譯階段設(shè)置

通過使用編譯指令配置相關(guān)的參數(shù)。例如 iOS 中的 libsqlite3.tbd 就是被編譯之后的庫,這個 lib 中 sqlite3 的線程模式被配置成了 2,也就是多線程模式:

iOS中的線程模式

具體的指令如下:

gcc -DSQLITE_THREADSAFE=0 shell.c sqlite3.c -ldl

其意義是:編譯時設(shè)置 SQLITE_THREADSAFE 參數(shù)的值為0,編譯shell.c和sqlite3.c,生成命令行執(zhí)行程序。sqlite3編譯設(shè)置

  • 初始化階段設(shè)置

在調(diào)用 sqlite3_initialize() 之前使用 sqlite3_config() 函數(shù)設(shè)置。因為sqlite3_initialize()一般都被封裝在了open方法中,所以這個階段可以認(rèn)為是在調(diào)用open方法之前使用sqlite3_config()來設(shè)置線程模式。

  • 運行時設(shè)置

通過sqlite3_open_v2()中的第三個參數(shù)來設(shè)置,可選值為SQLITE_OPEN_NOMUTEX(無鎖即多線程模式),SQLITE_OPEN_FULLMUTEX(全鎖即串行模式)

4. 編譯時期設(shè)置為單線程

這里需要注意:

如果編譯器設(shè)置成單線程模式,即 SQLITE_THREADSAFE == 0。那么在其他時期就沒辦法重新啟用鎖的邏輯了;

至于為什么呢,可以直接看源碼。除了編譯時期,對于線程模式的配置有 start 和 open 兩個階段,start 階段是通過調(diào)用 sqlite_config() 來進行設(shè)置,open 階段是通過 open_v2() 方法傳遞 NOMUTEX 和 FULLMUTEX 兩個參數(shù)來進行配置;

先看看 sqlite_config 的源碼關(guān)鍵部分:

SQLITE_API int sqlite3_config(int op, ...){
  va_list ap;
  int rc = SQLITE_OK;

  switch( op ){

    /* Mutex configuration options are only available in a threadsafe
    ** compile.(config函數(shù)只在編譯時確定線程安全時才起作用)
    */
#if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE>0 
    case SQLITE_CONFIG_SINGLETHREAD: {
      /* This option sets the threading mode to Single-thread. */
      sqlite3GlobalConfig.bCoreMutex = 0;  /* Disable mutex on core */
      sqlite3GlobalConfig.bFullMutex = 0;  /* Disable mutex on connections */
      break;
    }
#endif

#if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE>0
    case SQLITE_CONFIG_MULTITHREAD: {
      /* This option sets the threading mode to Multi-thread. */
      sqlite3GlobalConfig.bCoreMutex = 1;  /* Enable mutex on core */
      sqlite3GlobalConfig.bFullMutex = 0;  /* Disable mutex on connections */
      break;
    }
#endif

#if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE>0 
    case SQLITE_CONFIG_SERIALIZED: {
      /* This option sets the threading mode to Serialized. */
      sqlite3GlobalConfig.bCoreMutex = 1;  /* Enable mutex on core */
      sqlite3GlobalConfig.bFullMutex = 1;  /* Enable mutex on connections */
      break;
    }
#endif
    省略......
}

如上代碼,三個 switch case 的前提都是 SQLITE_THREADSAFE>0 ,即串行模式或者多線程模式下,編譯器才會生成這些代碼。也就是說,編一階段將 SQLITE_THREADSAFE 設(shè)置為 0,那么這些代碼都會失效,這也是為什么此時無法在 start 階段設(shè)置線程模式的原因;

編譯時期為設(shè)置為單線程模式時不可以通過 config 將線程模式設(shè)置成其他模式模式,但是如果編譯時期是非單線程模式,可以通過 config 將線程模式設(shè)置回單線程模式;

從上面代碼也可以看到 sqlite 對線程模式控制的核心是 bCoreMutex 和 bFullMutex,這兩個東西后面再詳細看看,現(xiàn)在只需要知道 bCoreMutex 管理內(nèi)核相關(guān)的鎖,即線程鎖??bFullMutex 管理數(shù)據(jù)庫連接相關(guān)的鎖,即防止多線程訪問同一個連接??????不懂。。。

再來看看 open 方法的關(guān)鍵代碼:

if( sqlite3GlobalConfig.bCoreMutex==0 ){
    isThreadsafe = 0;
  }else if( flags & SQLITE_OPEN_NOMUTEX ){
    isThreadsafe = 0;
  }else if( flags & SQLITE_OPEN_FULLMUTEX ){
    isThreadsafe = 1;
  }else{
    isThreadsafe = sqlite3GlobalConfig.bFullMutex;
  }
    
  if( isThreadsafe
#ifdef SQLITE_ENABLE_MULTITHREADED_CHECKS
   || sqlite3GlobalConfig.bCoreMutex
#endif
  ){
      // 獲取遞歸鎖
      // 串行模式下線程絕對安全,所以這里直接對數(shù)據(jù)庫連接也進行了加鎖,防止多線程訪問該數(shù)據(jù)庫連接
      // 如果是NOMUTEX,即多線程模式,此時不會對連接加鎖,所以同一個連接被多個線程訪問時可能存在問題
    // 這里是創(chuàng)建所,還沒加鎖
    db->mutex = sqlite3MutexAlloc(SQLITE_MUTEX_RECURSIVE);
      
      // 加鎖失敗
    if( db->mutex==0 ){
      sqlite3_free(db);
      db = 0;
      goto opendb_out;
    }
      
    if( isThreadsafe==0 ){
      sqlite3MutexWarnOnContention(db->mutex);
    }
  }

// ...省略
// 這里本質(zhì)是調(diào)用pthread_mutex_lock進行加鎖
sqlite3_mutex_enter(db->mutex);

這里有幾個重點:

  • isThreadsafe 本質(zhì)是代表當(dāng)前線程模式是否為串行模式?。?!

即:

  • open 方法只有在串行模式下會對同一個 sqlite 連接添加線程鎖;

分析:

  1. 根據(jù) config 方法的配置(另外初始化全局config時也有體現(xiàn)),只有在單線程模式下,bCoreMutex==0 ,isThreadsafe = 0;
  2. open_v2 方法如果添加了 SQLITE_OPEN_NOMUTEX 標(biāo)識,即為多線程模式,官方明確說明了此時不允許多個線程訪問同一個連接,所以 sqlite 沒有進行加鎖,isThreadsafe = 0;
  3. open_v2 如果添加了 SQLITE_OPEN_FULLMUTEX 了,首先表示編譯時期不是單線程模式,不然設(shè)置會無效。另外,這個 flag 表示開啟串行模式,即全鎖,需要保證即使多個線程訪問同一個連接也是安全的。所以 isThreadsafe = 1;
  4. bFullMutex 表示連接鎖,bFullMutex = 1時在 config 代碼中也有體現(xiàn),此時為 bCoreMutex 也為 1 ,為串行模式;

綜上:

  • open 方法只有在串行模式下才會對同一個連接添加線程鎖;

bCoreMutex 和 bFullMutex

首先,根據(jù)上文 config 的源碼可以知道三種模式下這兩個值為:

各模式下的值

不存在 bCoreMutex = 1,bFullMutex = 1的情況;

其次,在 sqlite3GlobalConfig 初始化的時候也有體現(xiàn):

sqlite3GlobalConfig初始化

SQLITE_THREADSAFE 默認(rèn)為 1 ,即串行模式,0 為單線程模式,2 為多線程模式;

SQLITE_THREADSAFE

所以,bCoreMutex 的初始值正好和 SQLITE_THREADSAFE 默認(rèn)值相匹配;

如果編譯時期將 SQLITE_THREADSAFE 設(shè)置為了 0,那么即使初始化 sqlite3GlobalConfig 時,bCoreMutex == bFullMutex == 1,根據(jù)源碼,bCoreMutex 和 bFullMutex 相關(guān)的代碼直接不會被編譯,所以此時 bFullMutex 和 bCoreMutex 的值并沒有意義,即:什么鎖也不會加;

總結(jié):

  1. bFullMutex 表示是否需要對單個連接進行加鎖;
  2. bCoreMutex 核心鎖????

知道了 bFullMutex 用于單個連接的線程鎖,那么 bCoreMutex 是用在哪的?

全局搜一下 bCoreMutex ,果然很多結(jié)果,這也側(cè)面說明 bCoreMutex 的使用上比 bFullMutex 的頻率更高;

sqlite3_threadsafe()方法

該方法包含以下幾個重點:

  • 該方法返回編譯時期所設(shè)置的線程模式的值
  • 該方法返回值不受其他階段重置線程模式的影響,也就是說即使在初始化或者runtime階段改變了線程模式,該函數(shù)的返回值不變。

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,...)可以設(shè)置多個值
  • sqlite3_config不是線程安全的,當(dāng)該方法在執(zhí)行時,確保其他線程沒有調(diào)用該方法
  • 這里的線程模式的值是1、2、3,而編譯階段設(shè)置的線程模式的值為0、1、2
Muti-thread模式下的并發(fā)

根據(jù)官方文檔,只要保證了多線程中不同時使用同一個connect即可,所以path使用同一個,也就意味著使用同一個數(shù)據(jù)庫,但是并發(fā)中取創(chuàng)建新的db,也就是open的是不同的db,也就是和同一個數(shù)據(jù)庫建立了多個不同的連接,代碼如下
主要并發(fā)邏輯:

- (void)mutiThreadTest {
    // iOS中的sqlite3lib默認(rèn)是2,也就是muti-thread。也就是說應(yīng)用程序需要自己去保證不再多線程中同時使用同一個數(shù)據(jù)庫連接(database-connection)。也就是說,可以通過建立多個數(shù)據(jù)庫連接來實現(xiàn)并行訪問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);
}

創(chuàng)建數(shù)據(jù)庫方法:

- (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);
}

結(jié)果:
step方法報錯,錯誤碼為5,即SQLITE_BUSY;
所以,sqlite中的線程安全意味著什么呢?

sqlite的多線程模式中的條件的具體意義

sqlite的線程安全意味著數(shù)據(jù)的安全,如果錯誤的使用將會導(dǎo)致異常,比如崩潰、數(shù)據(jù)錯亂。

如官方文檔中鎖描述的,其本質(zhì)是不要在多線程中同時使用同一個connect或者statement,這里的具體意思:
connect的代表是sqlite3對象,所以這里的維度不是以open-close為維度,而是以sqlite對象當(dāng)前被哪個線程使用到為標(biāo)準(zhǔn),也就是說可以在thread1中open,然后再thread2中step,然后在thread3中close,只要這3個步驟不是同時進行的就沒有問題。但是如果是同一個sqlite對象同時在兩個線程中被調(diào)用,哪怕只是sqlite3_errmsg(sqlite);這種函數(shù),也會引發(fā)崩潰!

驗證代碼:

#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:@"創(chuàng)建" 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:@"并發(fā)插入" 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:@"關(guān)閉" 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);
    
    // 并發(fā)操作
    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(@"數(shù)據(jù)庫打開失??!");
        return;
    }
    NSLog(@"數(shù)據(jù)庫打開成功!");

}

- (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(@"數(shù)據(jù)庫打開失??!");
        return;
    }
    NSLog(@"數(shù)據(jù)庫打開成功!");
    
}

- (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(@"數(shù)據(jù)庫表刪除失?。?);
        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(@"數(shù)據(jù)庫表創(chuàng)建失?。?);
    }
    NSLog(@"數(shù)據(jù)庫表創(chuàng)建成功!");
    sqlite3_free(err);
}


- (BOOL)testMethod {
    NSLog(@"%@",[NSThread currentThread]);
    
    // 這里即使只是調(diào)用sqlite3_errmsg方法,如果是多線程中同時使用,也會引起崩潰
    sqlite3_errmsg(sqlite);
    return YES;
}

- (void)close {
    if (sqlite3_close(sqlite) == SQLITE_OK) {
        NSLog(@"數(shù)據(jù)庫關(guān)閉成功");
    }
}

@end

結(jié)果:


resutl

GIF:


result

解釋:
1、第一次點擊并未崩潰,這里只是概率事件,因為sqlite3_errmsg執(zhí)行的速度較快的話可能會不崩潰;

2、演示過程中,數(shù)據(jù)庫在多個線程進行了操作,但是只要不是同時的,就不會崩潰;

sqlite多線程的實現(xiàn)

根據(jù)源代碼可知,多線程模式主要是由bCoreMutex和bFullMutex來控制,具體原理和實現(xiàn)步驟本文不作深究。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容