GCD的使用技巧

多線程編程相關概念

串行(Serial)與并發(Concurrent)

這兩個詞都是形容當執行任務時是否需要考慮其它任務。
串行:任務只能一個接著一個地執行
并發:同一時間內,多個任務可以是同時執行的

同步(Synchronous)與異步(Asychronous)

這兩個詞是用來形容,被調用的函數在什么時候將控制權返回給調用者。
同步:被調用的函數只會在執行完才將控制權給回調用者。
異步:被調用的函數會馬上將控制權返回給調用者,不管函數是否執行完。因此異步函數不會阻塞當前進程。

線程安全

能被多個線程同時執行的資源,且不會出現資源競爭、死鎖、順序錯亂、優先級翻轉等問題

危險代碼段(Critical Section)

不能被并發執行的代碼,不能被兩個線程同時執行,比如因為修改共享的資源或者變量,只要運行就會立即崩潰或者是出錯

資源競爭


如圖,有一個變量存儲著17,然后線程A讀取變量得到17,線程B也讀取了這個變量。兩個線程都進行了加1運算得,并寫入18都變量。從而導致了崩潰。這就是race condition,多線程使用共享資源,而沒有確保其它線程是已經結束使用共享資源

互相排除


如圖,使用鎖對線程正在使用的共享資源進行鎖定,當共享資源使用完后。其它線程才能使用這個共享變量。這樣就能避免race condition但會導致死鎖。

死鎖(Deadlock)


兩個線程等待著彼此的完成而陷入的困境稱為死鎖。

void swap(A, B)
{
    lock(lockA);
    lock(lockB);
    int a = A;
    int b = B;    
    A = b;    
    B = a;
    unlock(lockB);
    unlock(lockA);
}
swap(X, Y); 
// 線程 thread 1
swap(Y, X); 
// 線程 thread 2

結果就會導致X被線程1鎖住,Y被線程2鎖住。而線程1又不能使用Y直到線程2解鎖,同理,線程2也不能使用X。這就是死鎖,互相等待。

優先順序顛倒(Priority Inversion)

優先順序顛倒問題,是由低權限任務的阻塞著高權限任務的執行。從而讓順序顛倒。



假設低權限的線程和高權限的線程使用共享資源。本應該,低權限的線程任務使用完共享資源后高權限的線程任務就能沒有延遲地執行。
但由于一開始高權限的線程因為低線程的鎖而受到阻塞。所以就給了機會中權限的線程任務,因為現在高權限受阻,所以中權限的線程任務是權限最高的,所以中權限任務中斷低權限的線程任務的執行。從而讓低線程的鎖解不開,高線程任務也就延遲執行。從而優先順序顛倒。
所以在使用GCD的時候,最好將多線程的任務執行優先權限保持一致。

蘋果工程師在swift-dev郵件列表中討論weak屬性的線程安全問題的郵件里爆出自旋鎖有bug,郵件地址:https://lists.swift.org/pipermail/swift-dev/Week-of-Mon-20151214/000372.html。大概就是不同優先級線程調度算法會有優先級反轉問題,比如低優先級獲鎖訪問資源,高優先級嘗試訪問時會等待,這時低優先級又沒法爭過高優先級導致任務無法完成lock釋放不了。

GCD的隊列類型

QOS服務等級

隊列服務等級指的是優先級的抽象,針對的是在系統資源出現競爭或者是高性能負荷的情況下,根據服務等級動態調整資源分配,對于使用GCD的開發者來說,預先將程序中的隊列進行服務等級的劃分和管理,將需要進行執行的任務按照服務層次劃分,則可以更好的利用系統智能調度資源的特性。比如,響應用戶對請求的任務、無須等待的后臺任務、需要長時間且高性能保證的任務等等。

//創建QOS服務等級的隊列
    dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_USER_INTERACTIVE, QOS_MIN_RELATIVE_PRIORITY);
    dispatch_queue_t concurrent_queue = dispatch_queue_create("concurrent_queue.com", attr);
    dispatch_async(concurrent_queue, ^{
        NSLog(@"concurrent_queue task exc");
    });

QOS服務等級如下:

/*
QOS_CLASS_USER_INTERACTIVE:user interactive等級表示任務需要被立即執行提供好的體驗,用來更新UI,響應事件等。這個等級最好保持小規模。
QOS_CLASS_USER_INITIATED:user initiated等級表示任務由UI發起異步執行。適用場景是需要及時結果同時又可以繼續交互的時候。
QOS_CLASS_UTILITY:utility等級表示需要長時間運行的任務,伴有用戶可見進度指示器。經常會用來做計算,I/O,網絡,持續的數據填充等任務。這個任務節能。
QOS_CLASS_BACKGROUND:background等級表示用戶不會察覺的任務,使用它來處理預加載,或者不需要用戶交互和對時間不敏感的任務。
*/
__QOS_ENUM(qos_class, unsigned int,
    QOS_CLASS_USER_INTERACTIVE
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x21,
    QOS_CLASS_USER_INITIATED
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x19,
    QOS_CLASS_DEFAULT
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x15,
    QOS_CLASS_UTILITY
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x11,
    QOS_CLASS_BACKGROUND
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x09,
    QOS_CLASS_UNSPECIFIED
            __QOS_CLASS_AVAILABLE_STARTING(__MAC_10_10, __IPHONE_8_0) = 0x00,
);

從服務類型這個維度區分,可能會將本應該串行執行或者是并行執行的任務,卻因服務類型不同而切分不同的隊列,比如,一個網絡請求任務,一個相應用戶事件的任務,本應該兩個任務是串行執行的,但劃分了不同的QOS,導致放在兩個串行隊列中執行,這個兩個任務就成并行任務了,解決這個問題可以使用設置目標隊列。

設置目標隊列,劃分隊列層次

dispatch_queue_t targetQueue = dispatch_queue_create("test.target.queue", DISPATCH_QUEUE_SERIAL);  
      //這三個隊列可標記服務層級
    dispatch_queue_t queue1 = dispatch_queue_create("test.1", DISPATCH_QUEUE_SERIAL);  
    dispatch_queue_t queue2 = dispatch_queue_create("test.2", DISPATCH_QUEUE_SERIAL);  
    dispatch_queue_t queue3 = dispatch_queue_create("test.3", DISPATCH_QUEUE_SERIAL);  
      
    dispatch_set_target_queue(queue1, targetQueue);  
    dispatch_set_target_queue(queue2, targetQueue);  
    dispatch_set_target_queue(queue3, targetQueue);  
    dispatch_async(queue1, ^{  
        NSLog(@"1 in");  
        [NSThread sleepForTimeInterval:3.f];  
        NSLog(@"1 out");  
    });  
  
    dispatch_async(queue2, ^{  
        NSLog(@"2 in");  
        [NSThread sleepForTimeInterval:2.f];  
        NSLog(@"2 out");  
    });  
    dispatch_async(queue3, ^{  
        NSLog(@"3 in");  
        [NSThread sleepForTimeInterval:1.f];  
        NSLog(@"3 out");  
    });  

各種隊列使用的情況

  • dispatch_get_main_queue()-主隊列(串行隊列):需要異步向UI主線程提交任務或隊列中有任務完成需要更新UI時,dispatch_after在這種類型中使用,不能同步提交或者是提交阻塞任務。
  • 自定義順序隊列:順序執行后臺任務并追蹤它時。這樣做同時只有一個任務在執行可以防止資源競爭。dipatch barriers解決讀寫鎖問題的放在這里處理。dispatch groups也是放在這里。
  • 并發隊列:用來執行與UI無關的后臺任務,dispatch_sync放在這里,方便等待任務完成進行后續處理或和dispatch barrier同步。dispatch groups放在這里也不錯。

切莫濫用dispatch_apply

dispatch_apply(100000, dispatch_queue_create(DISPATCH_CURRENT_QUEUE_LABEL, NULL), ^(size_t i) {
        NSLog(@"%ld",i);
    });

dispatch_apply類似于for循環,循環體的邏輯可放在并發隊列中并行執行,可加快for循環速度,但切莫濫用,因為GCD理論上可并發創建64條線程,當并發隊列執行較多任務時,dispatch_apply提交到并發隊列的動作則會阻塞其所在線程。

使用串行同步隊列保證數據同步

-(NSString *)name{
    __block NSString * localName ;
    dispatch_sync(_t, ^{
        localName = _name;
    });
    return localName;
}
-(void)setName:(NSString *)name{
    dispatch_sync(_t, ^{
        _name = name;
    });
}
self.t =dispatch_queue_create("com.topic-gcd.com", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        self.name = @"111";
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@"%@",self.name);
    });

使用串行異步隊列保證數據同步

