從0開始弄一個面向OC數據庫(五)--多線程安全

前言

經過一步一個腳印的開發,咱們實現了數據庫的增刪查改,並支持多種類型數據存儲,如:全部基本數據類型,NSArray,NSMutableArray,NSDictionary,NSMutableDictionary,UIImage,NSURL,UIColor,NSSet,NSRange,NSAttributedString,NSData,自定義模型以及數組、字典、模型相互嵌套的複雜場景。html

而後咱們很是完美的將打開數據庫,建立數據庫表格,解析模型對象,插入數據,更新數據,數據庫升級,數據遷移,關閉數據庫一系列步驟封裝成一個方法,一行代碼智能實現複雜模型的數據存儲。若是想了解各個部分是如何實現的,能夠前往以前的文章,傳送門:git

本篇主要解決多線程安全問題,而後會隨着講一下經常使用的多線程安全技術以及關於在ARC下使用@autorelease的一個必要的場景,最後會分享咱們在進行單元測試的時候遇到的一個小坑。github

功能實現

實現功能以前,咱們先知道多線程安全要作什麼?簡單的來講,咱們就是要保證在多個線程同時對數據庫進行操做的時候是安全的,也能夠說咱們要保證全部數據庫的操做無論從哪一個線程過來都要等前面的操做執行完畢再執行本操做,避免資源競爭和衝突。sql

而後咱們要去了解一下OC下保證多線程安全的手段,對於OC咱們最多見的有原子性atomic,而後有NSLock鎖、@synchronized、GCD的信號量、串行隊列。數據庫

當咱們在糾結選擇何種方案的時候,咱們能夠先去看看前輩們的開源是如何作數據庫線程安全的,借鑑一下,最終咱們總結出兩個比較優秀的方案:一種方案是FMDB所使用的同步串行隊列:全部的操做都用一個串行的隊列排好,一個個操做排隊進行。另外一種是使用GCD信號量dispatch_semaphore_t。結合咱們以前寫的代碼,根據咱們目前的數據庫方案,快速對比一下哪一種更適合咱們,最終咱們選擇了GCD信號量,使用這個方案,咱們的代碼基本不用變更數組

在GCD中有三個函數是semaphore的操做,分別是:緩存

  • dispatch_semaphore_create   建立一個semaphore
  • dispatch_semaphore_wait    等待信號
  • dispatch_semaphore_signal   發送一個信號

簡單的介紹一下這三個函數:安全

dispatch_samaphore_t dispatch_semaphore_create(long value);
這個函數有一個長整形的參數,咱們能夠理解爲信號的總量;

long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
這個函數第一個參數爲信號量,第二個爲等待的時間,這個函數的做用是這樣的:
若是dsema信號量的值大於0,該函數所處線程就繼續執行下面的語句,而且將信號量的值減1;
若是desema的值小於等於0,那麼這個函數就阻塞當前線程等待timeout(注意timeout的類型爲dispatch_time_t);
若是在等待的期間desema大於0了,則向下執行操做並講信號量減1.

long dispatch_semaphore_signal(dispatch_semaphore_tdsema)
這個函數會使傳入的信號量dsema的值加1;

複製代碼

同時考慮到咱們目前的方法都是類方法,咱們須要一個實例來記住desema的值,因而咱們給CWSqliteModelTool開啓一個單例對象來記錄desema的值,設置信號總量爲1,以後在每個執行數據庫操做的的方法開始前進行等待信號量,若是當前信號量大於0,咱們執行操做數據庫,並講信號量減1,當操做完成以後,咱們發送信號,使信號量增1,這樣使其餘在等待的線程能開始執行操做,以執行查詢操做爲例:bash

- (instancetype)init
{
    self = [super init];
    if (self) {
        // 設置信號量爲1,表示最多同時只有1個線程進行操做
        self.dsema = dispatch_semaphore_create(1);
    }
    return self;
}
// 建立一個單例,來記錄信號量的值
static CWSqliteModelTool * instance = nil;
+ (instancetype)shareInstance {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[CWSqliteModelTool alloc] init];
    });
    return instance;
}

