iOS底層學習 - 多線程之GCD應用篇

通過前兩章的學習,咱們對多線程和GCD的使用已經有了瞭解,這章節就來探討一些GCD在開發中一些經常使用的GCD函數。html

系列文章傳送門:git

iOS底層學習 - 多線程之基礎原理篇github

iOS底層學習 - 多線程之GCD初探面試

iOS底層學習 - 多線程之GCD隊列原理篇安全

咱們知道GCD除了基本的dispatch_syncdispatch_async用法外,還有一些其餘的用法,好比信號量,調度組,延時執行等等。咱們來看一下這個使用是怎麼應用到咱們日常的多線程開發當中的。markdown

信號量dispatch_semaphore

入門小題

首先咱們來看一道經典面試題,問題是下面a會輸出什麼數值多線程

int a = 0;
while (a < 5) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        a++;
    });
}
NSLog(%d,a)
複製代碼

下面咱們來分析一下,咱們發如今主隊列中有while任務NSLog任務,因此串行執行,必然是執行完while纔會進行打印,若是沒有裏面dispatch_async任務,那麼a必然是5。閉包

可是咱們知道dispatch_async是異步執行的併發隊列,因此會開闢線程在進行a++操做,且這些線程操做的都是a的同一片內存空間,也就表示當一個線程執行完畢後,此時只在執行的線程上的a值都會變化,因此也就會存在返回的慢,而致使a大於5的狀況。併發

因此答案爲app

a => 5

這樣的寫法必然是浪費線程資源的,很是不合理,一般咱們進行I/O操做時,都是須要加鎖來保證線程安全的,這樣數據纔不會出錯。

下面咱們就用信號量的方法來簡單處理一下。

信號量的使用

信號量的使用主要有3個方法來搭配。

  1. dispatch_semaphore_create(value):此方法是用來建立信號量,一般加鎖的操做時,此時入參0
  2. dispatch_semaphore_wait(): 此方法是等待信號量,會對信號量減1(value - 1),當信號量 < 0時,會阻塞當前線程,等待信號(signal),當信號量 >= 0時,會執行wait後面的代碼。
  3. dispatch_semaphore_signal(): 此方法是信號量加1,當信號量 >= 0 會執行wait以後的代碼。

注意事項以下

  1. 這3個方法必須搭配使用,缺一不可
  2. dispatch_semaphore_wait()dispatch_semaphore_signal()是成對使用的

瞭解了信號量的使用時候,咱們就能夠很好的解決上面的問題了。咱們只須要在要加鎖的地方,使用dispatch_semaphore_signal()將信號量+1,等待其執行完a++,dispatch_async執行完畢後,在使用dispatch_semaphore_wait()將信號量-1,此時信號量爲0,跳出這次阻塞。具體代碼以下

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
int a = 0;
while (a < 5) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        a++;
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
NSLog(%d,a)
複製代碼

此時的打印結果:

5

柵欄函數dispatch_barrier

柵欄概念

當咱們有兩組異步任務須要有前後順序的執行的時候,使用柵欄函數能夠很好的解決這個問題。雖然這個函數在平常的開發中使用的不是不少,可是這也是一種比較簡單直觀的解決此類問題的好辦法。

柵欄函數有兩個API:(dispatch_barrier_asyncdispatch_barrier_sync)

這兩個API都會等柵欄前追加到隊列中的任務執行完畢後,再將柵欄後的任務追加到隊列中,而後等到dispatch_barrier_asyncdispatch_barrier_sync方法前的任務執行完畢後纔會去執行後邊追加到隊列中的任務,簡單來講dispatch_barrier_asyncdispatch_barrier_sync將異步任務分紅了兩個組,執行完第一組後,再執行本身,而後執行隊列中剩餘的任務。惟一不一樣的是dispatch_barrier_async不會阻塞線程。

柵欄使用

