iOS-多線程(三)-GCD函數

多線程(一)-原理
多線程(二)-GCD基礎
多線程(三)-GCD函數
多線程(四)-GCD定時器安全

單次函數dispatch_once

單次函數通常用來建立單例或者是執行只須要執行一次的程序。bash

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    NSLog(@"==只會執行一次的代碼==");
});

void dispatch_once(dispatch_once_t *predicate,
		DISPATCH_NOESCAPE dispatch_block_t block)
複製代碼

dispatch_once會保證block中的程序只執行一次,而且即便在多線程的環境下,dispatch_once也能夠保證線程安全。多線程

迭代函數dispatch_apply

dispatch_apply 函數會按照指定的次數將指任務添加到指定的隊列中進行執行。不管是在串行隊列,仍是併發隊列中,dispatch_apply都會等待所有任務執行完畢。閉包

若是是在串行隊列中使用dispatch_apply,會按順序同步執行,就和普通的for循環相似;若是是在異步隊列中使用,下標可能不是按順序來的。併發

void
dispatch_apply(size_t iterations,
		dispatch_queue_t DISPATCH_APPLY_QUEUE_ARG_NULLABILITY queue,
		DISPATCH_NOESCAPE void (^block)(size_t));
複製代碼
  • iterations:執行迭代的次數
  • queue:執行迭代的隊列,建議使用DISPATCH_APPLY_AUTO,會自動調用合適的線程隊列
  • void (^block)(size_t)):迭代的結果回調

延遲函數dispatch_after

延遲函數的做用是在指定的隊列中,按照給定的時間執行一個操做。app

void dispatch_after(dispatch_time_t when, dispatch_queue_t queue,
dispatch_block_t block);
複製代碼
  • dispatch_time_t when:指定執行任務的時間。
    • 可使用DISPATCH_TIME_NOW,可是不推薦,由於該函數調用了dispatch_async
    • 也可使用dispatch_time或者dispatch_walltime自定義時間:dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC))
    • 不能使用DISPATCH_TIME_FOREVER
  • dispatch_queue_t queue:指定隊列,執行任務的隊列。
  • dispatch_block_t block:要執行的任務,不能傳NULL

調度組函數dispatch_group

經過Dispatch Group,咱們能夠將多個任務放入一個組中,而且可讓他們在同一隊列或不一樣隊列上異步執行,執行完成以後,再執行其餘的依賴於這些任務的操做。異步

相關API:async

  1. 建立調度組
dispatch_group_t dispatch_group_create(void);
複製代碼
  1. 進組,開始執行組內任務
void dispatch_group_enter(dispatch_group_t group);
複製代碼
  1. 出組,組任務執行完成
void dispatch_group_leave(dispatch_group_t group);
複製代碼
  1. 同步等待,阻塞當前線程直到組的任務都執行完成或者timeout歸零纔會繼續下一步
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
複製代碼
  1. 組所關聯的全部任務已經完成,發出一個通知告知
void dispatch_group_notify(dispatch_group_t group,
	                   dispatch_queue_t queue,
	                   dispatch_block_t block);
複製代碼

下面咱們經過一個例子來看一下dispatch_group的使用:函數

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

dispatch_async(queue, ^{
    sleep(2);
    NSLog(@"==1==");
});
    
dispatch_async(queue, ^{
    NSLog(@"==2==");
});

dispatch_async(queue, ^{
    NSLog(@"==3==");
});
    
dispatch_group_notify(group, queue, ^{
    NSLog(@"===4=");
});
複製代碼

運行程序,控制檯輸出:post

能夠看出這並非咱們想要的結果。對程序進行修改,繼續運行:

一樣使用dispatch_group_wait也會獲得相應的結果:

可是dispatch_group_wait會阻塞以後的操做,好比咱們在組通知以後還執行了NSLog(@"==5=="),組任務並無阻塞到它的執行,而dispatch_group_wait就會阻塞。