// 查詢表內全部數據
+ (NSArray *)queryAllModels:(Class)cls uid:(NSString *)uid targetId:(NSString *)targetId {
    // 等待信號量,若是大於0,向下執行操做,不然等待
    dispatch_semaphore_wait([[self shareInstance] dsema], DISPATCH_TIME_FOREVER);
    
    NSString *tableName = [CWModelTool tableName:cls targetId:targetId];
    NSString *sql = [NSString stringWithFormat:@"select * from %@", tableName];
    
    NSArray <NSDictionary *>*results = [CWDatabase querySql:sql uid:uid];
    [CWDatabase closeDB];
    // 發送信號量,使信號量+1
    dispatch_semaphore_signal([[self shareInstance] dsema]);
    
    return [self parseResults:results withClass:cls];
}

複製代碼

咱們在其餘的方法內寫上一樣的代碼,而後咱們使用插入數據與刪除數據的方法進行單元測試,測試方案爲,開啓3條子線程,每條線程分別插入1000個複雜的模型,當數據插入結束的時候,再使用3條子線程刪除其中的2900條數據,最後剩下100條數據,而後咱們來進行單元測試:多線程

在使用單元測試時,分享一個咱們發現的坑~咱們發現一條數據都沒插入成功或者偶爾插入了一兩條數據,咱們反覆檢測咱們的代碼,理論上都是沒問題的,最終咱們定位到子線程隊列的任務壓根沒執行,思考以後最終咱們得出結論:單元測試是在主線程運行,咱們使用異步線程時並不會阻塞主線程的運行,因此這個測試用例順暢無阻的從第一行執行到了最後一行,而單元測試執行完最後一行以後程序就退出了,程序都退出了咱們異步的線程的操做固然無法再執行了~

因此咱們不能使用單元測試(或者在單元測試本身開啓一個runloop讓程序不退出),改在程序內進行測試,貼上咱們很是長的測試代碼(viewController內):

#pragma mark - for循環未使用autoreleasepool的多線程操做
- (void)testMultiThreadingSqliteMore {
    
    dispatch_queue_t queue1 = dispatch_queue_create("CWDBTest1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue2 = dispatch_queue_create("CWDBTest2", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue3 = dispatch_queue_create("CWDBTest3", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue4 = dispatch_queue_create("CWDBTest4", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_enter(group);
    dispatch_group_enter(group);
    dispatch_group_enter(group);
    
    dispatch_async(queue1, ^{
        for (int i = 1; i < 1000; i++) {
            Student *stu = [self studentWithId:i];
            BOOL result = [CWSqliteModelTool insertOrUpdateModel:stu uid:@"Chavez" targetId:nil];
            NSLog(@"result : %d %zd",result,stu.stuId);
        }
        NSLog(@"---------------組1結束");
        dispatch_group_leave(group);
    });
    
    dispatch_async(queue2, ^{
        for (int i = 1000; i < 2000; i++) {
            Student *stu = [self studentWithId:i];
            BOOL result = [CWSqliteModelTool insertOrUpdateModel:stu uid:@"Chavez" targetId:nil];
            NSLog(@"result : %d %zd",result,stu.stuId);
        }
        NSLog(@"---------------組2結束");
        dispatch_group_leave(group);
    });
    
    dispatch_async(queue3, ^{
        for (int i = 2000; i < 3000; i++) {
            Student *stu = [self studentWithId:i];
            BOOL result = [CWSqliteModelTool insertOrUpdateModel:stu uid:@"Chavez" targetId:nil];
            NSLog(@"result : %d %zd",result,stu.stuId);
        }
        NSLog(@"---------------組3結束");
        dispatch_group_leave(group);
    });
    
    // 當前面3個隊列的任務都完成,則調用此通知
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"----------------------插入結束");
        dispatch_async(queue4, ^{
            for (int i = 1; i < 1000; i++) {
                Student *stu = [self studentWithId:i];
                // 刪除數據
                BOOL result = [CWSqliteModelTool deleteModel:stu uid:@"Chavez" targetId:nil];
                NSLog(@"delete result : %d %zd",result,stu.stuId);
            }
        });
        dispatch_async(queue1, ^{
            for (int i = 2000; i < 3000; i++) {
                Student *stu = [self studentWithId:i];
                // 刪除數據
                BOOL result = [CWSqliteModelTool deleteModel:stu uid:@"Chavez" targetId:nil];
                NSLog(@"delete result : %d %zd",result,stu.stuId);
            }
        });
        
        dispatch_async(queue2, ^{
            // 刪除數據
            BOOL result = [CWSqliteModelTool deleteModel:[Student class] columnNames:@[@"stuId",@"stuId"] relations:@[@(CWDBRelationTypeMoreEqual),@(CWDBRelationTypeLess)] values:@[@(1000),@(1900)] isAnd:YES uid:@"Chavez" targetId:nil];
            NSLog(@"delete result : %d 1000-1900",result);
        });
    });
}