經過例子來看一下柵欄函數的使用。經過打印結果過咱們能夠看到,柵欄函數並無阻塞主線程的調用,可是異步任務2的執行完畢是在異步任務1後面的。說明柵欄並無阻塞線程,而只是阻塞了隊列的執行。

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.wy.barrier1", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"開始");
    dispatch_async(concurrentQueue, ^{
        NSLog(@"任務1-%@",[NSThread currentThread]);
    });
    
    /* 2. 柵欄函數 */
    dispatch_barrier_async(concurrentQueue, ^{
        NSLog(@"---------------------%@------------------------",[NSThread currentThread]);
    });
    NSLog(@"停止");
    
    dispatch_async(concurrentQueue, ^{
        NSLog(@"任務2-%@",[NSThread currentThread]);
    });
    
    NSLog(@"結束");
複製代碼

打印結果:

若是將例子中的dispatch_barrier_async換成dispatch_barrier_sync。經過下面的打印結果過能夠看出,dispatch_barrier_sync不只阻塞了線程的執行,也阻塞了隊列的執行。整個任務都按照順序來執行了。可是阻塞主線程的操做仍是儘可能來避免。

注意事項以下

  1. 柵欄函數最直接的做用: 控制任務執行順序,同步
  2. dispatch_barrier_async 前面的任務執行完畢纔會來到這裏
  3. dispatch_barrier_sync 做用相同,可是這個會堵塞線程,影響後面的任務執行
  4. 柵欄函數只能控制同一併發隊列

調度組dispatch_group

對於調度組dispatch_group的使用,在平常的開發中時很是多的,主要也是爲了解決多線程中多個異步執行之間,順序執行的問題。

調度組的使用

調度組使用主要有一下幾個函數:

  • dispatch_group_create:用來建立一個調度組
  • dispatch_group_async:先把任務添加到隊列中,而後將隊列方到調度組中
  • dispatch_group_enterdispatch_group_leave:設置進組和出組,必須成對使用,和dispatch_group_async做用相同
  • dispatch_group_wait: 進組任務執行等待時間
  • dispatch_group_notify:執行調度組結束後接下來的任務

dispatch_group_async使用

具體使用的代碼以下。能夠發現,不管任務一和任務兒耗時多少,都會在所有執行結束後,調用dispatch_group_notify方法,咱們通常在此方法中獲取到dispatch_get_main_queue主線程,用來刷新UI等操做。

//建立調度組
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t queue1 = dispatch_queue_create("com.wy.group", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"任務一");
    });
    
    dispatch_group_async(group, queue1, ^{
        sleep(2);
        NSLog(@"任務二");
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"任務執行完成");
    });
    
複製代碼

打印結果:

進出組方式使用

使用進出組的方式和dispatch_group_async效果相同,只不過是有了進出組的代碼,邏輯更加清晰明瞭。並且dispatch_group_enterdispatch_group_leave成對使用的,必須先進組再出組,缺一不可

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"任務一");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"任務二");
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"全部任務完成,能夠更新UI");
    });
複製代碼

打印結果:

dispatch_group_wait使用

對於dispatch_group_wait的使用,在平時的開發中可能使用較少,可是它很是的好用。

dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout)須要傳入兩個參數,一個參數是調度組,另外一個參數是指定等待的時間。

那麼這個指定等待時間是什麼意思呢?

這裏的等待表示,一旦調用dispatch_group_wait函數,該函數就處理調用的狀態而不返回值,只有當函數的currentThread中止,或到達wait函數指定的等待的時間,或Dispatch Group中的操做所有執行完畢以前,執行該函數的線程中止.

  • 當指定timeoutDISPATCH_TIME_FOREVER時就意味着永久等待
  • 當指定timeoutDISPATCH_TIME_NOW時就意味不用任何等待便可斷定屬於Dispatch Group的處理是否所有執行結束
  • 若是dispatch_group_wait函數返回值不爲0,就意味着雖然通過了指定的時間,但Dispatch Group中的操做並未所有執行完畢
  • 若是dispatch_group_wait函數返回值爲0,就意味着Dispatch Group中的操做所有執行完畢

這裏的應用場景能夠在任務一和任務二兩個異步任務,也要有前後的執行順序時,經過wait來阻塞當前線程,只有當執行完一組任務或者超過超時時間後才能夠繼續向下進行。

經過一個例子來看一下。經過打印結果能夠發現,當執行的之間爲DISPATCH_TIME_FOREVER或者未超時時,是先執行了任務一後,才執行的任務二,而後回到主線程的。

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_enter(group);
    
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"任務一");
        dispatch_group_leave(group);
    });
    