注意,dispatch_group_enterdispatch_group_leave必須成對出現,不然會形成死鎖。

柵欄函數dispatch_barrier

柵欄函數分爲dispatch_barrier_asyncdispatch_barrier_sync函數,這兩個函數既有共同點,又有不一樣點:

  • 共同點:
  1. 等待在它前面插入隊列的任務先執行完
  2. 等待他們本身的任務執行完再執行後面的任務
  • 不一樣點:
  1. dispatch_barrier_sync將本身的任務插入到隊列的時候,須要等待本身的任務結束以後纔會繼續插入被寫在它後面的任務,而後執行它們
  2. dispatch_barrier_async將本身的任務插入到隊列以後,不會等待本身的任務結束,它會繼續把後面的任務插入到隊列,而後等待本身的任務結束後才執行後面任務。

下面咱們配合一個例子說明一下:

- (void)barrierAsync {
    dispatch_queue_t queue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"--1--");
    });
    dispatch_async(queue, ^{
        NSLog(@"--2--");
    });
    dispatch_barrier_async(queue, ^{
        NSLog(@"--barrier_async--%@--",[NSThread currentThread]);
        sleep(2);
    });
    
    NSLog(@"=======barrierAsync=======");
    dispatch_async(queue, ^{
        NSLog(@"--3--");
    });
    dispatch_async(queue, ^{
        NSLog(@"--4--");
    });
    dispatch_async(queue, ^{
        NSLog(@"--5--");
    });
}
複製代碼

運行程序:

dispatch_barrier_async函數改成dispatch_barrier_sync,而後運行程序:

經過打印結果能夠看出柵欄函數不論是同步異步,都會對當前隊列中的任務起到隔離做用,就是會讓柵欄以前的多線程操做先執行,讓柵欄以後的多線程操做後執行。不一樣的是dispatch_barrier_async函數以後的多線程操做都是併發執行,而dispatch_barrier_sync以後的操做都是同步執行,因此咱們打印的barrierAsync的執行順序和barrierSync不一樣。

簡而言之,dispatch_barrier_syncdispatch_barrier_async都會隔離隊列中柵欄先後的任務,不一樣的是會不會阻塞當前隊列。因此柵欄函數和其攔截的任務必須是同一隊列的,否則沒有阻塞效果。因此在AFN中使用柵欄函數沒有效果,AFN本身維護了一個串行隊裏,除非使用這個隊列纔會起做用。

注意,當咱們在主線程中調用任務,並且將同步柵欄函數也添加到主隊列中,會發生死鎖現象。使用柵欄函數要使用自定義隊列,防止阻塞、死鎖。

信號量dispatch_semaphore_t

一種可用來控制訪問資源的數量的標識,設定了一個信號量,在線程訪問以前,加上信號量的處理,則可告知系統按照咱們指定的信號量數量來執行多個線程。

相關API:

  1. 建立信號量,參數:信號量的初值,若是小於0則會返回NULL,該參數控制當前能開啓的線程數量。
dispatch_semaphore_t dispatch_semaphore_create(long value)
複製代碼
  1. 等待(減小)信號量,信號出現以後纔會返回。
long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
複製代碼
  • dispatch_semaphore_t dsema: 信號量。若是傳入的dsema大於0,就繼續向下執行,並將信號量減1;若是dsema等於0,阻塞當前線程等待資源被dispatch_semaphore_signal釋放。若是等到了信號量,繼續向下執行並將信號量減1,若是一直沒有等到信號量,就等到timeout再繼續執行。

  • dispatch_time_t timeout: 超時,阻塞線程的時長。通常傳DISPATCH_TIME_FOREVER或者DISPATCH_TIME_NOW,也能夠自定義。dispatch_time_t t = dispatch_time(DISPATCH_TIME_NOW, 1*100*100*100);

  • 若是成功則返回0,超時會返回其餘值

  1. 發信號(增長信號量)。若是以前的值小於零,該函數會喚醒等待的線程