#pragma mark - 快速獲取一個模型
- (Student *)studentWithId:(int)stuId {
    School *school1 = [[School alloc] init];
    school1.name = @"北京大學";
    school1.schoolId = 2;
    
    School *school = [[School alloc] init];
    school.name = @"清華大學";
    school.schoolId = 1;
    school.school1 = school1;
    
    Student *stu = [[Student alloc] init];
    stu.stuId = stuId;
    stu.name = @"Baidu";
    stu.age = 100;
    stu.height = 190;
    stu.weight = 140;
    stu.dict = @{@"name" : @"chavez"};
    // 字典嵌套模型
    stu.dictM = [@{@"清華大學" : school , @"北京大學" : school1 , @"money" : @(100)} mutableCopy];
    // 數組嵌套字典,字典嵌套模型
    stu.arrayM = [@[@"chavez",@"cw",@"ccww",@{@"清華大學" : school}] mutableCopy];
    // 數組嵌套模型
    stu.array = @[@(1),@(2),@(3),school,school1];
    NSAttributedString *attributedStr = [[NSAttributedString alloc] initWithString:@"attributedStr,attributedStr"];
    stu.attributedString = attributedStr;
    // 模型嵌套模型
    stu.school = school;
    UIImage *image = [UIImage imageNamed:@"001"];
    NSData *data = UIImageJPEGRepresentation(image, 1);
    stu.image = image;
    stu.data = data;
    
    return stu;
}
複製代碼

而後執行,獲取測試結果,可是在反覆側測試的時候,咱們發現一個問題,以下圖:

發現內存最高漲到了恐怖的500M!!程序運行起來52M,瞬間翻了10倍。且結束以後內存還有90多M,這必定是咱們程序的問題!! 對於有經驗的人來講,他們必定能立刻定位到問題出如今哪裏,甚至他們在寫代碼的時候就能知道這樣寫會有問題,而我是沒有經驗的,我思考了一陣,因爲有一點理論的知識,最終定位到多是for循環建立了大量的臨時變量沒有被及時釋放致使的,而後根據以前有看到過使用@autoreleasepool釋放臨時變量,蘋果官方文檔有(Using Autorelease Pool Blocks)說到:當有大量中間臨時變量產生時,爲了不內存使用峯值太高,應到使用@autoreleasepool及時釋放內存。最終咱們修改代碼,並進行測試:
測試時發現,內存一直維持在54M左右,效果很是明顯,基本上和程序剛啓動佔用的內存差很少,經過此次經驗,咱們更加深刻的理解,在ARC環境下,如何使用@autoreleasepool來控制程序的內存。而後咱們打開數據庫檢測數據是不是剩下對應的100條數據,測試是沒問題的,而後咱們再調用咱們本身寫的查詢數據庫方法進行查詢,查詢的結果也是沒問題的。

在咱們解決了多線程安全的問題以後咱們發現,既然可能有存在須要批量插入數據的狀況,咱們就多增長一個接口來處理批量插入操做,批量插入實際上就是咱們替用戶進行for循環插入,可是在插入的過程當中,咱們用事務控制插入操做,而且插入過程比用戶少的是每次插入數據都要執行打開和關閉數據庫操做以及每次都去檢測是否須要更新數據庫表。