// long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW,3 * NSEC_PER_SEC));
    long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_FOREVER, 0));
    if (timeout == 0) {
        NSLog(@"回來了");
    }else{
        NSLog(@"等待中 -- 轉菊花");
    }
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"任務二");
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"全部任務完成,能夠更新UI");
    });
複製代碼

打印結果:

若是咱們把超時時間設置的短一點,好比1秒,long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW,1 * NSEC_PER_SEC));能夠發現打印結果不一樣,和沒有進行wait時的打印結果是相同的。

注意事項以下

  1. dispatch_group_enterdispatch_group_leave成對使用的,必須先進組再出組,缺一不可
  2. dispatch_group_wait能夠設置等待時間,用來區分異步任務執行
  3. dispatch_group_notify爲組任務所有完成後執行的回調,通常在處理主線程邏輯

延遲函數dispatch_after

對於延遲執行的函數dispatch_after的使用確定是不會陌生的。

可是須要注意的是:dispatch_after方法並非在指定時間以後纔開始執行處理,而是在指定時間以後將任務追加到主隊列中。嚴格來講,這個時間並非絕對準確的,但想要大體延遲執行任務,dispatch_after 方法是頗有效的。

-(void)afterTask{
    NSLog(@"開始");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"執行---%@",[NSThread currentThread]);
    });
}
複製代碼

單次函數dispatch_once

對於單次函數你們也很是熟悉。咱們再建立單例的時候常常會用到

dispatch_once方法能夠保證一段代碼在程序運行過程當中只被調用一次,並且在多線程環境下能夠保證線程安全。

+ (instancetype)shareInstance{
    static WYManager *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [WYManager alloc]init];
    });
    return instance;
}
複製代碼

事件源dispatch_source

概念

dispatch_source是一種基本的數據類型,能夠用來監聽一些底層的系統事件,在平常的開發中,咱們常用它來建立一個GCDTimer。可是它還有不少其餘的監聽類型,經過查看官方文檔咱們可得如下監聽:

  • Timer Dispatch Source:定時調度源。
  • Signal Dispatch Source:監聽UNIX信號調度源,好比監聽表明掛起指令的SIGSTOP信號。
  • Descriptor Dispatch Source:監聽文件相關操做和Socket相關操做的調度源。
  • Process Dispatch Source:監聽進程相關狀態的調度源。
  • Mach port Dispatch Source:監聽Mach相關事件的調度源。
  • Custom Dispatch Source:監聽自定義事件的調度源

dispatch_source使用

使用dispatch_source時,一般是先指定一個但願監聽的系統事件類型,再指定一個捕獲到事件後進行邏輯處理的閉包或者函數做爲回調函數,而後再指定一個該回調函數執行的dispatch_queue便可。當監聽到指定的系統事件發生時,Dispatch Source會將已指定的回調函數做爲一個任務放入指定的隊列中執行,也就是說當監聽到系統事件後就會觸發一個任務,並自動將其加入隊列執行。

這裏與一般的手動添加任務的模式不一樣,一旦dispatch_sourcedispatch_queue關聯後,只要監聽到系統事件,dispatch_source就會自動將任務(回調函數)添加到關聯的隊列中,直到咱們調用函數取消監聽。

爲了保證監聽到事件後回調函數可以都到執行,已關聯的dispatch_queue會被dispatch_source強引用。

有些時候回調函數執行的時間較長,在這段時間內Dispatch Source又監聽到多個系統事件,理論上就會造成事件積壓,但好在Dispatch Source有很好的機制解決這個問題,當有多個事件積壓時會根據事件類型,將它們進行關聯和結合,造成一個新的事件。

主要使用的API以下:

  • dispatch_source_create: 建立事件源
  • dispatch_source_set_event_handler: 設置數據源回調
  • dispatch_source_merge_data: 設置事件源數據
  • dispatch_source_get_data: 獲取事件源數據
  • dispatch_resume: 繼續
  • dispatch_suspend: 掛起
  • dispatch_cancle: 取消

自定義GCDTimer