long dispatch_semaphore_signal(dispatch_semaphore_t dsema)
複製代碼

減小和增長信號量一般成對使用,使用的順序是先減小信號量(wait)而後再增長信號量(signal)

下面咱們結合一個例子,說明一下信號量的使用:

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
//任務1
dispatch_async(queue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"執行任務1");
    sleep(1);
    NSLog(@"任務1完成");
    dispatch_semaphore_signal(semaphore);
});
    
//任務2
dispatch_async(queue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"執行任務2");
    sleep(1);
    NSLog(@"任務2完成");
    dispatch_semaphore_signal(semaphore);
});
    
//任務3
dispatch_async(queue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"執行任務3");
    sleep(1);
    NSLog(@"任務3完成");
    dispatch_semaphore_signal(semaphore);
});
複製代碼

運行程序,控制檯輸出:

將建立的信號量改成2:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
複製代碼

將建立的信號量改成3,或者大於3:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
複製代碼

同理,咱們還能夠將例子中的併發任務改成同步任務。能夠得出以下結論:

  • 若是是同步任務,無論建立的信號量和任務數的關係,都是按照順序一個接一個執行
  • 若是是異步任務:
    • 建立的信號量小於任務數,就會先按照信號量的數量執行相應的任務,剩下任務會等到以前執行的任務執行完成纔會接着執行
    • 建立的信號量大於等於任務數,全部任務都會併發執行

再來看一個例子:

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

因爲異步線程的問題,咱們打印a的值,多是大於等於5,此時依靠信號量就能夠控制讓循環外輸出a=5。以下:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
__block 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(@"==a==%d==", a);
複製代碼

關於信號量的時候,咱們須要注意的是防止線程被阻塞,當執行dispatch_semaphore_wait方法的時候必定要保證傳入的信號量大於0。

調度源函數dispatch_source

當有一些特定的較底層的系統事件發生時,調度源會捕捉到這些事件,而後能夠作其餘的邏輯處理,調度源有多種類型,分別監聽對應類型的系統事件。也就是用GCD的函數指定一個但願監聽的系統事件類型,再指定一個捕獲到事件後進行邏輯處理的閉包或者函數做爲回調函數,而後再指定一個該回調函數執行的隊列便可,當監聽到指定的系統事件發生時會調用回調函數,將該回調函數做爲一個任務放入指定的隊列中執行。

相關的API

  1. 建立源
dispatch_source_t
dispatch_source_create(dispatch_source_type_t type,
	uintptr_t handle,
	unsigned long mask,
	dispatch_queue_t _Nullable queue);
複製代碼
  1. 設置源事件回調
void
dispatch_source_set_event_handler(dispatch_source_t source,
	dispatch_block_t _Nullable handler);
複製代碼
  1. 設置源事件數據
void
dispatch_source_merge_data(dispatch_source_t source, unsigned long value);
複製代碼
  1. 獲取源事件數據
unsigned long
dispatch_source_get_data(dispatch_source_t source);
複製代碼

獲取的數據類型和源事件的類型相關:

  • 讀文件類型的dispatch_source,返回的是讀到文件內容的字節數。
  • 寫文件類型的dispatch_source,返回的是文件是否可寫的標識符,正數表示可寫,負數表示不可寫。
  • 監聽文件屬性更改類型的dispatch_source,返回的是監聽到的有更改的文件屬性,用常量表示,好比DISPATCH_VNODE_RENAME等。
  • 進程類型的dispatch_source,返回監聽到的進程狀態,用常量表示,好比DISPATCH_PROC_EXIT等。
  • Mach端口類型的dispatch_source,返回Mach端口的狀態,用常量表示,好比DISPATCH_MACH_SEND_DEAD等。
  • 自定義事件類型的dispatch_source,返回使用dispatch_source_merge_data函數設置的數據。
  1. 繼續監聽
void
dispatch_resume(dispatch_object_t object);
複製代碼
  1. 掛起監聽操做