#pragma mark - 測試批量插入數據
- (void)testGroupInsert {
    NSMutableArray *arr = [NSMutableArray array];
    for (int i = 1; i < 2000; i++) {
        Student *stu = [self studentWithId:i];
        [arr addObject:stu];
    }
    NSLog(@"開始插入數據");
    // 2017-12-23 16:25:46.145023+0800 CWDB[14678:1604328] 開始插入數據
    BOOL result = [CWSqliteModelTool insertOrUpdateModels:arr uid:@"Chavez" targetId:nil];
    NSLog(@"---%zd---插入結束",result);
    // 2017-12-23 16:25:48.466352+0800 CWDB[14678:1604328] ---1---插入結束
    // 使用批量插入的方法 插入2000條數據,總共耗時2.3秒
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"---------------組1開始");
        // 2017-12-23 16:25:48.466587+0800 CWDB[14678:1604407] ---------------組1開始
        for (int i = 2000; i < 4000; i++) {
            @autoreleasepool {
                Student *stu = [self studentWithId:i];
                BOOL result = [CWSqliteModelTool insertOrUpdateModel:stu uid:@"Chavez" targetId:nil];
                NSLog(@"result : %d %zd",result,stu.stuId);
            }
        }
        NSLog(@"---------------組1結束");
        // 2017-12-23 16:25:56.247631+0800 CWDB[14678:1604407] ---------------組1結束
        // 自行遍歷的方式插入2000條數據,總共耗時8秒(且要自行增長autoreleasepool釋放臨時變量)
    });
    
}
複製代碼

最終咱們經過批量插入以及單個插入2000條數據的時間比較,批量插入消耗的時間遠低於單個分別插入。

在寫完功能以後,咱們對項目又進行了一小部分緩存優化,在不考慮動態給模型添加屬性的狀況下,咱們每次去獲取模型成員變量以及類型必定是同樣的,因此,首先咱們在這裏使用NSCache來緩存了模型的全部成員變量名以及類型,這樣能夠不用每次都去解析模型。其次,在判斷數據庫表結構是否須要更新的時候,咱們也作了緩存,在程序運行期間,只要更新過一次,後面都不用去判斷更新,由於成員變量不變,表結構必定不會變,這都是對性能方面的一些小優化,在作的時候能夠適當的考慮一下。

本篇結束

在此,咱們封裝的數據庫功能已經開發完了(其實本身封裝一個數據庫也沒想象中那麼難,你也能夠的~)回到第一篇文章所立的軍令狀:咱們封裝的簡單適用,安全可靠,功能全面,咱們說到作到。增刪查改全部操做都只須要一行代碼。就算你給數組添加一個對象也須要兩步:先初始化一個數組,而後再想數組添加對象,而咱們向數據庫插入一個模型,只須要一步,調用insertOrUpdateModel:便可。

在接下來,咱們會將封裝的代碼進行少許的重構和優化,去掉一些沒必要要暴露的方法和對應的單元測試,儘可能讓API簡潔明瞭以及去掉一些重複代碼的封裝,將註釋補全再通過大量的測試場景測試以後,咱們將會把咱們的CWDB推薦給你們使用,若是你有興趣瞭解或者想本身動手封裝一個數據庫,能夠前往本系列文章第一篇開始看一看(文章的鏈接在本文的開頭),每篇文章對應的代碼在github的release下都有分別的tag,你能夠找到他而且下載下來。。

本篇文章實現的代碼地址:github:CWDB ------tag爲1.4.0-------

(注意:若是要直接運行,必須在CWDatabase.m開頭的位置修改數據庫存放的路徑,開發調試階段我寫在了我電腦的桌面,不修改會出現路徑錯誤,致使打開數據庫失敗)

最後以爲有用的同窗,但願能給本文點個喜歡,給github點個star以資鼓勵,謝謝你們。歡迎你們向我拋issue,有更好的思路也歡迎你們留言。

給你們安利一個0耦合的仿QQ側滑框架,真正的一行代碼實現,多了你抽我😁: 一行代碼集成超低耦合的側滑功能

相關文章
相關標籤/搜索