- (NSString *)name1{
    __block NSString * localName1;
    dispatch_sync(_t, ^{
        for (int i = 0; i<10000; i++) {
            
        }
        localName1 = _name1;
    });
    return localName1;
}
-(void)setName1:(NSString *)name1{
    dispatch_async(_t, ^{
        _name1 = name1;
    });
}
NSDate * date = [NSDate date];
    self.name = @"222";
    NSLog(@"%@",self.name);
    NSLog(@"name----%f",[NSDate date].timeIntervalSinceNow - date.timeIntervalSinceNow);
    date = [NSDate date];
    self.name1 = @"111";
    NSLog(@"%@",self.name1);
    NSLog(@"name1----%f",[NSDate date].timeIntervalSinceNow - date.timeIntervalSinceNow);

結果如下:

2017-04-11 13:33:45.512 topic-gcd[52228:4492576] 222
2017-04-11 13:33:45.513 topic-gcd[52228:4492576] name----0.000390
2017-04-11 13:33:45.513 topic-gcd[52228:4492576] 111
2017-04-11 13:33:45.513 topic-gcd[52228:4492576] name1----0.000198

使用sync(read)和barrier_async(write)并利用并發隊列,執行數據讀寫任務,確保read、write有序,防止多線程中資源死鎖。

- (void)setName2:(NSString *)name2{
    dispatch_barrier_async(self.t, ^{
        _name2 = name2;
    });
}
- (NSString *)name2{
    __block NSString * localName2;
    dispatch_sync(self.t, ^{
        for (int i = 0; i<1000000; i++) {
            
        }
        localName2 = _name2;
    });
    return localName2;
}
//使用并行隊列,讀操作異步執行,寫操作同步執行
    self.t = dispatch_queue_create(DISPATCH_CURRENT_QUEUE_LABEL, NULL);
    
    self.name2 = @"33";
    NSLog(@"1.read name2---->%@",self.name2);
    NSLog(@"2.read name2---->%@",self.name2);
    NSLog(@"3.read name2---->%@",self.name2);
    self.name2 = @"333";
    NSLog(@"4.read name2---->%@",self.name2);
    NSLog(@"5.read name2---->%@",self.name2);
    NSLog(@"6.read name2---->%@",self.name2);

dispatch_barrier_async為什么適合使用在自定義并發隊列呢?

  1. 串行隊列本來就是一個任務一個任務地串行執行,沒有意義
  2. 全局隊列(Global Concurrent Queue),因為全局隊列很多地方在用,包括系統的API也在用,使用dispatch_barrier_async可以阻塞此隊列,所以不建議使用
  3. 自定義并發隊列,只要并發隊列里使用的是資源線程安全的。所以比較建議

使用Dispatch Group,將不同隊列進行分組,監聽或等待同一組的隊列任務

Dispatch Group將不同隊列設置為一個group,可以利用dispatch_group_wait設置阻塞點,等待整個組內的異步任務完成,或者是使用dispatch_group_notify異步執行完成組內任務的回調,從而不阻塞當前線程。注意,group中如果添加主隊列再使用dispatch_group_wait有可能引起死鎖。

//使用Dispatch Group,并發隊列異步執行任務
    dispatch_group_t group = dispatch_group_create();
    //在主線程隊列中執行
    dispatch_group_async(group, dispatch_get_main_queue(), ^{
        NSLog(@"任務1執行");
    });
    //在創建的一個并行隊列中執行
    dispatch_group_async(group, self.t, ^{
        NSLog(@"任務2執行");
    });
    //在全局隊列中執行
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"任務3執行");
    });
    //死鎖
//    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
//    NSLog(@"任務4執行");
    //在主線程隊列中監聽所有任務執行完畢的回調
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"任務5執行");
    });

使用dispatch_group_enter與dispatch_group_leave合并異步任務組

對于某些異步任務,想要利用任務組來管理達到監聽完成回調或者是阻塞當前線程進行線程同步,則可以使用dispatch_group_enter與dispatch_group_leave,例子如下:

//使用dispatch_group_enter與dispatch_group_leave合并異步任務組
    dispatch_group_t block_group = dispatch_group_create();
    dispatch_group_enter(block_group);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"異步任務1");
        dispatch_group_leave(block_group);
    });
    dispatch_group_enter(block_group);
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"異步任務2");
        dispatch_group_leave(block_group);
    });
    dispatch_group_notify(block_group, dispatch_get_main_queue(), ^{
        NSLog(@"異步任務1和任務2全部完成");
    });

雖然異步任務1與異步任務2是不同類型的隊列里面的任務,但可以通過enter與leave來達到將兩個任務進行任務組編排,比方說一個界面需要等待三個接口調用全部完成才能夠初始化,除了以前設置哨兵變量以外,還可以使用上面的方式來進行任務編排。在使用CoreData的時候就可以將異步數據庫操作形成任務組。