void
dispatch_suspend(dispatch_object_t object);
複製代碼
  • dispatch_source_type_t type:設置dispatch_source方法的類型
  • uintptr_t handle:取決於要監聽的事件類型,好比若是是監聽Mach端口相關的事件,那麼該參數就是mach_port_t類型的Mach端口號,若是是監聽事件變量數據類型的事件那麼該參數就不須要,設置爲0就能夠了。
  • unsigned long mask:取決於要監聽的事件類型
  • dispatch_queue_t _Nullable queue:執行的隊列,默認爲全局隊列

dispatch_source_type_t的取值以下:

  • DISPATCH_SOURCE_TYPE_DATA_ADD:屬於自定義事件,能夠經過dispatch_source_get_data函數獲取事件變量數據,在咱們自定義的方法中能夠調用dispatch_source_merge_data函數向dispatch_source設置數據。
  • DISPATCH_SOURCE_TYPE_DATA_OR:屬於自定義事件,用法同DISPATCH_SOURCE_TYPE_DATA_ADD
  • DISPATCH_SOURCE_TYPE_MACH_SENDMach端口發送事件。
  • DISPATCH_SOURCE_TYPE_MACH_RECVMach端口接收事件。
  • DISPATCH_SOURCE_TYPE_PROC:與進程相關的事件。
  • DISPATCH_SOURCE_TYPE_READ:讀文件事件。
  • DISPATCH_SOURCE_TYPE_WRITE:寫文件事件。
  • DISPATCH_SOURCE_TYPE_VNODE:文件屬性更改事件。
  • DISPATCH_SOURCE_TYPE_SIGNAL:接收信號事件。
  • DISPATCH_SOURCE_TYPE_TIMER:定時器事件。
  • DISPATCH_SOURCE_TYPE_MEMORYPRESSURE:內存壓力事件。

下面咱們結合一個例子,具體的說明一下使用:

@property (nonatomic, strong) dispatch_source_t source;
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, assign) NSUInteger totalComplete;

- (void)initSource {
    self.queue = dispatch_queue_create("soureQueue", 0);
    // 建立soure事件
    self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
    
    // 監聽soure事件發生變化
    dispatch_source_set_event_handler(self.source, ^{
        // 獲取source事件的值
        NSUInteger value = dispatch_source_get_data(self.source); 
        self.totalComplete += value;
        NSLog(@"進度:%.2f", self.totalComplete/100.0);
    });
    // 啓動監聽
    dispatch_resume(self.source);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    for (NSUInteger index = 0; index < 100; index++) {
        dispatch_async(self.queue, ^{
            sleep(1);
            // 設置source事件的數據
            dispatch_source_merge_data(self.source, 1); 
        });
    }
}
複製代碼

運行程序:

總結

  1. dispatch_once
    • 會執行一次
    • 線程安全
  2. dispatch_after是異步執行的
  3. dispatch_apply
    • 串行隊列和普通循環相同
    • 併發隊列,循環的下標不是按順序來的
  4. dispatch_group
    • dispatch_group_enterdispatch_group_leave必須成對出現,不然會形成死鎖
    • 先進後出,先enterleave
    • dispatch_group_wait會阻塞當前線程
  5. dispatch_barrier
    • 有同步的效果
    • 性能安全
    • 根本原理是堵塞隊列
    • 不要使用全局隊列和主隊列
    • 攔截任務和柵欄函數須要是同一隊列
  6. dispatch_semaphore
    • 起到鎖的做用
    • 是性能最高的鎖
    • 可以控制最大併發數
    • dispatch_semaphore_wait的參數爲0的時候會堵塞線程
  7. dispatch_source
    • 建立、監聽回調、設置改變,造成了dispatch_source的基本操做
    • 設置、接收數據的時候須要注意source的類型

參考資料:
官方文檔
iOS 多線程:『GCD』詳盡總結

相關文章
相關標籤/搜索