下面咱們經過代碼,使用dispatch_source來簡單實現一個GCDTimer來加深理解。

首先咱們建立一個類WYGCDTimer,用來處理定時器的邏輯,相關代碼在此

typedef void(^WYGCDTimerBlock)(void);
@interface WYGCDTimer : NSObject

/// 初始化Timer
/// @param interval 時間間隔
/// @param repeat 重複
/// @param completion 回調
+ (WYGCDTimer *)timerWithInterval:(NSTimeInterval)interval
                           repeat:(BOOL)repeat
                       completion:(WYGCDTimerBlock)completion;

/// 開始
- (void)startTimer;

/// 結束
- (void)invalidateTimer;

/// 暫停
- (void)pauseTimer;

/// 恢復
- (void)resumeTimer;

@end
複製代碼
@interface WYGCDTimer ()

@property (nonatomic, assign) NSTimeInterval interval;
@property (nonatomic, assign) BOOL repeat;
@property (nonatomic, copy) WYGCDTimerBlock completion;

@property (nonatomic, strong)dispatch_source_t timer;
@property (nonatomic, assign) BOOL isRunning;

@end

@implementation WYGCDTimer

+ (WYGCDTimer *)timerWithInterval:(NSTimeInterval)interval repeat:(BOOL)repeat completion:(WYGCDTimerBlock)completion {
    WYGCDTimer *timer = [[WYGCDTimer alloc] initWithInterval:interval repeat:repeat completion:completion];
    return timer;
}

- (instancetype)initWithInterval:(NSTimeInterval)interval repeat:(BOOL)repeat completion:(WYGCDTimerBlock)completion {
    if (self = [super init]) {
        self.interval = interval;
        self.repeat = repeat;
        self.completion = completion;
        self.isRunning = NO;
        
        ✅// 初始化timer
        self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
        ✅// 設置timer
        dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, self.interval * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
        
        __weak typeof(self) weakSelf = self;
        ✅// 設置回調
        dispatch_source_set_event_handler(self.timer, ^{
            [weakSelf excute];
        });
        
    }
    return self;
}

- (void)excute {
    if (self.completion) {
        self.completion();
    }
}

/// 開始
- (void)startTimer {
    if (self.timer && !self.isRunning) {
        self.isRunning = YES;
        dispatch_resume(self.timer);
    }
}

/// 結束
- (void)invalidateTimer {
    if (_timer) {
        dispatch_source_cancel(_timer);
        self.isRunning = NO;
        _timer = nil;
    }
}

/// 暫停
- (void)pauseTimer {
    if (self.timer && self.isRunning) {
        dispatch_suspend(self.timer);
    }
}

/// 恢復
- (void)resumeTimer {
    if (self.timer && !self.isRunning) {
        dispatch_resume(self.timer);
    }
}

複製代碼

自定義dispatch_source

咱們剛剛使用了dispatch_source系統提供的定時器,下面咱們使用自定義的dispatch_source來實現一個進度條。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    dispatch_queue_t progressQueue = dispatch_queue_create("com.wy.gcdtimer", DISPATCH_QUEUE_CONCURRENT);
    self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, progressQueue);
    
    __weak typeof(self) weakself = self;
    dispatch_source_set_event_handler(self.source, ^{
        NSUInteger progress = dispatch_source_get_data(self.source);
        if (progress >= 100) {
            progress = 100;
            dispatch_source_cancel(weakself.source);
            weakself.source = nil;
        }
        NSLog(@"percent: %@", [NSString stringWithFormat:@"%ld",progress]);
    });
    
    dispatch_resume(self.source);
    
}
複製代碼

咱們使用剛剛建立的timer來循環執行,模擬進度條的進度

_timer = [WYGCDTimer timerWithInterval:1 repeat:YES completion:^{
           static NSUInteger _progress = 0;
            _progress += 10;
            if (_progress > 100) {
                _progress = 100;
                [weakself.timer invalidateTimer];
                weakself.timer = nil;
            }
            if (weakself.source) {
                dispatch_source_merge_data(weakself.source, _progress);
            }
        }];
複製代碼

打印結果:

參考資料

Dispatch Source學習

官方文檔

相關文章
相關標籤/搜索