dispatch_group_enter(group);
[self.context performBlock:^(){
     //some core data actions
     dispatch_group_leave(group);
}];

需要注意的是:
dispatch_group_async等價于dispatch_group_enter() 和 dispatch_group_leave()的組合。
dispatch_group_enter() 必須運行在 dispatch_group_leave() 之前。
dispatch_group_enter() 和 dispatch_group_leave() 需要成對出現的

使用dispatch_block_wait與dispatch_block_notify來設置任務block的異步阻塞或者是異步完成回調監聽

//dispatch_block_wait使用
dispatch_queue_t block_serialQueue = dispatch_queue_create("com.block_serialQueue.com", DISPATCH_QUEUE_SERIAL);
dispatch_block_t block = dispatch_block_create(0, ^{
    [NSThread sleepForTimeInterval:5.f];
    NSLog(@"block_serialQueue block end");
});
dispatch_async(block_serialQueue, block);
//設置DISPATCH_TIME_FOREVER會一直等到前面任務都完成
dispatch_block_wait(block, DISPATCH_TIME_FOREVER);
block = dispatch_block_create(0, ^{
    NSLog(@"second block_serialQueue block end");
});
dispatch_async(block_serialQueue, block);
dispatch_block_notify(block, dispatch_get_main_queue(), ^{
    NSLog(@"block_serialQueue block finished");
});

當某一個異步任務block,需要監聽其是否完成,可以使用wait與notify來簡化代碼邏輯。例如,可使用該特性,來組合異步任務的執行順序呢,主要是針對已存在的異步任務邏輯代碼。比方說,可將網絡請求的異步任務進行串聯。

使用Dispatch Group與dispatch_block_cancel,取消異步任務

可以使用group與dispatch_block_cancel將一組編排好的異步任務組中的異步任務進行取消,比方說在某個視圖控制器中,發起了N個網絡請求,離開界面的時候,如果網絡請求沒有全部結束,而用戶退出該界面,則可以取消這些異步任務回收線程資源,簡單實現如下:

 //使用Dispatch Group與dispatch_block_cancel,取消異步任務
    NSMutableArray * request_blocks = [NSMutableArray array];
    dispatch_group_t request_blocks_group = dispatch_group_create();
    //開啟五個異步網絡請求任務
    for (int i = 0; i<5; i++) {
        dispatch_block_t request_block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, ^{
            NSURLRequest * request = [[NSURLRequest alloc]init];
            dispatch_group_enter(request_blocks_group);
           [self postWithRequest:request completion:^(id responseObjecy, NSURLResponse *response, NSError *error) {
              //do somethings
               dispatch_group_leave(request_blocks_group);
           }];
        });
        [request_blocks addObject:request_block];
        dispatch_async(dispatch_get_main_queue(), request_block);
    }
    //取消這五個任務
    for (dispatch_block_t request_block in request_blocks) {
        dispatch_block_cancel(request_block);
        dispatch_group_leave(request_blocks_group);
    }
    dispatch_group_notify(request_blocks_group, dispatch_get_main_queue(), ^{
        NSLog(@"網絡任務請求執行完畢或者全部取消");
    });

dispatct_io_t的使用

//dispatch_io_t的使用
    NSString * plist_path = [[NSBundle mainBundle]pathForResource:@"Info" ofType:@".plist"];
//獲取文件dispatch_fd_t 的描述符
    dispatch_fd_t fd = open(plist_path.UTF8String, O_RDONLY);
//創建文件讀取所使用的操作隊列
    dispatch_queue_t queue = dispatch_queue_create("test io", NULL);
//根據描述符創建GCD的讀取流
    dispatch_io_t pipe_channel = dispatch_io_create(DISPATCH_IO_STREAM, fd, queue, ^(int error) {
        close(fd);
    });
//設置讀取大小
    dispatch_io_set_low_water(pipe_channel, SIZE_MAX);
//使用文件流來讀取內容
    dispatch_io_read(pipe_channel, 0, SIZE_MAX, queue, ^(bool done, dispatch_data_t  _Nullable data, int error) {
        if (error==0) {
            
        }
        if (done) {

        }
        size_t len = dispatch_data_get_size(data);
        if (len>0) {
            
        }
    });

GCD當中有關io讀取的API非常豐富,利用unix風格的io函數來進行文件操作,與隊列進行配合,可獲取GCD自動調度資源利用多線程進行io操作的能力,同時前面提到的各種異步阻塞或者同步的模型可以利用,創建一套高效的io操作,滿足大文件讀取的高性能需求。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容