1. 程式人生 > Android開發 >阿里、位元組:一套高效的iOS面試題(八 - 多執行緒 GCD)

阿里、位元組:一套高效的iOS面試題(八 - 多執行緒 GCD)

多執行緒

擼面試題中,文中內容基本上都是搬運自大佬部落格及自我理解,可能有點亂,不喜勿噴!!!

原文題目來自:阿里、位元組:一套高效的iOS面試題

主要以GCD為主

1、iOS開發中有多少型別的執行緒?分別對比

  1. Pthreads : 跨系統 c 語言多執行緒框架,不推薦。
  2. NSThread : ## 面向物件,需手動管理生命週期。
  3. GCD : Grand Central Dispatch,主打任務與佇列,告訴他要做什麼即可。
  4. NSOperation & NSOperationQueue : GCD 的封裝,面向物件

2、GCD有哪些佇列,預設提供哪些佇列

  1. 主佇列

    dispatch_get_main_queue()

  2. 全域性併發佇列

    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)

    佇列優先順序從高到底為:

    DISPATCH_QUEUE_PRIORITY_HIGH

    DISPATCH_QUEUE_PRIORITY_DEFAULT

    DISPATCH_QUEUE_PRIORITY_LOW

    DISPATCH_QUEUE_PRIORITY_BACKGROUND

  3. 自定義佇列(序列 Serial 與並行 Concurrent)

    dispatch_queue_create("這裡是佇列名字",DISPATCH_QUEUE_SERIAL)

    序列 DISPATCH_QUEUE_SERIAL

    並行 DISPATCH_QUEUE_CONCURRENT

3、GCD有哪些方法api

  • 佇列

dispatch_get_main_queue()

dispatch_get_global_queue()

dispatch_queue_create()

  • 執行

dispatch_async()

dispatch_sync()

dispatch_after()

dispatch_once()

dispatch_apply()

dispatch_barrier_async()

dispatch_barrier_sync()

  • 排程組

dispatch_group_create()

dispatch_group_async()

dispatch_group_enter()

dispatch_group_leave()

dispatch_group_notify()

dispatch_group_wait()

  • 訊號量=

dispatch_semaphore_create()

dispatch_semaphore_wait()

dispatch_semaphore_signal()

  • 排程資源

dispatch_source_create()

dispatch_source_set_timer()

dispatch_source_set_event_handler()

dispatch_resume()

dispatch_suspend()

dispatch_source_cancel()

dispatch_source_testcancel()

dispatch_source_set_cancel_handler()

4、GCD主執行緒 & 主佇列的關係

提交到主佇列的任務在主執行緒執行。

5、如何實現同步,有多少方式就說多少

dispatch_async(在同一個序列佇列,...)

dispatch_sync()

dispatch_barrier_sync()

dispatch_group_create() + dispatch_group_wait()

dispatch_apply(1,...)

[NSOpertaion start]

NSOperationQueue.maxConcurrentOperationCount = 1

鎖 OSSpinLock (不推薦)

os_unfair_lock

pthread_mutex(互斥鎖,遞迴鎖,條件鎖,讀寫鎖)

@synchronied(obj)

NSLock

NSRecursiveLock

NSConditionLock & NSCondition

訊號量 dispatch_semaphore_create() + dispatch_semaphore_wait()

6、dispatch_once實現原理

iOS原始碼解析: dispatch_once是如何實現的?

  1. 讀取 token 值 dispatch_once_t.dgo_once
  2. 若 Block 已完成,return;
  3. 若 Block 沒有完成,嘗試原子性修改 dispatch_once_t.dgo_once 值為 DLOCK_ONCE_UNLOCKED

    3.1 修改成功,執行 Block,原子性修改 dispatch_once_t.dgo_onceDLOCK_ONCE_DONE; 然後喚醒等待的執行緒

    3.2 若失敗,則進入迴圈等待

7、什麼情況下會死鎖

  • 單個執行緒

對正在執行任務的序列佇列新增同步任務

/// 在主執行緒中執行這句程式碼
dispatch_sync(dispatch_get_main_queue(),^{
    NSLog(@"這裡死鎖了");
});


/// 在哪裡執行都可以
dispatch_queue_t theSerialQueue = dispatch_queue_create("我是個序列佇列",DISPATCH_QUEUE_SERIAL);
dispatch_sync(theSerialQueue,^{
    NSLog(@"第一層");
    
    /// 同一個序列佇列
    dispatch_sync(theSerialQueue,^{
        NSLog(@"第二層");
        
    });
});
複製程式碼
  • 多個執行緒

簡單來說, A 等 B,同時 B 等 A。

某個任務需要多個資源,比如資源 1、資源 2。此時,執行緒 A 與執行緒 B 都要執行這個任務。但是,執行緒 A 先搶佔了資源 1,同時執行緒 B 搶佔了資源 2。這個時候,執行緒 A 還需要資源 2 才能執行任務;同樣的,執行緒 B 還需要資源 1 才能執行任務。於是,A 等 B,B 等 A,死鎖來了。

- (void)deadLock {
    
    
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(deadLock1) object:nil];
    [thread1 setName:@"【執行緒 湯姆】"];
    
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(deadLock2) object:nil];
    [thread2 setName:@"【執行緒 傑瑞】"];
    
    [thread1 start];
    [thread2 start];
}

- (void)deadLock1 {
    
    [self.lock1 lock];
    NSLog(@"%@ 鎖住 lock1",[NSThread currentThread]);
    
    // 執行緒休眠一秒
    [NSThread sleepForTimeInterval:1];
    
    [self.lock2 lock];
    NSLog(@"%@ 鎖住 lock2",[NSThread currentThread]);

    
    [self doSomething];
    
    
    [self.lock2 unlock];
    NSLog(@"%@ 解鎖 lock2",[NSThread currentThread]);
    
    [self.lock1 unlock];
    NSLog(@"%@ 解鎖 lock1",[NSThread currentThread]);
}


- (void)deadLock2 {
    
    [self.lock2 lock];
    NSLog(@"%@ 鎖住 lock2",[NSThread currentThread]);
    
    // 執行緒休眠一秒
    [NSThread sleepForTimeInterval:1];
    
    [self.lock1 lock];
    NSLog(@"%@ 鎖住 lock1",[NSThread currentThread]);
    
    
    [self doSomething];
    
    
    [self.lock1 unlock];
    NSLog(@"%@ 解鎖 lock1",[NSThread currentThread]);
    
    [self.lock2 unlock];
    NSLog(@"%@ 解鎖 lock2",[NSThread currentThread]);
}

複製程式碼

8、有哪些型別的執行緒鎖,分別介紹下作用和使用場景

種類 備註
OSSpinLock 自旋鎖 不安全,iOS 10 已啟用
os_unfair_lock 互斥鎖 替代 OSSpinLock
pthread_mutex 互斥鎖 PTHREAD_MUTEX_NORMAL#import <pthread.h>
pthread_mutex (recursive) 遞迴鎖 PTHREAD_MUTEX_RECURSIVE#import <pthread.h>
pthread_mutex (cond) 條件鎖 pthread_cond_t#import <pthread.h>
pthread_rwlock 讀寫鎖 讀操作重入,寫操作互斥
@synchronized 互斥鎖 效能差,且無法鎖住記憶體地址更改的物件
NSLock 互斥鎖 封裝 pthread_mutex
NSRecursiveLock 遞迴鎖 封裝 pthread_mutex (recursive)
NSCondition 條件鎖 封裝 pthread_mutex (cond)
NSConditionLock 條件鎖 可以指定具體條件值

9、NSOperationQueue中的maxConcurrentOperationCount預設值

-1。這個值使系統根據系統條件而設定最大值

10、NSTimer、CADisplayLink、dispatch_source_t 的優劣

優點 缺點
NSTimer 使用簡單 依賴 Runloop,具體表現在 無 Runloop 無法使用、NSRunLoopCommonModes、不精確 加入到 主執行緒
CADisplaylink 依賴螢幕重新整理頻率出發事件,最精確。最合適做 UI 重新整理。 若螢幕重新整理被影響,事件也被影響、事件觸發的時間間隔只能是螢幕重新整理 duration 的倍數、若事件所需時間大於觸發事件,跳過數次、不能被繼承
dispatch_source_t 不依賴 Runloop 並不精確,使用相對麻煩
  • NSTimer

    NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    [timer invalidate];
    複製程式碼
  • CADisplaylink

    CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(takeTimer:)];
    [link addToRunLoop:[NSRunLodop currentRunLoop] forMode:NSRunLoopCommonModes];
    link.paused = !link.paused;
    [link invalidate];
    複製程式碼
  • dispatch_source_t : 具體檢視 2.10 dispatch_source

    __block int countDown = 6;
    
    /// 建立 計時器型別 的 Dispatch Source
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,dispatch_get_main_queue());
    /// 配置這個timer
    dispatch_source_set_timer(timer,DISPATCH_TIME_NOW,1 * NSEC_PER_SEC,0);
    /// 設定 timer 的事件處理
    dispatch_source_set_event_handler(timer,^{
        //定時器觸發時執行
        if (countDown <= 0) {
            dispatch_source_cancel(timer);
            
            NSLog(@"倒計時 結束 ~~~");
        }
        else {
            NSLog(@"倒計時還剩 %d 秒...",countDown);
        }
        
        countDown--;
    });
    
    /// 啟動 timer
    dispatch_resume(timer);
    複製程式碼

搞事情~~~

一、 NSThread

NSThread 是蘋果封裝過的,面向物件。可以使用它直接操作執行緒,但需要開發者手動管理其生命週期。

但是相比於 GCD 與 NSOperation / NSOperationQueue 來說更加輕量。

1 建立 NSThread

在 iOS 10之前:

// 建立 NSThread
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething) object:nil];
// 啟動 thread
[thread start];

// 建立並啟動執行緒 - ;類方法
[NSThread detachNewThreadSelector:@selector(doSomething) toTarget:self withObject:nil];
複製程式碼

在 iOS 10之後,蘋果貼心地為我們準備了 Block 的回撥方式:

- (instancetype)initWithBlock:(void (^)(void))block;

+ (void)detachNewThreadWithBlock:(void (^)(void))block;
複製程式碼

除了顯示建立執行緒例項之外,Apple 還為我們提供多種 NSObject 的分類方法來使用,具體函式詳見 NSObject - Objective-C Runtime 分類 Sending Messages

2 NSThread 常用方法

NSThread 的方法雖說不多,但其實也不少

  • 常用的類方法與類屬性:
// 獲取當前執行緒,只讀類屬性
@property (class,readonly,strong) NSThread *currentThread;

// 若 number 為 1,則證明為主執行緒
// <NSThread: 0x281dd6100>{number = 1,name = main}


// 獲取主執行緒,只讀類屬性
@property (class,strong) NSThread *mainThread;

// 判斷當前執行緒是否是主執行緒,只讀類屬性
@property (class,readonly) BOOL isMainThread;


// 休眠一定時間
+ (void)sleepUntilDate:(NSDate *)date;

// 休眠到特定時間
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;


// 退出當前執行緒
+ (void)exit;
複製程式碼
  • 常用的例項方法與例項屬性:
// 執行緒名
@property (nullable,copy) NSString *name;

// 是否正在執行任務
@property (readonly,getter=isExecuting) BOOL executing;
// 是否已執行結束
@property (readonly,getter=isFinished) BOOL finished;
// 是否已被取消(一旦被取消,則該執行緒應 exit)
@property (readonly,getter=isCancelled) BOOL cancelled;

// 是否為主執行緒
@property (readonly) BOOL isMainThread;

// 取消執行緒(一旦取消,則該執行緒應 exit)
- (void)cancel;

// 啟動執行緒
- (void)start;
複製程式碼

二、GCD

通常情況,系統都會允許應用提交非同步請求,然後系統處理請求的過程中,應用可以繼續處理自己的事情。

GCD 便是基於這個準則而設計。

Dispatch - Apple 中這樣介紹 GCD:

Execute code concurrently on multicore hardware by submitting work to dispatch queues managed by the system.

通過向系統管理的 dispatch queues 提交工作來在多核硬體上併發執行程式碼。

GCD,是 iOS 中多執行緒程式設計使用最多也是最方便的解決方案。

使用 GCD 有如下好處:

  • GCD 會自動使用更多的 CPU 核心;
  • GCD 自動管理執行緒的生命週期;
  • GCD 能通過延遲昂貴計算任務並在後臺執行來改善應用的相應效能;
  • GCD 提供了一個易於使用的併發模型(不僅僅是執行緒與鎖);
  • 開發者只需要告訴 GCD 該幹什麼,無需多餘的執行緒管理程式碼;

1、任務與佇列

GCD 中有兩個重要概念:任務佇列

1.1 任務

執行的操作,也就是使用 GCD 時 Block 中需要執行的那段程式碼。

我個人理解,任何一句程式碼都是一個任務。比如 int a = 1 或者 NSLog(@"log");

執行任務有兩種方式:同步執行(dispatch_sync非同步執行(dispatch_async。兩者區別在於是否會阻塞當前執行緒以及是否具有開啟新執行緒的能力。

  • 同步執行:阻塞當前執行緒並等待 Block 中的任務執行完成,然後當前執行緒才會繼續往後執行。不具備開啟新執行緒的能力。

  • 非同步執行:不阻塞當前執行緒,當前執行緒直接往後執行。具備開啟新執行緒的能力,但不一定會開啟新執行緒。

1.2 佇列

存放任務的佇列。佇列是一種特殊的線性表,採用先進先出(FIFO)的規則。

也就是說,新加入的任務總是被插入到佇列的末尾,但執行任務是從佇列頭開始的。這就跟日常生活中的排隊一樣。

佇列分為 序列佇列(Serial Dispatch Queue)並行佇列(Concurrent Dispatch Queue)

  • 序列佇列中的任務按照 FIFO 的順序取出並執行,前一個任務執行完才會取出下一個。

  • 並行佇列中的任務也是按照 FIFO 的順序取出,但是 GCD 會開啟新的執行緒來執行取出的任務。

    這個取出任務並開啟新執行緒執行的動作非常快,所以看起來就像是任務一起執行的。

    但是,如果佇列中的任務數量過大,GCD 也不可能開啟一萬條執行緒同時執行任務的。

    同時,並對佇列的併發功能只在 非同步執行 時有效。

序列佇列與並行佇列的區別可以使用 這篇部落格 的兩張圖來說明:

GCD 公開有五中不同的佇列:主執行緒的 main queue,3個不同優先順序的後臺佇列,一個優先順序更低的後臺佇列(用於 I/O)

同時,使用者還可以建立自定義佇列,序列佇列或並行佇列都可以。在自定義佇列中被排程的所有 Block 最終都將放入到系統的全域性佇列和執行緒池中。

複製一張 大佬的圖

同步執行 非同步執行
序列佇列 當前執行緒,一個一個執行 其他執行緒,一個一個執行
並行佇列 當前執行緒,一個一個執行 開很多執行緒,同時執行

2、使用 GCD

都說 GCD 簡單易用,那就來用一下:

  1. 先建立一個佇列(或者獲取系統的全域性佇列);
  2. 將任務追加到佇列中。

完了,然後系統就會根據任務型別和佇列來執行任務(到底是同步執行,還是非同步執行,在那個佇列執行)。

2.1 建立佇列

主佇列(Main Dispatch Queue)

主佇列,一個特殊的 序列佇列。所有放到主佇列的任務都會放到主執行緒執行。主要用於重新整理 UI,當然,你也可以把任何操作都放到主佇列中。

原則上來說任何重新整理 UI 的操作都應該放到主佇列執行,而耗時操作儘量放到其他執行緒執行。

主佇列無法建立,只能獲取。

/// 獲取主佇列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
複製程式碼

全域性佇列(Global Dispatch Queue)

全域性佇列,蘋果提供給開發者可以直接使用的全域性併發佇列。

一些與 UI 無關的操作應該放到全域性佇列來執行,而不是主佇列。比如網路請求這類操作。

通過 GCD 提供的 dispatch_get_global_queue 方法獲取全域性佇列:

/*
 * @function: dispatch_get_global_queue
 * @abstract: 獲取全域性佇列
 * @para identifier
        佇列優先順序,一般使用 DISPATCH_QUEUE_PRIORITY_DEFAULT。
 * @para flags
        保留引數,傳 0。傳遞除零以外的任何值都可能導致返回值為 NULL。
 * @result: 返回指定的佇列,若失敗則返回 NULL
 */
dispatch_queue_global_t 
dispatch_get_global_queue(long identifier,unsigned long flags);
複製程式碼

dispatch_get_global_queue 第一個引數 identifier 有如下選擇:


/*
 * - DISPATCH_QUEUE_PRIORITY_HIGH
 * - DISPATCH_QUEUE_PRIORITY_DEFAULT
 * - DISPATCH_QUEUE_PRIORITY_LOW
 * - DISPATCH_QUEUE_PRIORITY_BACKGROUND
 */

/// 派發到此佇列的任務將以最高優先順序執行
/// 此佇列的任務將會被安排到預設優先順序及低優先順序的任務之前執行
#define DISPATCH_QUEUE_PRIORITY_HIGH 2

/// 派發到此佇列的任務將以預設優先順序執行 
/// 此佇列的任務將會被安排在 “所有高優先順序任務全部排程完成之後,低優先順序任務被排程之前” 排程執行
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0

/// 派發到此佇列的任務將以低優先順序執行
/// 此佇列的任務將會被安排在 “所有高優先順序和預設優先順序的任務全度排程完成之後” 排程執行
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)

/// 派發到此佇列的任務將以後臺優先順序執行
/// 此佇列的任務將會被安排在所有高優先順序任務之後,才會被排程執行。系統將在具有後臺狀態的執行緒(setThreadPriority)上執行該佇列上的任務,
/// (磁碟 I/O 收到限制,執行緒的排程優先順序被設定為最低值)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
複製程式碼

獲取 預設優先順序 的全域性佇列

// 獲取 預設優先順序 的全域性佇列
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
複製程式碼

自 iOS 8 開始,蘋果還引入了執行緒的服務質量 qos_class

/*
 * 蘋果建議我們使用服務質量類別的值來標記全域性併發佇列
 *  - QOS_CLASS_USER_INTERACTIVE
 *  - QOS_CLASS_USER_INITIATED
 *  - QOS_CLASS_DEFAULT
 *  - QOS_CLASS_UTILITY
 *  - QOS_CLASS_BACKGROUND
 *
 * 全域性併發佇列仍然可以通過優先順序來識別,它會被對映到以下QOS類:
 *  - DISPATCH_QUEUE_PRIORITY_HIGH:         QOS_CLASS_USER_INITIATED
 *  - DISPATCH_QUEUE_PRIORITY_DEFAULT:      QOS_CLASS_DEFAULT
 *  - DISPATCH_QUEUE_PRIORITY_LOW:          QOS_CLASS_UTILITY
 *  - DISPATCH_QUEUE_PRIORITY_BACKGROUND:   QOS_CLASS_BACKGROUND
 */
 
/*
 * @constant QOS_CLASS_USER_INTERACTIVE
 * @abstract 這個 QOS 類表明該執行緒執行與使用者互動的工作。
 * @discussion 與系統的其他工作相比,這些工作被要求以最高優先順序執行。
 * 指定這個 QOS 類將會請求幾乎所有可以用的系統 CPU 資源和 I/O 頻寬執行,甚至不惜爭奪資源。
 * 這不是一個適合大型任務的節能 QOS 類。這個類應該僅限於與使用者關鍵互動。
 * 例如處理主迴圈的事件,繪圖,動畫等。
 *
 * @constant QOS_CLASS_USER_INITIATED
 * @abstract 這個 QOS 類表明該執行緒執行使用者發起並可能在等待結果的工作。
 * @discussion 這類工作的優先順序低於使用者關鍵互動,但又高於系統上的其他操作。
 * 這不是一個適合大型任務的節能 QOS 類。它的使用應該被限制在極短的時間內,從而使用者不至於在等待期間切換任務。
 * 典型的使用者發起的通過顯示佔位符或模態展示使用者介面來指示進度的工作。
 * 
 *
 * @constant QOS_CLASS_DEFAULT
 * @abstrct 系統在缺少具體 QOS 類資訊的情況下使用的預設 QOS 類。
 * @discussion 這類工作優先順序低於使用者關鍵操作和使用者發起的工作,但高於實用工具和後臺任務。
 * 通過 pthread_create 建立且沒有指定 QOS 類屬性的執行緒將預設為 QOS_CLASS_DEFAULT。
 * 這個 QOS 類並不打算作為工作分類,它應該只作為系統提供給傳播或恢復的 QOS 類的值。
 *
 *
 * @constant QOS_CLASS_UTILITY
 * @abstract 這個 QOS 類表明該執行緒執行的工作可能不由使用者發起,且使用者不期待立即等待結果。
 * @discussion 這類工作優先順序低於使用者關鍵操作和使用者發起的工作,但高於低階別的系統維護工作。
 * 這個 QOS 類指明這類工作應該以節能高效方式執行。
 * 這種實用工具的工作可能不表明給使用者,但是這類工作的影響是使用者可見的。
 *
 *
 * @constant QOS_CLASS_BACKGROUND
 * @abstract 這個 QOS 類表明該執行緒執行的工作不由使用者發起,且使用者可能並不知道結果。
 * @discussion 這類工作優先順序低於其他工作。
 * 這個 QOS 類指明這類工作應該以最節能高效的方式執行。
 *
 *
 * @constant QOS_CLASS_UNSPECIFIED
 * @abstract 這是一個指示 QOS 類資訊缺失或者被移除的標記。
 * @discussion 作為 API 返回值,可能表示執行緒或 pthread 被不相容的遺留 API 配置,或與 QOS 類系統衝突。
 */
__QOS_ENUM(qos_class,unsigned int,QOS_CLASS_USER_INTERACTIVE = 0x21,QOS_CLASS_USER_INITIATED = 0x19,QOS_CLASS_DEFAULT = 0x15,QOS_CLASS_UTILITY = 0x11,QOS_CLASS_BACKGROUND = 0x09,QOS_CLASS_UNSPECIFIED = 0x00,);
複製程式碼

自定義佇列

蘋果允許開發者建立自定義佇列,序列佇列和並行佇列都可以建立。

/*
 * @funcion: dispatch_queue_create
 * @abstract: 建立自定義佇列
 * @para label
        佇列標籤,可以為 NULL。
 * @para attr
        佇列型別,序列佇列還是並行佇列,DISPATCH_QUEUE_SERIAL 與 NULL 表示序列佇列,DISPATCH_QUEUE_CONCURRENT 表示並行佇列。
 * @result: 返回建立好的佇列。
 */
dispatch_queue_t
dispatch_queue_create(const char *_Nullable label,dispatch_queue_attr_t _Nullable attr);
複製程式碼

如註釋所說

  • 第一個引數為佇列標籤,可為 NULL,開發者可是用這個值來方便地 DEBUG。佇列標籤推薦使用應用程式 ID 這種的逆序域名。
  • 第二個引數就比較重要了,它表明開發者需要建立的佇列型別,序列佇列還是並行佇列。

    序列佇列: DISPATCH_QUEUE_SERIALNULL 。 並行佇列: DISPATCH_QUEUE_CONCURRENT

2.2 建立任務

搞了這麼半天其實都是準備工作,只是為了建立一個可以存放任務的容器。只不過這個容器不可或缺。

/*
 * @function: dispatch_sync
 * @abstract: 在當前執行緒同步執行任務,會阻塞當前執行緒直到這個任務完成。
 * @para queue
        佇列。開發者可以指定在哪個佇列執行這個任務
 * @para block
        任務。開發者在這個 Block 內執行具體任務。
 * @result: 無返回值
 */
//void
//dispatch_sync(dispatch_queue_t queue,DISPATCH_NOESCAPE dispatch_block_t block);

dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^{
    NSLog(@"同步執行");
});



/*
 * @function: dispatch_async
 * @abstract: 另開執行緒非同步執行任務,不會阻塞當前執行緒。
 * @para queue
        佇列。開發者可以指定在哪個佇列執行這個任務
 * @para block
        任務。開發者在這個 Block 內執行具體任務。
 * @result: 無返回值
 */
// void
// dispatch_async(dispatch_queue_t queue,DISPATCH_NOESCAPE dispatch_block_t block);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,^{
    NSLog(@"非同步執行");
});
複製程式碼

如註釋所說,第一個引數表明放在哪個佇列執行,第二個表明任務是什麼。

同步與非同步最大的區別就在於是否會阻塞當前執行緒:

  • dispatch_sync 會阻塞當前執行緒。
  • dispatch_async 不會阻塞當前執行緒。

2.3 組合任務與佇列

在當前執行緒為主執行緒的情況下,任務執行方式與佇列種類兩兩組合一下:

  1. 同步執行 + 序列佇列
  2. 同步執行 + 並行佇列
  3. 非同步執行 + 序列佇列
  4. 非同步執行 + 序列佇列

對了,還忘了兩個,主佇列

  1. 同步執行 + 主佇列
  2. 非同步執行 + 主佇列

在當前執行緒為主執行緒的情況下:

序列佇列 並行佇列 主佇列
同步執行 不開啟新執行緒,序列執行任務 不開啟新執行緒,序列執行任務 死鎖
非同步執行 開啟一條新執行緒,序列執行任務 開啟新執行緒(可能會有多條),併發執行任務 不開啟新執行緒,序列執行任務

說人話:

  • 同步執行,在當前執行緒執行指定任務,而且會阻塞當前執行緒的後續任務;

    在同步執行條件下,並行佇列也無法並行,畢竟阻塞了。

    注意:同步執行 + 主佇列 = 死鎖

  • 非同步執行不需要阻塞,開啟新執行緒執行任務,且不阻塞當前執行緒的後續任務。

    在非同步執行條件下,序列佇列與並行佇列都會開啟新執行緒

    只不過序列佇列值開啟一條新執行緒,並行佇列會盡量開啟多的執行緒來分別執行任務(畢竟有上限,不可能同時開啟1000000條)。

  • 主佇列是個序列佇列,且只能選擇非同步執行,畢竟 同步執行 + 主佇列 = 死鎖

驗證一下 同步執行 + 序列佇列 = 死鎖

/**
 * 同步執行 + 主佇列
 */
- (void)syncMain {
    
    NSLog(@"當前執行緒:%@",[NSThread currentThread]);
    NSLog(@"syncMain --- 開始");
    
    dispatch_sync(dispatch_get_main_queue(),^{
        NSLog(@"同步執行 + 主佇列");
    });
    
    NSLog(@"syncMain --- 結束");
}
複製程式碼

同步執行 + 主佇列

程式崩潰了。僅打印出 “當前執行緒資訊” 以及 “syncMain --- 開始”,然後便沒有然後了。

先收集當前執行環境:

  1. 主執行緒執行;
  2. 同步執行;
  3. 主佇列任務、

我在上邊說過,任何一句程式碼都是一個任務。

很明顯,程式崩潰的時候,正在執行 dispatch_sync 任務(稱之為 任務1),而 任務1 的內容是 停止當前工作,立即執行 ^{ NSLog(@"同步執行 + 主佇列"); } (稱之為 任務2)。

dispatch_sync 會阻塞當前執行緒,具體是 【在執行完 Block 之前,dispatch_sync 不會 return】。這就意味著 直到完成 任務2dispatch_sync 才能 return。

說人話,主執行緒一直在執行 任務1,除非 任務2 完成。

但是我們是使用 主佇列 來執行這裡同步操作的,主佇列如果要執行下一個任務,那麼當前任務必須完成。

此時,任務1 等待任務完成,自己才能完成;而 任務2 等待 任務1 完成,自己才能開始執行。

是不是跟死鎖的機制一模一樣:我在等著你,而你也在等著我。

沒錯!!!這裡就是死鎖,不過新版 GCD 加入了死鎖檢測機制,如果發生死鎖,則會引發 crash。

有興趣的朋友可以去 Apple 開原始碼 - libdispatchGCD原始碼吐血分析(2)

事實上,並不是 同步執行 + 主佇列 = 死鎖,而是 在主執行緒環境下 + 同步執行 + 主佇列 = 死鎖

在上升一層, 一個序列佇列的任務正在被執行,若此時給這條序列佇列同步提交任務時,則會引發 crash

證明一下:

至於 GCD原始碼吐血分析(2) 裡說的 可以繞開 crash 來引發死鎖,我想我可能做到了。。。(感興趣的朋友可以試試下面這段程式碼)

- (void)theDeadLock {
    
    NSLog(@"當前執行緒:%@",[NSThread currentThread]);
    
    dispatch_queue_t theQueue = dispatch_queue_create("com.junes.serialQueue",DISPATCH_QUEUE_SERIAL);
    
    /// 如果是在 主執行緒中執行,那這裡一定要非同步,否則 第二層直接涼涼
    /// 如果在其他執行緒,那麼這裡同步非同步沒有關係
    dispatch_async(theQueue,^{ /// 第一層
        NSLog(@"第一層 開始 - %@",[NSThread currentThread]);
        
        /// 這裡一定要 同步執行
        /// 如果這裡提前 return了,那麼 theQueue 中將暫時沒有任務
        /// 即可以立即執行 第三層任務,就不符合死鎖條件
        dispatch_sync(dispatch_get_main_queue(),^{     /// 第二層
            NSLog(@"第二層 開始 - %@",[NSThread currentThread]);
            
            dispatch_sync(theQueue,^{      /// 第三層  /// 20200621 - 這一句之前貼上漏了
                NSLog(@"第三層 - 奧利給 %@",[NSThread currentThread]);
            });
            
            NSLog(@"第二層 完成 - %@",[NSThread currentThread]);
        });
        
        NSLog(@"第一層 完成 - %@",[NSThread currentThread]);
    });
    
    
    NSLog(@"最外層 %@",[NSThread currentThread]);
}
複製程式碼

  • 這裡觸發死鎖的原理:執行第三層時,theQueue 必須不能為空。

    第二層不能在 第三層完成之前 return,否在 theQueue 中沒有任務,那完全可以立即執行 第三層的任務。

  • 能避開 crash 的原理:我也不清楚。。。。

    反正把 第二層的主佇列 換成 全域性並行佇列或者自定義序列佇列都會直接引發 crash。。。

    如果有大佬知道原理,求告知。。。拜謝

至於在 佇列中巢狀佇列 ,這裡也給一個表格(【】代表外層操作,並且所有外層操作都能正常執行前提下):

【同步執行 + 序列佇列】巢狀同一個序列佇列 【同步執行 + 並行佇列】巢狀同一個並行佇列 【非同步執行 + 序列佇列】巢狀同一個序列佇列 【非同步執行 + 並行佇列】巢狀同一個並行佇列
同步 死鎖 當前執行緒序列執行 死鎖 當前執行緒序列執行
非同步 另開執行緒(1條)序列執行 另開執行緒並行執行 另開執行緒(1條)序列執行 另開執行緒並行執行

2.4 延遲執行 dispatch_after

需求:延後一段時間再執行任務。

  • dispatch_after 方法可以實現延時執行任務。

其引數為:

  1. when:再過多久將任務提交至佇列;
  2. queue:提交到哪個佇列;
  3. block:提交什麼任務。

dispatch_afterNSTimer 優秀,因為他不需要指定 Runloop 的執行模式。 dispatch_afterNSObject.performSelector:withObject:afterDelay: 優秀,因為它不需要 Runloop 支援。

NSLog(@"開始執行 dispatch_after");
    
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(3 * NSEC_PER_SEC)),dispatch_get_main_queue(),^{
    NSLog(@"三秒後");
});
複製程式碼

但是請注意,dispatch_after 並不是在指定時間後執行任務,而是在指定時間之後才將任務提交到佇列中。

所以,這個延遲的時間是不精確的。這是缺點之一。

第二個缺點便是,dispatch_after 延後執行的 Block 無法直接取消。但是 Dispatch-Cancel 提供了一種解決方案。

其具體實現可以在 Apple 開原始碼 - libdispatchd > Dispatch Source > source.c 中檢視,這裡就不細說了:

判斷 when,如果是現在,非同步執行它;否則就建立一個 dispatch source 以便在指定時間觸發 dispatch_async

2.5 單次執行 dispatch_once

需求:單例模式。

  • dispatch_once 允許開發者線上程安全地執行且只執行一次指定任務。這非常適合單例模式.

其引數為:

  1. predicate:單次執行的標記;
  2. block:需要單次執行的任務。
static TheClass *instance = nil;
+ (instance)sharedInstance 
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken,^{
        instance = [[TheClass alloc] init];
    });
    
    return instance;
}
複製程式碼

GCD 是執行緒安全的。任何試圖訪問臨界區(即傳遞給 dispatch_once 的任務)的執行緒會在臨界區已有一個執行緒的情況下被阻塞,直到臨界區執行緒完成操作。

遺憾的是,Swift 取消了 dispatch_once 這個操作,畢竟在 Swift 中實現單例實在是太簡單了(只需要將初始化方法設定為私有,然後提供一個靜態例項變數即可)。

這裡提供一個 Swift 版的 dispatch_once

// MARK: - DispatchQueue once
extension DispatchQueue {
    
    private static var _onceTracker = [String]()
    
    /**
     Executes a block code,associated with a unique token,only once.
     The clode is thread safe and will only execute the code once even in
     the prescence of multithread calls.
     
     - parameter token: A unique reverse DNS style name suce as com.vectorfrom.<name> or a GUID
     - parameter block: Block to execute once
     **/
    public class func once(token: String,block: () -> Void) {
        objc_sync_enter(self)
        defer {
            objc_sync_exit(self)
        }
        
        guard !_onceTracker.contains(token) else { return }
        _onceTracker.append(token)
        
        block()
    }
}
複製程式碼

2.6 併發迭代 dispatch_apply

需求: 遍歷一個很大很大的集合,使用 for 迴圈將會花費很多很多事件。

  • dispatch_apply 按照指定的次數將指定的任務提交到指定的佇列中,同步執行並等待所有任務完成後 return。

說人話,dispatch_apply 就是一個高階一些的 for 迴圈,它支援併發迭代。並且它 是同步執行的,必須等到所有工作完成才能返回,這與 for 迴圈一樣。

其引數為:

  1. iterations:需要迭代的次數;
  2. queue:將迭代任務提交到哪個佇列;
  3. block:具體的迭代任務是什麼。
dispatch_apply(10,dispatch_get_global_queue(0,^(size_t index) {
    NSLog(@"dispatch_apply --- %zu --- %@",index,[NSThread currentThread]);
});
複製程式碼

上邊的例子並不值得使用 dispatch_apply:建立並執行執行緒是需要付出代價的(時間開銷,記憶體開銷)。

針對簡單的迭代,使用 for 迴圈遠比 dispatch_apply 實惠。如果需要迭代非常大的集合,才應該考慮使用 dispatch_apply

dispatch_apply 在各佇列上的表現(當前為主執行緒):

  • 主佇列:死鎖(畢竟這是同步執行);
  • 序列佇列:序列佇列會完全抵消 dispatch_apply 並行迭代的功能,還不如 for 迴圈;
  • 並行佇列:並行執行迭代任務,這是非常好的選擇,也是 dispatch_apply 的意義所在。

2.7 柵欄方法 dispatch_barrier_async

需求:非同步執行兩組任務,但第二組任務需要第一組完成之後才能執行。

  • dispatch_barrier_async 可以提供一個 “柵欄” 將兩組非同步執行的任務分隔開,保證先於柵欄方法提交到佇列的任務全部執行完成之後,然後開始執行將柵欄任務,等到柵欄任務執行完成後,該佇列便恢復原本執行狀態。

其引數為:

  1. queue:需要隔開的任務所在的佇列;
  2. block:柵欄任務的具體內容。
- (void)barrier_display {
    
    NSLog(@"當前執行緒 -- %@",[NSThread currentThread]);
    
    dispatch_queue_t theConcurrentQueue = dispatch_queue_create("com.junes.serial.queue",DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_queue_t theQueue = theConcurrentQueue;
    
    dispatch_async(theQueue,^{
        NSLog(@"任務1 開始");
        // 模擬耗時任務
        [NSThread sleepForTimeInterval:2];
        
        NSLog(@"任務1 完成");
    });
    
    dispatch_async(theQueue,^{
        NSLog(@"任務2 開始");
        // 模擬耗時任務
        [NSThread sleepForTimeInterval:1];
        
        NSLog(@"任務2 完成");
    });
    
    
    
    dispatch_barrier_async(theQueue,^{
        NSLog(@"==================  柵欄任務 ==================");
    });
    
    
    dispatch_async(theQueue,^{
        NSLog(@"任務3 開始");
        // 模擬耗時任務
        [NSThread sleepForTimeInterval:4];
        
        NSLog(@"任務3 完成");
    });
    dispatch_async(theQueue,^{
        NSLog(@"任務4 開始");
        // 模擬耗時任務
        [NSThread sleepForTimeInterval:3];
        
        NSLog(@"任務4 完成");
    });
}
複製程式碼

看看 dispatch_barrier_asyncdispatch_async

檢視原始碼:

  • dispatch_barrier_async

    void dispatch_barrier_async(dispatch_queue_t dq,dispatch_block_t work)
    {
    	dispatch_continuation_t dc = _dispatch_continuation_alloc();
    	uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_BARRIER;
    	dispatch_qos_t qos;
    
    	qos = _dispatch_continuation_init(dc,dq,work,dc_flags);
    	_dispatch_continuation_async(dq,dc,qos,dc_flags);
    }
    複製程式碼
  • dispatch_async

    void dispatch_async(dispatch_queue_t dq,dispatch_block_t work)
    {
    	dispatch_continuation_t dc = _dispatch_continuation_alloc();
    	uintptr_t dc_flags = DC_FLAG_CONSUME;
    	dispatch_qos_t qos;
    
    	qos = _dispatch_continuation_init(dc,dc->dc_flags);
    }
    複製程式碼

可以發現,dispatch_barrier_asyncdispatch_async 機會一模一樣,唯一的區別就在於

/// 這是 dispatch_barrier_async
uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_BARRIER;

/// 這是 dispatch_async
uintptr_t dc_flags = DC_FLAG_CONSUME;
複製程式碼

兩者唯一的區別在於 建立 dispatch_qos_t qos 傳入的 dc_flags

dispatch_barrier_asyncdispatch_async 多了一個標記 DC_FLAG_BARRIER

而這個標記對全域性併發佇列不起作用。。。。

看看dispatch_barrier_syncdispatch_sync

至於 dispatch_barrier_syncdispatch_sync。檢視原始碼:

  • dispatch_barrier_sync

    void dispatch_barrier_sync(dispatch_queue_t dq,dispatch_block_t work)
    {
    	uintptr_t dc_flags = DC_FLAG_BARRIER | DC_FLAG_BLOCK;
    	if (unlikely(_dispatch_block_has_private_data(work))) {
    		return _dispatch_sync_block_with_privdata(dq,dc_flags);
    	}
    	_dispatch_barrier_sync_f(dq,_dispatch_Block_invoke(work),dc_flags);
    }
    
    
    static void _dispatch_barrier_sync_f(dispatch_queue_t dq,void *ctxt,dispatch_function_t func,uintptr_t dc_flags)
    {
    	_dispatch_barrier_sync_f_inline(dq,ctxt,func,dc_flags);
    }
    
    複製程式碼
  • dispatch_sync

    void dispatch_sync(dispatch_queue_t dq,dispatch_block_t work)
    {
    	uintptr_t dc_flags = DC_FLAG_BLOCK;
    	if (unlikely(_dispatch_block_has_private_data(work))) {
    		return _dispatch_sync_block_with_privdata(dq,dc_flags);
    	}
    	_dispatch_sync_f(dq,dc_flags);
    }
    
    
    static void _dispatch_sync_f(dispatch_queue_t dq,uintptr_t dc_flags)
    {
    	_dispatch_sync_f_inline(dq,dc_flags);
    }
    
    
    static inline void _dispatch_sync_f_inline(dispatch_queue_t dq,uintptr_t dc_flags)
    {
    	if (likely(dq->dq_width == 1)) {
    		/// 序列佇列執行到這裡
    		return _dispatch_barrier_sync_f(dq,dc_flags);
    	}
    
    	if (unlikely(dx_metatype(dq) != _DISPATCH_LANE_TYPE)) {
    		DISPATCH_CLIENT_CRASH(0,"Queue type doesn't support dispatch_sync");
    	}
    
    	dispatch_lane_t dl = upcast(dq)._dl;
    	// Global concurrent queues and queues bound to non-dispatch threads
    	// always fall into the slow case,see DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE
    	/// 通過下面的堆疊,當建立全域性並行佇列的時候,才會執行到此方法
    	if (unlikely(!_dispatch_queue_try_reserve_sync_width(dl))) {
    		return _dispatch_sync_f_slow(dl,dl,dc_flags);
    	}
    
    	if (unlikely(dq->do_targetq->do_targetq)) {
    		return _dispatch_sync_recurse(dl,dc_flags);
    	}
    	_dispatch_introspection_sync_begin(dl);
    	/// 執行 Block
    	_dispatch_sync_invoke_and_complete(dl,func DISPATCH_TRACE_ARG(
    			_dispatch_trace_item_sync_push_pop(dq,dc_flags)));
    }
    複製程式碼

沒看明白???

那就看看堆疊:

  • dispatch_barrier_sync

    dispatch_barrier_sync_dispatch_barrier_sync_f

  • dispatch_sync

    dispatch_sync_dispatch_sync_f_dispatch_sync_f_inline_dispatch_barrier_sync_f

如果是序列佇列,最後呼叫到了同一個方法 _dispatch_barrier_sync_f

柵欄方法在在各佇列上的表現(當前為主執行緒):

主佇列 自定義序列佇列 全域性並行佇列 自定義並行佇列
dispatch_barrier_async 序列佇列毫無意義 序列佇列毫無意義 相當於 dispatch_async,無法達成柵欄目的 在之前和之後的任務之間加一道柵欄,柵欄任務在之前的所有任務完成之後開始執行,完成之後恢復佇列原本的工作狀態
dispatch_barrier_sync 死鎖 序列執行任務

在當前為主執行緒環境下,一個個驗證(序列佇列就沒必要驗證了):

  • dispatch_barrier_async + 自定義並行佇列

    完美符合需求,不是嗎?

    自定義併發佇列dispatch_barrier_async 最佳拍檔。

  • dispatch_barrier_async + 全域性並行佇列

    完全跟 dispatch_async 相同,根本無法達成柵欄目的。

    全域性併發佇列,可能也會被系統系統使用,請不要為了柵欄而壟斷它。

  • dispatch_barrier_sync + 主佇列

    死鎖,上邊的原始碼解讀能找到原因。

  • dispatch_barrier_sync + 自定義序列佇列

    序列佇列有啥好加柵欄的。。。況且 dispatch_barrier_sync 還會阻塞執行緒。

  • dispatch_barrier_sync + 全域性並行佇列

    dispatch_barrier_async + 全域性並行佇列類似,毫無柵欄效果。。。

  • dispatch_barrier_sync + 自定義並行佇列

    也是符合要求的,不是嗎?

結論:自定義並行佇列 是柵欄方法的好幫手

2.8 排程組 dispatch_group_t

需求:分別非同步執行幾個耗時任務,然後當幾個耗時任務都執行完畢後再回到主執行緒執行任務。

初步看到這個任務,剛才的柵欄任務也能做到嘛,有必要花時間瞭解一個新東西嗎?且往下看。。。

  • Dispatch Group 會在整個組的任務全部完成時通知開發者。這些任務可以使同步的,也可以是非同步的,甚至可以再不同佇列。

要監控如此分散的任務的執行情況,這會讓開發者頭疼痛不已。幸好有排程組 dispatch_gourp_t來幫開發者記下這些不同的任務。

  • 建立排程組
/// 建立排程組
dispatch_group_t = dispatch_group_create();
複製程式碼
  • 將任務放進排程組

建立完排程組之後,需要將任務放進排程組中。有兩種方式都可以完成這個工作,但是其側重點不同:

  1. dispatch_group_async

    非同步請求。任務自動完成,其內部程式碼執行完畢即視為任務完成。

    網路請求一般也是非同步請求。所以只要請求傳送完成即視為任務完成,但其實任務並沒有真正完成

    適合內部任務為同步完成的,比如處理一個非常大的集合,或者計算量很大的任務。

  2. dispatch_group_enter

    通知排程組有一個任務開始執行了。任務並不會自動完成,需要我們使用 dispatch_group_leave 來告訴排程組有一個任務完成了。

    適合內部任務為非同步完成的,比如非同步的網路請求、檔案下載。

    但是 dispatch_group_enter 必須與 dispatch_group_leave 成對出現,否則可能會出現崩潰。

先來驗證一下以上說的適不適合的問題,同時也演示一下用法。

- (void)group_validate {
    
    /// 建立一個排程組
    dispatch_group_t group = dispatch_group_create();
    
    
    dispatch_queue_t theGlobalQueue = dispatch_get_global_queue(0,0);
    dispatch_queue_t theSerialQueue = dispatch_queue_create("com.junes.serial.queue",DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t theConcurrentQueue = dispatch_queue_create("com.junes.serial.queue",DISPATCH_QUEUE_CONCURRENT);
    
    
    
    /// 將任務丟進排程組
    dispatch_group_async(group,theGlobalQueue,^{
        NSLog(@"任務1 開始 +++++++");
        
        /// 模擬耗時操作
        sleep(2);
        
        NSLog(@"任務1 完成 -----------------");
    });
    
    dispatch_group_async(group,theSerialQueue,^{
        NSLog(@"任務2 開始 +++++++");
        
        /// 模擬耗時操作
        sleep(4);
        
        NSLog(@"任務2 完成 -----------------");
    });
    
    dispatch_group_async(group,theConcurrentQueue,^{
        NSLog(@"任務3 開始 +++++++");
        
        /// 模擬非同步網路請求
        dispatch_async(dispatch_get_global_queue(0,^{
            sleep(5);
            
            NSLog(@"任務3 現在才真正完成 -----------------");
        });
        
        NSLog(@"任務3 現在被 dispatch_group_notify 已經完成了");
    });
    
    
    dispatch_group_notify(group,^{
        NSLog(@"所有任務都完成了。。。");
    });
    
}
複製程式碼

結果圖的紅框部分可以證明剛才的理論。

接下來,將“非同步的網路操作” 改用 dispatch_group_enter 來放入排程組再看一下。

- (void)group_display {
    
    /// 建立一個排程組
    dispatch_group_t group = dispatch_group_create();
    
    
    dispatch_queue_t theGlobalQueue = dispatch_get_global_queue(0,^{
        NSLog(@"任務2 開始 +++++++");
        
        /// 模擬耗時操作
        sleep(4);
        
        NSLog(@"任務2 完成 -----------------");
    });
    
    
    dispatch_group_enter(group);
    /// 模擬非同步網路請求
    dispatch_async(theConcurrentQueue,^{
        NSLog(@"任務3 開始 +++++++");
        
        sleep(5);
        
        NSLog(@"任務3 完成 -----------------");
        dispatch_group_leave(group);
    });
    
    
    dispatch_group_notify(group,^{
        NSLog(@"所有任務都完成了。。。");
    });
    
    NSLog(@"dispatch_group_notify 為非同步執行,並不會阻塞執行緒。我就是證據");
}
複製程式碼

這才是使用 排程組 的正常操作!

  • 任務完成
  1. dispatch_group_notify

    非同步執行。指定的排程組內任務全部完成之後,將 Block 加入到特定佇列。

    對於 dispatch_group_async 的任務,只要其 Block 程式碼執行完成即認為任務已完成。(無論其 Block 內是否還有非同步請求,這一點在上邊已經驗證過了)

    對於 dispatch_group_enter 的任務,必須使用 dispatch_group_leave 來通知排程組本任務已經完成。

  2. dispatch_group_wait

    同步執行,會阻塞執行緒。

    在所有任務完成(或者超時)之前,該方法會一直阻塞執行緒。

上邊已經演示了 dispatch_group_notify 的使用。接下來看一下 dispatch_group_wait 的用法。

- (void)group_display {
    
    /// 建立一個排程組
    dispatch_group_t group = dispatch_group_create();
    
    
    dispatch_queue_t theGlobalQueue = dispatch_get_global_queue(0,^{
        NSLog(@"任務3 開始 +++++++");
        
        sleep(5);
        
        NSLog(@"任務3 完成 -----------------");
        dispatch_group_leave(group);
    });
    
    NSLog(@"dispatch_group_wait 即將囚禁執行緒");

    /// 傳入指定排程組,與超時時間(DISPATCH_TIME_FOREVER 代表永不超時,DISPATCH_TIME_NOW 代表立馬超時,完全搞不懂這個有什麼用)。
    dispatch_group_wait(group,DISPATCH_TIME_FOREVER);


    NSLog(@"dispatch_group_wait 釋放了執行緒");

    // 排程組內所有任務都完成了,該做什麼就做什麼
    dispatch_async(dispatch_get_main_queue(),^{
        NSLog(@"所有任務完成了");
    });
}
複製程式碼

提醒一下dispatch_group_wait 第二個引數指明瞭何時超時。為了方便,蘋果提供了 DISPATCH_TIME_NOWDISPATCH_TIME_FOREVER 兩個常量。

  1. DISPATCH_TIME_FOREVER

    永不超時,如果任務一直無法完成,那麼執行緒將一直阻塞。

    如果 dispatch_group_leave 數量少於 dispatch_group_enter ,那結果值得期待。

  2. DISPATCH_TIME_NOW

    立馬超時,沒有任何非同步有機會完成。。。

2.9 訊號量 dispatch_semaphore_t

先看一段程式碼:

__block int theNumber = 0;
    
/// 建立排程組
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group,^{
    NSLog(@"任務 1 開始了 %@",[NSThread currentThread]);
    
    for (int i = 0; i < 10000000; ++i) {
        theNumber++;
    }
    
    NSLog(@"任務 1 完成了 %@",[NSThread currentThread]);
});

dispatch_group_async(group,^{
    NSLog(@"任務 2 開始了 %@",[NSThread currentThread]);
    
    for (int i = 0; i < 10000000; ++i) {
        theNumber++;
    }
    
    NSLog(@"任務 2 完成了 %@",[NSThread currentThread]);
});

dispatch_group_notify(group,^{
    NSLog(@"theNumber = %d",theNumber);
});
複製程式碼

GCD 來使用多個執行緒使用同一個資源的例子。最後的執行結果並不是簡單的 迴圈次數 * 2(當然,需要迴圈次數稍微大一點。。。)

多執行緒程式設計時,不可避免地會發生多個執行緒使用同一個資源的情況。如果沒有鎖機制,那麼就失去了程式的正確性。

為了確保 GCD 程式設計的正確性,使用資源時(主要是修改資源)必須加鎖。

訊號量(dispatch_semaphore_t),便是 GCD 的鎖機制。意為持有計數的訊號,蘋果提供了 3 個 API 供開發者使用。

  1. dispatch_semaphore_create

    根據傳入的初始值建立一個訊號量。

    不可傳入負值。執行過程中,若內部值為負數,則這個值的絕對值便是正在等待資源的執行緒數。

  2. dispatch_semaphore_wait

    訊號量 -1。

    -1 之後的結果值小於 0 時,執行緒阻塞,並以 FIFO 的方式等待資源。

  3. dispatch_semaphore_signal

    訊號量 +1。

    +1 之後的結果值大於 0 時,以 FIFO 的方式喚醒等待的執行緒。

給上邊的問題程式碼加上訊號量:

__block int theNumber = 0;
    
/// 建立訊號值為 1 的訊號量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

/// 建立排程組
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group,^{
    /// 訊號值 -1
    dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER);
    
    NSLog(@"任務 1 開始了 %@",[NSThread currentThread]);
    /// 訊號值 +1
    dispatch_semaphore_signal(semaphore);
});

dispatch_group_async(group,^{
    /// 訊號值 -1 (此時訊號量為負數了,執行緒阻塞以等待資源)
    dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER);
    
    NSLog(@"任務 2 開始了 %@",[NSThread currentThread]);
    /// 訊號值 +1
    dispatch_semaphore_signal(semaphore);
});

dispatch_group_notify(group,theNumber);
});
複製程式碼

問題迎刃而解

2.10 排程資源 dispatch_source

dispatch source 是基礎資料型別,協調處理特定的底層系統事件。

dispatch source 有以下特徵。

  1. 配置一個 dispatch source 時,需要指定監測的事件、dispatch quue以及處理事件的 Block。
  2. 當事件發生時,dispatch source 會將指定的 Block 提交到指定的佇列上執行。
  3. 為了防止事件積壓到 dispatch queue,dispatch source 採取了事件合併機制。如果新的是時間在上一個事件處理前到達,新舊事件會被合併。根據事件型別的不同,合併操作可能會替換舊事件,或者更新舊事件的資訊。
  4. dispatch source 提供連續的事件,除非顯示取消,dispatch source 會一直保留與 dispatch queue 的關聯。
  5. dispatch source 非常輕量,CPU負荷非常小,幾乎不佔用資源。它是 BSD 系核心慣有功能 kqueue 的包裝,kqueue 是 XUN 核心中發生各種事件時,在應用程式程式設計執行處理的技術。kqueue 可以稱為應用程式處理 XUN 核心中豐盛各種事件的方法中最優秀的一種。

dispatch_source 的種類

/*
 *當同一時間,一個事件的的觸發頻率很高,那麼Dispatch Source會將這些響應以ADD的方式進行累積,然後等系統空閒時最終處理。
 * 如果觸發頻率比較零散,那麼Dispatch Source會將這些事件分別響應。
 */
DISPATCH_SOURCE_TYPE_DATA_ADD        自定義的事件,變數增加
DISPATCH_SOURCE_TYPE_DATA_OR         自定義的事件,變數OR
DISPATCH_SOURCE_TYPE_DATA_REPLACE    自定義的事件,變數Replace
DISPATCH_SOURCE_TYPE_MACH_SEND       MACH埠傳送    
DISPATCH_SOURCE_TYPE_MACH_RECV       MACH埠接收 
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE  記憶體報警
DISPATCH_SOURCE_TYPE_PROC            程式監聽,如程式的退出、建立一個或更多的子執行緒、程式收到UNIX訊號
DISPATCH_SOURCE_TYPE_READ            IO操作,如對檔案的操作、socket操作的讀響應
DISPATCH_SOURCE_TYPE_SIGNAL          接收到UNIX訊號時響應
DISPATCH_SOURCE_TYPE_TIMER           定時器
DISPATCH_SOURCE_TYPE_VNODE           檔案狀態監聽,檔案被刪除、移動、重新命名
DISPATCH_SOURCE_TYPE_WRITE           IO操作,如對檔案的操作、socket操作的寫響應
DISPATCH_MACH_SEND_DEAD
複製程式碼

使用 dispatch source

所有 dispatch source 種類中,最常用的莫過於 DISPATCH_SOURCE_TYPE_TIMER 了。

/*!
 * @abstract:建立指定的 dispatch source
 * @param type
 * 需要建立的 diapatch source 的種類。必須是其種類常量。
 *
 * @param handle
 * 需要監視的基礎系統控制程式碼。此引數由 type 引數中提供的常量確定。傳 0 即可。
 *
 * @param mask
 * 標誌掩碼,指定需要哪些事件。此引數由 type 引數中提供的常量確定。傳 0 即可。
 *
 * @param queue
 * 在哪個佇列處理事件。
 */
dispatch_source_t
dispatch_source_create(dispatch_source_type_t type,uintptr_t handle,unsigned long mask,dispatch_queue_t _Nullable queue);
複製程式碼

dispatch_source_create 建立的 dispatch source 處於掛起狀態,需要手動喚醒。

配置 dispatch source

/*!
 * @abstract
 * 配置這個計時器型別的 dispatch source
 *
 * @param start
 * 何時開始接收事件。更多資訊檢視  dispatch_time() 和 dispatch_walltime()。
 *
 * @param interval
 * 計時器間隔(納秒級單位)。使用 DISPATCH_TIME_FOREVER 即代表一次性使用。
 *
 * @param leeway
 * 允許的誤差(納秒級單位)。
 */
void
dispatch_source_set_timer(dispatch_source_t source,dispatch_time_t start,uint64_t interval,uint64_t leeway);	


/*!
 * @abstract
 * 為給定 dispatch source 設定事件處理。
 *
 * @param source
 * 需要配置的 dispatch dispatch。
 *
 * @param handler
 * 收到事件時的處理回撥。前者為 Block,後者為函式。
 */
void
dispatch_source_set_event_handler(dispatch_source_t source,dispatch_block_t _Nullable handler);
void
dispatch_source_set_event_handler_f(dispatch_source_t source,dispatch_function_t _Nullable handler);
複製程式碼

dispatch_source_set_event_handler 使用 Block 作為回撥。而 dispatch_source_set_event_handler_f 則使用函式指標作為回撥。

啟動、掛起、取消 dispatch source

/// 喚醒指定 dispatch source
void
dispatch_resume(dispatch_object_t object);

/// 掛起指定 dispatch source。
void
dispatch_suspend(dispatch_object_t object);

/// 取消指定 dispatch source
void
dispatch_source_cancel(dispatch_source_t source);

/// 檢視指定 dispatch source 是否已經取消。已取消返回零,否則非零。
long
dispatch_source_testcancel(dispatch_source_t source);

/// 取消 dispatch source 後最後一次事件的處理。
void
dispatch_source_set_cancel_handler(dispatch_source_t source,dispatch_block_t _Nullable handler);
複製程式碼

關於以上方法,有幾下幾點需要解釋:

  1. 新建立的 dispatch source 處於掛起狀態,必須手動呼叫 dispatch_resume 才能工作;
  2. dispatch source 處於掛起狀態時,發生的所事件都會被累積。 dispatch source 被恢復,但是不會一次性傳遞所有事件,而是先合併到單一事件中;
  3. 取消 dispatch source 是一個非同步操作,呼叫 disaptch_source_cancel 之後,不會再有新的事件被傳遞,但是正在被處理的事件會被繼續處理;
  4. 處理完最後的事件之後, dispatch source 會執行自己的取消處理器(dispatch_source_set_cancel_handler)。在取消處理器中,可以執行記憶體和資源的釋放工作;
  5. 一定要在 dispatch source 正常工作的情況下取消它。在掛起狀態千萬不要呼叫 disaptch_source_cancel 取消 dispatch source

好,上一個完整例項:

__block int countDown = 6;

/// 建立 計時器型別 的 Dispatch Source
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,countDown);
    }
    
    countDown--;
});

/// 啟動 timer
dispatch_resume(timer);
複製程式碼

三、NSOperation 和 NSOperationQueue

NSOperationNSOperationQueue 是基於 GCD 更高一層的封裝,完全面向物件。兩者分別對應 GCD 的任務與佇列。相比 GCD,NSOperationNSOperationQueue 更加簡單易用,程式碼可讀性也更高,但是系統開銷會稍微大一點。

借用 大佬的一張思維導圖 來說明相關的知識點:

1、NSOperation 操作

NSOperation 翻譯過來就是 “操作”,對應 GCD 中的任務。

NSOperation 是個抽象類,本身無法直接使用,不過 Apple 為我們準備了兩個子類:NSInvocationOperationNSBlockOperation。當然,我們也可以自定義子類(AFNetworking 中自定義了 一個子類 AFURLConnectionOperation)。

NSOperation 是一次性的,它的任務只能被執行一次,執行完之後不能再次執行。

NSoperation 有三種重要狀態:

  1. isReady:返回 YES 則代表已準備好被執行,否則說明還有一些準備工作還未完成;
  2. isExecuting:返回 YES 則代表正在被執行;
  3. isFinished:返回 YES 則代表已完成(被取消 isCancelled 也被認為已完成了)。

1.1 啟動 NSOperation 有兩種方式

  1. NSOperation 可以配合 NSOperationQueue使用;

    只需將 NSOperation 新增到 NSOperationQueue

    系統會從 NSOperationQueue 中獲取 NSOperation 然後新增到一個新執行緒中執行,這種方式預設 非同步執行

  2. NSOperation 也可以獨立使用。

    使用 start 方法開啟操作。

    這種方式預設 同步執行

    如果這個 NSOperation 還沒有準備好(isReady 返回 NO),那麼會觸發異常。

推薦 NSOperation 配合 NSOperationQueue 一起使用。

1.2 操作依賴 dependencies

當需要以特定順序執行 NSOperation 時,依賴 是一個方便的選擇。

可以使用 addDependency:removeDepencency 來新增或移除依賴。預設情況下,如果一個 NSOperation 的依賴沒有執行完成,那麼它絕不會準備好;一旦它的最後一個執行完成,這個 NSOperation 就準備好了。

NSOperation 的依賴規則不會區分依賴操作是否真正完成(被取消也被認為完成)。不過,開發者可以決定當依賴操作被取消或未真正完成時是否繼續完成這個 NSOperation

1.3 完成回撥 completionBlock

NSOperation 完成之後,會在執行這個 NSOperation 的執行緒回撥這個 completionBlock

不過,這裡的完成,是真正的完成,cancel 是無法觸發的。

completionBlock 是一個屬性,通過 setter 直接設定即可。

1.4 符合 KVO 的屬性

NSOperation 類的部分屬性是符合 KVC 和 KVO 的。

  • isCancelled

    是否被 cancel。只讀。

  • isAsynchronous

    是否是非同步執行的。只讀。

  • isExecuting

    是否正在執行。只讀。

  • isFinished

    是否已完成。只讀。

  • isReady

    是否已準備好被執行。只讀。

  • dependencies

    所有的依賴項。只讀。

  • queuePriority

    佇列優先順序。可讀可寫。

  • completionBlock

    完成回撥 Block。可讀可寫。

在子類化 NSOperation 時,如果對上述幾個屬性提供了自定義實現,務必實現 KVC 和 KVO。同樣的,要是新增了一些屬性,最好也實現 KVC 與 KVO。

1.5 子類 NSBlockOperation

NSBlockOperation 以 Block 形式儲存任務,使用非常簡單。

  • 建立 NSBlockOperation
/// 建立方式一:類方法 blockOperationWithBlock:
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"我是 NSBlockOperation 的任務");
}];


/// 建立方式二:
NSBlockOperation *blockOperation2 = [[NSBlockOperation alloc] init];
複製程式碼

一般都是使用第一種方式。畢竟第一種方式直接建立了例項並加入了任務。

  • NSBlockOperation 新增任務

如果執行以下 blockOperation2 ,你會發現沒有任何反應。這也是合乎情理的,畢竟裡邊沒有任何任務。

我們可以使用 addExecutionBlock: 方法來為 NSBlockOperation 新增任務。

[blockOperation2 addExecutionBlock:^{
    NSLog(@"我是 NSBlockOperation 的任務");
}];
複製程式碼

可以使用 addExecutionBlock: 來為一個 NSBlockOperation 例項新增任務。不論是通過 addExecutionBlock 新增的任務,還是 blockOperationWithBlock 初始化時傳入的任務,都儲存在其例項屬性 executionBlocks 中。沒錯,一個 NSBlockOperation 例項可以存在多個任務。

可以看出:直接在當前執行緒執行,並且會阻塞當前執行緒。

1.6 子類 NSInvocationOperation

建立 NSInvocationOperation 的方式也有兩種:

/// 建立方式一:分別傳入 target / selector / object 
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(theInvocationSelector) object:nil];

、

/// 建立方式二:直接傳入 NSInvocation 例項
//    Method theMethod = class_getInstanceMethod([self class],@selector(theInvocationSelector));
//    const char *theType = method_getTypeEncoding(theMethod);
//    NSMethodSignature *theMethodSignature = [NSMethodSignaturesignatureWithObjCTypes:theType];

NSMethodSignature *theMethodSignature = [self methodSignatureForSelector:@selector(theInvocationSelector)];

NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:theMethodSignature];

NSInvocationOperation *invicationOperation2 = [[NSInvocationOperation alloc]
initWithInvocation:invocation];
複製程式碼

第一種方式較好理解。第二種方式要傳入 NSInvocation 例項,不知道是什麼東西的朋友可以將 NSInvocation 理解為可以傳多個引數的 performSelector:withObject: 即可(或者看一下 iOS - NSInvocation的使用改進的 performSelector)。

1.7 自定義子類

自定義 NSOperation 的子類有蠻多需要注意的點。

先借用 大佬的圖 來看一下 NSOperation 幾個重要方法的預設實現:

另外,NSOperation 有一個非常重要的概念:狀態。這些狀態改變時,需要發出 KVO 通知,也用一下 大佬的圖

如果只需要自定義非非同步(也就是同步)的 NSOperation ,只需要重寫 main 方法就好了。如果還想要重寫訪問 NSOperation 資料的 gettersetter ,那請一定保證這些方法時執行緒安全的。

預設情況下, main 方法不做任何事情。在重寫該方法時,不要呼叫 [super main]

同時,main 方法將自動在一個自動釋放池中執行,無所另外建立自動釋放池。

但對於非同步的 NSOperation, 那麼至少要重寫以下方法或屬性:

  1. start

    以非同步的方式啟動操作。

    一旦啟動,更新操作的執行屬性 executing

    start 前,必須檢查是否已經被 cancel。若已被取消

    絕對不能呼叫 [super start]

  2. asynchronous

    返回 YES 即可,最好實現 KVO 通知。

  3. executing

    執行緒安全地返回操作的執行狀態。

    值發生變化時必須發出 KVO 通知。KVO 的 keyPath 為 isExecuting。

  4. finished

    執行緒安全地返回操作的完成狀態。

    值發生變化時必須發出 KVO 通知。

    一旦操作被取消,任務也被認為完成了。(操作佇列在任務完成之後才會移除該操作)

當然,重寫以上屬性只是最低要求,實際開發中,我們肯定需要重寫更多。

Apple 官方檔案中的 Maintaining Operation Object States 可以找到各個 KVO 支援的屬性的 keyPath:

屬性 KVO 的 keyPath 備註
ready isReady 一般情況無需重寫此屬性。但如果 ready 的值由外部因素決定,開發者最好提供自定義實現。取消一個正在等待依賴項完成的 NSOperation,這些依賴項將被忽略而直接將此屬性的值更新為 YES,以表示可正常執行。此時,操作佇列將更快將其移除。
executing isExecuting 若重寫 start 方法,則必須重寫該屬性。並在其值改變時發出 KVO 通知
finished isFinished 若重寫 start 方法,則必須重寫該屬性,並在 NSOperation 完成執行或被取消時將值置為 YES 併發出 KVO 通知
cancelled isCancelled 不推薦發出此屬性的 KVO 通知,畢竟 cancel 時該 NSOperation 的屬性 readyfinished 的值也會更改

注意

  1. 自定義 NSOperation 時,務必支援 cancel 操作。

    執行任務的主流程應該週期性地檢查 cancelled 屬性。如果返回 YES,NSOperation 應該儘快清理並退出。

    如果重寫了 start 方法,那麼就必須包括取消操作的早期檢查。

  2. 自行管理屬性 executingfinished時, 務必在 executing 屬性值置回 NO 時將 finished 屬性值置為 YES。

    即使在開始執行之前被取消,也一定要處理好這些改變。

學學大佬 AFNetworking

當然,這裡看的並不是最新版本,而是 AFNetworking 的 2.3.1 版本。

AFNetworking 3.0 之後的版本全面使用 NSURLSessionNSURLSession 本身非同步、且不需要 runloop 的配合。因此 3.0 之後的版本並沒有使用 NSOperation

AFURLConnectionOperation 是個 非同步NSOperation 子類。來看一看:

  • 啟動操作 -start
- (void)start {
    /// # 1
    [self.lock lock];
    
    /// # 2
    if ([self isCancelled]) {
        /// # 2.1
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } 
    /// # 3
    else if ([self isReady]) {
        /// # 3.1
        self.state = AFOperationExecutingState;
        
        /// # 3.2
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    
    /// # 4
    [self.lock unlock];
}
複製程式碼
  1. 使用 NSRecursiveLock(遞迴鎖)加鎖,保證執行緒安全;
  2. 檢查 NSOperation 是否已被 cancel;

    2.1 通過 子執行緒 執行取消操作。

  3. 檢查 NSOperation 是否已經 ready;

    3.1 更新狀態 state,併發出 KVO 通知。其內部也使用了遞迴鎖;

    3.2 通過 子執行緒 開始網路請求。

  4. 操作完成之後,將 NSRecursiveLock(遞迴鎖)解鎖。

start 中可以看出, AFNetworking 通過子執行緒來執行取消操作與真正的任務,來看一看:

  • 專用子執行緒 +networkRequestThread
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    /// # 2.1
    @autoreleasepool {
        /// # 2.2
        [[NSThread currentThread] setName:@"AFNetworking"];
    
        /// # 2.3
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        /// # 2.4
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    /// # 1
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate,^{
        /// # 2
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    
    return _networkRequestThread;
}
複製程式碼
  1. 使用 dispatch_once 來建立子執行緒;
  2. 指定執行緒入口為 networkRequestThreadEntryPoint

    2.1 在 autoreleasepool 在執行操作,方便管理;

    2.2 更改執行緒名,方便使用;

    2.3 重點 建立 NSRunloop 物件,保持執行緒活躍,同時配合 NSURLConnection 執行網路請求;

    2.4 重點 建立 NSMechPort 物件,實現執行緒間通訊,保證始終在建立的子執行緒處理邏輯。

看完子執行緒,再看看一下取消 connection 這個操作(這並不是 -cancel 方法):

  • 取消 connection cancelConnection
- (void)cancelConnection {
    /// 收集錯誤資訊,就不放了
    /// ...

    /// # 1
    if (![self isFinished]) {
        /// # 2
        if (self.connection) {
            /// # 2.1
            [self.connection cancel];
            /// # 2.2
            [self performSelector:@selector(connection:didFailWithError:) withObject:self.connection withObject:error];
        } 
        /// # 3
        else {
            self.error = error;
            [self finish];
        }
    }
}
複製程式碼
  1. 檢查是否已經 finished;
  2. 檢查 connection 屬性是否存在,若存在;

    2.1 取消當前網路請求;

    2.2 呼叫 onnection:didFailWithError: 儲存錯誤資訊,在內部清理工作並執行終結操作 finish

  3. connection 不存在。

    儲存錯誤資訊,然後直接執行終結操作 finish

  • 終結操作 -finish
- (void)finish {
    [self.lock lock];
    self.state = AFOperationFinishedState;
    [self.lock unlock];

    /// ...
}
複製程式碼

簡單明瞭,在保證執行緒安全的同時,利用 -setState: 管理自身狀態併發出 KVO 通知。

  • 執行任務 -operationDidStart
- (void)operationDidStart {
    /// # 1
    [self.lock lock];
    /// # 2
    if (![self isCancelled]) {
        /// ...
    }
    [self.lock unlock];
}
複製程式碼
  1. 這裡依然使用 NSRecursiveLock(遞迴鎖)保證執行緒安全;
  2. 再次檢查是否已被 cancel(完全遵循 Apple 明確的 “定期檢查 cancelled 屬性。”);

好的,最後看一下 Apple 心心念唸的取消操作

  • 取消操作 -cancel
- (void)cancel {
    /// # 1
    [self.lock lock];
    /// # 2
    if (![self isFinished] && ![self isCancelled]) {
        /// # 2.1
        [super cancel];

        /// # 2.2
        if ([self isExecuting]) {
            [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
        }
    }
    [self.lock unlock];
}
複製程式碼
  1. 依然使用 NSRecursiveLock(遞迴鎖)保證執行緒安全;
  2. 檢查自身狀態,如果已經 finishedcancelled,那也除了解鎖也沒啥號執行的了。

    2.1 呼叫 [super cancel];(mainstart 務必不要呼叫父類方法)

    2.2 檢查是否正在 executing。如果正在執行,跟 -start 中一樣取消 connection 的任務。

可以看到, NSOperation 本身是存在 -cancel 方法的。但是這裡還需要處理自身任務的 cancelConnection

畢竟這是個 NSOperation ,其狀態不止取決於業務邏輯,還要與其父類溝通好,於是 AFNetworking 方法重寫 readyexecutingfinished 的 getter。

- (BOOL)isReady {
    return self.state == AFOperationReadyState && [super isReady];
}

- (BOOL)isExecuting {
    return self.state == AFOperationExecutingState;
}

- (BOOL)isFinished {
    return self.state == AFOperationFinishedState;
}
複製程式碼

最後,抄兩個高階操作

  1. qualityOfService

    服務質量。表示 NSOperation 在獲取系統資源時的優先順序,預設為 NSQualityOfServiceDefault

    優先順序最高為 NSQualityOfServiceUserInteractive

  2. queuePriority

    佇列優先順序。表示 NSOperation 在操作佇列中的相對優先順序,預設為 NSOperationQueuePriorityNormal

    統一操作佇列中,優先順序更高的 NSOperation 將會被先執行,當然前提是 ready 為 YES。

    最高優先順序為 NSOperationQueuePriorityVeryHigh。吐槽,這個起名有點上頭。。。

2、NSOperationQueue

NSOperationQueue ,基於優先順序與就緒狀態執行 NSOperation 的操作佇列。

一旦一個 NSOpertaion 被加入到 NSOperationQueue 中,無法直接移除,除非它報告自己完成了操作,否則一直在操作佇列中。

將一個 NSOperation 例項加入到 NSOperationQueue 之後,它的 asynchronous 已經沒有任何作用了。此時,NSOperationQueue 只是呼叫 GCD 來非同步執行它。

對於操作佇列中 ready 為 YES 的 NSOperation,操作佇列將選擇 queuePriority 最大的執行。

2.1 建立操作佇列

NSOperationQueue 一共有兩種:主佇列、自定義佇列。

/// 主佇列(其實不能叫建立)
NSOperationQueue *theMainOperationQueue = [NSOperationQueue mainQueue];

/// 自定義佇列
複製程式碼

所有新增到主佇列的 NSOperation 都會放到主執行緒執行。

新增到自定義佇列的 NSOperation,預設放到子執行緒併發執行。

2.2 向 NSOperationQueue 新增操作

存在多種方法可以向 NSOperationQueue 新增操作。

NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation1");
}];

NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation2");
}];

NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation3");
}];

NSBlockOperation *operation4 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation4");
}];

NSBlockOperation *operation5 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation5");
}];

/// 新增單個 NSOperation
[theOperationQueue addOperation:operation1];

/// 新增多個 NSOperation
[theOperationQueue addOperations:@[operation2,operation3,operation4,operation5] waitUntilFinished:NO];

/// 便利方法,直接新增 Block 到操作佇列中
[theOperationQueue addOperationWithBlock:^{
    NSLog(@"addOperationWithBlock");
}];
複製程式碼

對於 -addOperation:-addOperations:waitUntilFinished: 而言,一個 NSOperation 一次最多隻能在一個操作佇列中。如果該 NSOperation 已經在某個佇列中,則 此方法將會丟擲 NSInvalidArgumentException ;同樣的,如果某個 NSOperation 正在被執行,也將丟擲這個異常。而且,就算第二次新增使用的是同一個佇列,也是會丟擲該異常的。

需要提醒的是,-addOperations:waitUntilFinished: 的第二個引數如果傳入 YES,那麼將阻塞當前執行緒,直到第一個引數中的 NSOperation 全部 finished

最後來個特例:-addBarrierBlock:。意為新增柵欄方法,具體功效請檢視方法 dispatch_barrier_asyn

2.3 NSOperationQueue 控制併發數量

NSOperationQueue 有一個名為 maxConcurrentOperationCount 的屬性。這個屬性的值用來控制一個操作佇列中同時最多可以有多少個 NSOperation 參與併發執行。

maxConcurrentOperationCount 預設為 -1,即不限制。同時 Apple 也推薦我們設定為該值,這個值會使系統根據系統條件來設定最大的值。

2.4 暫停/恢復操作 suspended

suspended 其實只是一個屬性。當我們設定它為 YES 時,此時就將佇列暫停了;同時將其設定為 NO 時,此時佇列恢復。

這裡所謂的暫停,並不是設定之後立馬暫停,而是執行當前正在執行的操作之後不繼續執行。

2.5 取消操作 -cancelAllOperations

呼叫 -cancelAllOperations 可以直接取消佇列中的所有操作。就是所有操作。。。

suspended 不同,suspended 暫停之後可以恢復。而這裡取消了就是真的取消了。

2.6 操作同步 waitUntilAllOperationsAreFinished

呼叫此方法之後,阻塞當前執行緒,直到佇列中的所有任務完成。


3、對比 GCD 與 NSOperationQueue

最後,借用 大佬的一張圖 來對比一下 GCD 與 NSOperationQueue

簡單的任務使用 GCD 就好了。

如果需要控制併發數、取消任務、新增依賴關係等,那就使用 NSOperation Queue 好了。只不過很多時候都需要子類化 NSOperation。。。

四、iOS 中的鎖

iOS 中有很多種鎖,先擺上 ibireme 大佬在 不再安全的 OSSpinLock 的效能測試圖 :

當然,加鎖方案是很多的,比如利用 序列佇列柵欄方法排程組 也可以實現加鎖目的。但是這裡只討論 真正的鎖

先來瞭解幾個概念 (參考子 維基百科):

  • 臨界區:一塊對公共資源進行訪問的程式碼。就是一段程式碼不能被併發執行,也就是,兩個執行緒不能同時執行這段程式碼。

  • 自旋鎖:執行緒反覆檢查鎖變數是否可用。在這個過程中,執行緒一直保持執行,所以是一種 忙碌等待。自旋鎖有效避免了程式上下文的排程開銷,這對於執行緒阻塞時間很短的場合很有效。但是,單核單執行緒 CPU 不適用於自旋鎖

  • 互斥鎖:防止兩條執行緒同時對同一公共資源(比如全域性變數,這個變數就是互斥量)進行讀寫的機制。該目的通過將程式碼切片成一個一個的臨界區而達成。等待互斥鎖的執行緒進入休眠,被喚醒時需要進行上下文切換。

  • 讀寫鎖:又稱為 “共享-互斥鎖” 與 “多讀者-單寫者鎖”。用於解決多執行緒對公共資源的讀寫問題。讀操作可併發重入,寫操作是互斥的。讀寫鎖通常用互斥鎖、條件變數、訊號量實現

  • 條件鎖:條件變數。任務需要的某些資源要求不滿足時就進入休眠,條件鎖鎖上。當被分配到資源時,條件鎖解開,任務程繼續進行。條件變數總是與互斥量一同出現,

  • 遞迴鎖:遞迴鎖可以被一個執行緒多次 lock,且不會造成死鎖問題。遞迴鎖會追蹤它被 lock 多少次,只有 unlock 次數與 lock 次數平衡才能真正釋放這個鎖。遞迴鎖適用於遞迴方法。

  • 訊號量:一個同步物件,用於保持在 0 至指定最大值之間的一個計數值。執行緒對該訊號量完成一次 wait,計數值 -1;執行緒對該訊號量完成一次 signal(release),計數值 +1。當計數值為 0 或小於 0 時,執行緒必須等待該訊號量知道其數值大於 0。訊號量適用於一個僅能同時被有限數量的使用者使用的共享資源,是一種無需 忙碌等待 的鎖。

先來一個搶火車票的經典場景:


- (void)trainTicket {
    
    self.trainTicketRemainder = 10000;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0,0);
    
    dispatch_async(queue,^{
      
        for (int i = 0; i < 1000; ++i) {

            [self buyTrainTicket];
            sleep(0.1);
        }
    });
    
    dispatch_async(queue,^{
      
        for (int i = 0; i < 1000; ++i) {
            
            [self buyTrainTicket];
            sleep(0.2);
        }
    });
    
}


- (void)buyTrainTicket {
    if (self.trainTicketRemainder < 1) {
        NSLog(@"票量不足...");
        return;
    }
    
    self.trainTicketRemainder--;
    
    NSLog(@"售票成功,當前餘量: %d",self.trainTicketRemainder);
    
}
複製程式碼

按照常理來講,最後一個列印的 log 中的數量應該是 10000 - 1000 * 2 = 8000 才對。但是這裡的數字是 8028。很明顯,這違背了程式的正確性。

發生這種問題的主要原因就在於兩個不同的執行緒同時在對一個共享資源(self.trainTicketRemainder)進行修改。為了避免這種問題,我們就需要對執行緒進行 加鎖

加鎖的原理也不難,來一段虛擬碼:

do {
    Acquire lock            /// 獲得鎖
        Critical section    /// 臨界區     
    Release lock            /// 釋放鎖
        Reminder section    /// 非臨界區
}

複製程式碼

對於上述例子,可以將 self.trainTicketRemainder-- 這句程式碼作為臨界區。

1、OSSpinLock 自旋鎖

從上邊的圖可以看出,這種鎖效能最佳,但是它已經不安全了。

簡單描述一下這裡的不安全:低優先順序執行緒拿到鎖時,高優先順序會處於 OSSpinLock 的忙等待狀態而消耗大量 CPU 時間,這使低優先順序執行緒搶不到 CPU 時間,從而導致低優先順序執行緒無法完成任務並釋放鎖。更多請移步 不再安全的 OSSpinLock

這種問題被稱為 優先順序反轉。(這裡建議先看一下執行緒服務質量 qos_class,或者看一下 2.1 建立佇列 中全域性隊列出的的執行緒優先順序)

使用 OSSpinLock 需要 #import <libkern/OSAtomic.h>

/// 初始化 OSSpinLock
/// OS_SPINLOCK_INIT 預設值為 0,在 locked 狀態下大於 0,unlocked 狀態下也為 0。
OSSpinLock theOSSpinLock = OS_SPINLOCK_INIT;

/// @abstract 上鎖
/// @param __lock : OSSpinLock 的地址
OSSpinLockLock(&theOSSpinLock);

/// @abstract 解鎖
/// @param __lock : OSSpinLock 的地址
OSSpinLockUnlock(&theOSSpinLock);

/// @abstract 上鎖
/// @discussion 嘗試加鎖,可以加鎖理解加鎖並返回 YES,否則返回 NO
/// @param __lock :OSSpinLock 的地址
OSSpinLockTry(&theOSSpinLock);
複製程式碼

注意 OSSpinLock 自 iOS 10 已被廢棄,使用 os_unfair_lock 代替。

列個表:

種類 備註
OSSpinLock 自旋鎖 不安全,iOS 10 已啟用
os_unfair_lock 互斥鎖 替代 OSSpinLock
pthread_mutex 互斥鎖 PTHREAD_MUTEX_NORMAL#import <pthread.h>
pthread_mutex (recursive) 遞迴鎖 PTHREAD_MUTEX_RECURSIVE#import <pthread.h>
pthread_mutex (cond) 條件鎖 pthread_cond_t#import <pthread.h>
pthread_rwlock 讀寫鎖 讀操作重入,寫操作互斥
@synchronized 互斥鎖 效能差,且無法鎖住記憶體地址更改的物件
NSLock 互斥鎖 封裝 pthread_mutex
NSRecursiveLock 遞迴鎖 封裝 pthread_mutex (recursive)
NSCondition 條件鎖 封裝 pthread_mutex (cond)
NSConditionLock 條件鎖 可以指定具體條件值

2、os_unfair_lock 互斥鎖

os_unfair_lock 時 Apple 推薦用於取代不安全的 OSSpinLock,但僅限於 iOS 10 及以上系統。

os_unfair_lock 是一種互斥鎖,處於等待的執行緒不會像自旋鎖那樣忙等,而是休眠。

使用 os_unfair_lock 需要 #import <os/lock.h>

/// 初始化 os_unfair_lock
os_unfair_lock theOs_unfair_lock = OS_UNFAIR_LOCK_INIT;
 
 
/// @abstract 上鎖
/// @param lock : os_unfair_lock 的地址
os_unfair_lock_lock(&theOs_unfair_lock);
 
 
/// @abstract 解鎖
/// @param lock : os_unfair_lock 的地址
os_unfair_lock_unlock(&theOs_unfair_lock);


/// @abstract 上鎖
/// @discussion 嘗試加鎖,可以加鎖理解加鎖並返回 YES,否則返回 NO
/// @param lock : os_unfair_lock 的地址
os_unfair_lock_trylock(&theOs_unfair_lock);
複製程式碼

3、pthread_mutex 互斥鎖

pthread 表示 POSIX thread,定義了一組跨平臺的執行緒相關的 API。

pthread_mutex 可以是一個互斥鎖。

使用 pthread_mutex 需要 #import <pthread.h>

/// 定義一個屬性變數
pthread_mutexattr_t attr;

/// @abstract 初始化屬性
/// @param attr : 屬性的地址
pthread_mutexattr_init(&attr);

/// @abstract 設定屬性型別為 PTHREAD_MUTEX_NORMAL
/// @param __lock : 屬性 pthread_mutexattr_t 的地址
/// @param type : 鎖的型別
pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_NORMAL);

/// 定義一個鎖變數
pthread_mutex_t mutex;

/// @abstract 使用指定屬性初始化鎖
/// @param mutex : 鎖的地址
/// @param attr : 屬性 pthread_mutexattr_t 的地址
pthread_mutex_(&mutex,&attr);
    
/// @abstract 銷燬屬性
/// @param attr : 屬性的地址
pthread_mutexattr_destroy(&attr);

/// @abstract 上鎖
/// @param mutex : 鎖的地址
pthread_mutex_lock(&mutex);

/// @abstract 解鎖
/// @param mutex : 鎖的地址
pthread_mutex_unlock(&mutex);

/// @abstract 銷燬 pthread_mutex
/// @discussion 一般在 dealloc 中執行
/// @param mutex : 鎖的地址
pthread_mutex_destroy(&mutex);
複製程式碼

其中,在 pthread_mutexattr_settype 方法的第二個引數代表鎖的型別,一共有四種:

#define PTHREAD_MUTEX_NORMAL		0                       /// 普通的鎖
#define PTHREAD_MUTEX_ERRORCHECK	1                       /// 錯誤檢查
#define PTHREAD_MUTEX_RECURSIVE		2                       /// 遞迴鎖
#define PTHREAD_MUTEX_DEFAULT		PTHREAD_MUTEX_NORMAL    /// 預設的鎖,也就是 PTHREAD_MUTEX_NORMAL
複製程式碼

當型別是 PTHREAD_MUTEX_DEFAULT 時,相當於 null。所以上邊可以改寫為:

pthread_mutexattr_settype(&attr,null);
複製程式碼

4、pthread_mutex ( recursive ) 遞迴鎖

在上一節中,說到 pthread_mutexattr_settype 方法的第二個引數有多種取值。如果這個值傳入 PTHREAD_MUTEX_RECURSIVE,由設定此值屬性初始化的 pthread_mutex 就是一個遞迴鎖。

如果是互斥鎖或者互斥鎖,一個執行緒對同一個鎖加鎖多次,那麼定會造成思索。但是遞迴鎖允許一個執行緒對同一個鎖多次加鎖,不會造成死鎖問題。不過,只有 unlock 次數與 lock 次數平衡時,遞迴鎖才會真正釋放。

使用 pthread_mutex 需要 #import <pthread.h>

相關方法演示這裡就不貼了,跟上一節幾乎一模一樣,除了這一句 pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE)

不過舉一個例子:

- (void)display_PTHREAD_MUTEX_RECURSIVE {
    
    /// 定義一個屬性
    pthread_mutexattr_t attr;
    /// 初始化屬性
    pthread_mutexattr_init(&attr);
    /// 設定鎖的型別
    pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE);
    
    
    /// 初始化鎖
    pthread_mutex_init(&mutex,&attr);
    
    /// 銷燬屬性
    pthread_mutexattr_destroy(&attr);
    
    
    [self test_PTHREAD_MUTEX_RECURSIVE];
    
    /// 銷燬鎖(一般在 dealloc 中)
    pthread_mutex_destroy(&mutex);
    
}

- (void)test_PTHREAD_MUTEX_RECURSIVE {
    
    static int count = 5;
    
    // 第一次進來直接加鎖,第二次進來,已經加鎖了。還能遞迴繼續加鎖
    pthread_mutex_lock(&mutex);
    NSLog(@"加鎖 %@",[NSThread currentThread]);
    
    if (count > 0) {
        count--;
        [self test_PTHREAD_MUTEX_RECURSIVE];
    }
    
    NSLog(@"解鎖 %@",[NSThread currentThread]);
    pthread_mutex_unlock(&mutex);
    
}
複製程式碼

5、pthread_mutex 條件鎖

pthread_mutex 除了互斥鎖、遞迴鎖,還可以扮演條件鎖。

不過, pthread_mutex 想要扮演條件鎖,還需要條件變數 pthread_cond_t 的配合。

使用 pthread_mutex 需要 #import <pthread.h>

/// 初始化鎖,使用預設屬性
pthread_mutex_init(&mutex,NULL);

/// 定義一個條件變數
pthread_cond_t cond;

/// 初始化條件變數
pthread_cond_init(&cond,NULL);

/// 等待條件(進入休眠時,放開 mutex 鎖;被喚醒後,對 mutex 重新加鎖)
pthread_cond_wait(&cond,&mutex);

/// 喚醒一個正在等待該條件的執行緒
pthread_cond_signal(&cond);

/// 喚醒所有正在等待該條件的執行緒
pthread_cond_broadcast(&cond);

/// 銷燬條件變數
pthread_cond_destroy(&cond);

/// 銷燬 mutex 鎖
pthread_mutex_destroy(&mutex);

複製程式碼

條件鎖的使用場景並不是特別多。這裡使用 “生產者 - 消費者”來演示一下。

先定義幾個變數:

/// mutex 鎖
pthread_mutex_t mutex;
/// 條件變數
pthread_cond_t cond;
/// 用於儲存資料
NSMutableArray      *shop;
複製程式碼

上程式碼:

- (void)setup_pthread_cond {
    
    /// 初始化鎖,使用預設屬性
    pthread_mutex_init(&mutex,NULL);
    
    /// 初始化條件變數
    pthread_cond_init(&cond,NULL);
    
    /// 喚醒所有正在等待該條件的執行緒
    pthread_cond_broadcast(&cond);
    
    
    shop = [NSMutableArray array];
    
    
    NSLog(@"請開始你的表演...");
    
    dispatch_queue_t theQueue = dispatch_get_global_queue(0,0);
    dispatch_async(theQueue,^{
        [self produce];
    });

    dispatch_async(theQueue,^{
        [self buy];
    });
    
}


/// 假裝是生產者
- (void)produce {
    
    while (true) {
    
        pthread_mutex_lock(&mutex);
        
        /// 生產需要時間(doge)
        sleep(0.1);
        
        if (shop.count > 5) {
            NSLog(@"商店滿了,不能再生產了");
            pthread_cond_wait(&cond,&mutex);
        }
        
        /// 將生產的產品丟進商店
        [shop addObject:@"fan"];
        NSLog(@"生產了一個 fan");
        
        /// 喚醒一個正在等待的執行緒
        pthread_cond_signal(&cond);
        
        pthread_mutex_unlock(&mutex);
    }
}


/// 假裝是消費者
- (void)buy {
    
    while (true) {
    
        pthread_mutex_lock(&mutex);
        
        /// shop 內沒有存貨,買不到
        /// 進入等待(進入休眠,放開 _mutex;被喚醒時,會重新對 _mutex 加鎖)
        if (shop.count < 1) {
            NSLog(@"現在買不到, 我等一下吧");
            pthread_cond_wait(&cond,&mutex);
        }
        
        
        [shop removeObjectAtIndex:0];
        NSLog(@"終於買到了,不容易");
        
        pthread_cond_signal(&cond);
        
        pthread_mutex_unlock(&mutex);
    }
}

複製程式碼

iOS設計模式之(二)生產者-消費者 提出了使用 條件鎖 的場景。

不得不說,生產者 - 消費者 這種模式能很好地解決 【奪命連環 call】。

6、pthread_rwlock 讀寫鎖

pthread_rwlock,對鞋所。又稱為 “共享-互斥鎖” 與 “多讀者-單寫者鎖”。用於解決多執行緒對公共資源的讀寫問題。讀操作可併發重入,寫操作是互斥的。

使用 pthread_rwlock 需要 #import <pthread.h>

pthread_rwlock_t rwlock;

/// 初始化鎖
pthread_rwlock_init(&rwlock,NULL);

/// 讀 - 加鎖
pthread_rwlock_rdlock(&rwlock);
/// 讀 - 嘗試加鎖
pthread_rwlock_tryrdlock(&rwlock);

/// 寫 - 加鎖
pthread_rwlock_wrlock(&rwlock);
/// 寫 - 嘗試加鎖
pthread_rwlock_trywrlock(&rwlock);

/// 解鎖
pthread_rwlock_unlock(&rwlock);

/// 銷燬鎖
pthread_rwlock_destroy(&rwlock);
複製程式碼

程式碼演示:

- (void)setup_pthread_rwlock {
    
    // pthread_rwlock_t rwlock;

    /// 初始化鎖
    pthread_rwlock_init(&rwlock,NULL);

    dispatch_queue_t theQueue = dispatch_get_global_queue(0,0);
    
    for (int i = 0; i < 3; ++i) {
        dispatch_async(theQueue,^{
            [self write];
        });
    }
    
    for (int i = 0; i < 3; ++i) {
        dispatch_async(theQueue,^{
            [self read];
        });
    }
}


- (void)write {
    pthread_rwlock_wrlock(&rwlock);
    
    sleep(3);
    NSLog(@"%s",__func__);
    
    pthread_rwlock_unlock(&rwlock);
}

- (void)read {
    pthread_rwlock_rdlock(&rwlock);
    
    sleep(1);
    NSLog(@"%s",__func__);
    
    pthread_rwlock_unlock(&rwlock);
}
複製程式碼

7、@synchronized 互斥鎖(遞迴鎖)

@synchronized 是 iOS 中使用最簡單的鎖,但是也是效能最差的鎖(見第四章開頭的圖)。

@synchronized 是互斥鎖,當然他也是一個遞迴鎖,不然怎麼可能巢狀呢?

它需要一個引數,這個引數是我們要鎖住的物件。如果不知道要鎖住啥,那就選擇 self。

簡單的用法演示:

- (void)setup_synchronized {
    
    dispatch_queue_t theQueue = dispatch_get_global_queue(0,^{
            [self display_synchronized];
        });
    }
    
}


- (void)display_synchronized {
    
    @synchronized (self) {
        sleep(3);
        NSLog(@"%@",[NSThread currentThread]);
    }
}
複製程式碼

注意 @synchronized 無法鎖住“被加鎖物件”地址更改的情況,具體原因看這裡:

原理也很簡單。在 objc_sync_enter 利用 id2data 將傳入的物件 id 轉換為 SyncData,然後利用 SyncData.mutex->lock()。Clang 將 @synchronized 改寫的原始碼 clang - RewriteObjC.cpp,真正實現 objc_sync_enter 原始碼在 runtime 中,下載地址 Apple 官網github

另外,建議大家看下這篇文章 關於 @synchronized,這兒比你想知道的還要多

8、NSLock 互斥鎖

NSLock,互斥鎖。由屬性為 PTHREAD_MUTEX_NORMALpthread_mutex 封裝而來的。

iOS 中存在一個 NSLocking 協議:

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end
複製程式碼

NSLock 遵循 NSLocking 協議,可以直接使用 - lock 來加鎖,使用 - unlock 來解鎖。

此外,NSLock 還提供了 - lockBeforeDate:- tryLock 兩種便利性方法。

NSLock 用法演示:

/// 初始化一個 NSLock
NSlock *lock = [[NSLock alloc] init];

/// 加鎖
[lock lock];

/// 解鎖
[lock unlock];

/// 在 10s 內加鎖,成功返回 YES,否則返回 NO。
[lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];

/// 嘗試加鎖,成功返回 YES,否則返回 NO。
[lock tryLock];
複製程式碼

為了方便, NSLock 還提供一個名為 name 的屬性。我們可以頭通過這個屬性來方便開發。

9、NSRecursiveLock 遞迴鎖

NSRecursiveLock,遞迴鎖。由屬性為 PTHREAD_MUTEX_RECURSIVEpthread_mutex 封裝而來的。

NSLock 相同,NSRecursiveLock 遵循了 NSLocking 協議可以直接使用 - lock 來加鎖,使用 - unlock 來解鎖。

其 API 與 NSLock 一致,使用方法也完全相同。

10、NSCondition 條件鎖

NSCondition,條件鎖。由 pthread_mutexpthread_cond 封裝而來。

NSCondition 遵循了 NSLocking 協議,可以直接使用 - lock 來加鎖,使用 - unlock 來解鎖。

在瞭解更多的資訊之前,先讀一讀 NSCondition - Foundation | Apple 官方檔案 :

The semantics for using an NSCondition object are as follows:

使用 NSCondition 物件的語義如下:

  1. Lock the condition object.
  1. 鎖定條件物件。
  1. Test a boolean predicate. (This predicate is a boolean flag or other variable in your code that indicates whether it is safe to perform the task protected by the condition.)
  1. 測試布林謂詞。(此謂詞是程式碼中的布林標誌或其他變數,只是執行售條件保護的任務是否安全。)
  1. If the boolean predicate is false,call the condition object’s wait or waitUntilDate: method to block the thread. Upon returning from these methods,go to step 2 to retest your boolean predicate. (Continue waiting and retesting the predicate until it is true.)
  1. 如果布林謂詞為 false,呼叫條件物件的 waitwaitUntilDate: 方法來阻塞執行緒。根據這兩個方法的返回值,跳轉到第二步重新測試布林謂詞。(持續等待和測試,直到布林謂詞為真。)
  1. If the boolean predicate is true,perform the task.
  1. 如果布林為此為 true,執行任務。
  1. Optionally update any predicates (or signal any conditions) affected by your task.
  1. 根據需要來改變影響任務的任何謂詞(或者是向條件變數發訊號)。
  1. When your task is done,unlock the condition object.
  1. 當任務完成時,解鎖條件變數。

布林謂詞,就是一個 【判斷依據】,也就是一個只有 True 與 False 的標識,就跟平時使用的 if 是一樣的,不過在使用 NSCondition,使用的 while。 Aple 還提供了一段虛擬碼:

lock the condition
while (!(boolean_predicate)) {
    wait on condition
}
do protected work
(optionally,signal or broadcast the condition again or change a predicate value)
unlock the condition
複製程式碼

這基本瞭解 NSCondition 是個什麼意思,且怎麼用。但是還沒完...

NSCondition 還提供了以下方法與屬性:

/// 為鎖設定名字,方便使用
@property (nullable,copy) NSString *name;

/// 等待條件變數,阻塞當前執行緒一直到條件條件被訊號通知
- (void)wait;

/// 在指定時間之前,等待條件變數,阻塞當前執行緒一直到條件條件被訊號通知
- (BOOL)waitUntilDate:(NSDate *)limit;

/// 向條件變數發訊號,喚醒一個正在等待的執行緒
- (void)signal;

/// 向條件變數發訊號,喚醒所有正在等待的執行緒
- (void)broadcast;
複製程式碼

讓我們改寫一下 4.5 pthread_mutex 條件鎖 中的例項程式碼,將 pthread_mutex 的使用換成 NSCondition

/// 假裝是生產者
- (void)produce_NSCondition {
    
    while (true) {
    
        [condition lock];
        
        /// 生產需要時間(doge)
        sleep(0.1);
        
        /// 【shot.count > 5】 這就是 布林謂詞
        /// shop 內庫存滿了,不能再生產了 >>> v
        if (shop.count > 5) {
            NSLog(@"商店滿了,不能再生產了");
            [condition wait];
        }
        
        /// 將生產的產品丟進商店
        [shop addObject:@"fan"];
        NSLog(@"生產了一個 fan");
        
        /// 向條件變數發通知
        [condition signal];
        
        [condition unlock];
    }
}


/// 假裝是消費者
- (void)buy_NSCondition {
    
    while (true) {
    
        [condition lock];
        
        
        /// 【shot.count < 1】 這就是 布林謂詞
        /// shop 內沒有存貨,買不到 >>> 布林謂詞為 false
        /// 進入等待
        if (shop.count < 1) {
            NSLog(@"現在買不到, 我等一下吧");
            [condition wait];
        }
        
        
        [shop removeObjectAtIndex:0];
        NSLog(@"終於買到了,不容易");
        
        /// 向條件變數發通知
        [condition signal];
        
        [condition unlock];
    }
}
複製程式碼

執行結果與 pthread_mutex_t 配合 pthread_cond_t 一模一樣。。。

11、NSConditionLock 條件鎖

NSConditionLockNSCondition 更高階,可以指定具體的條件值。

NSConditionLock 遵循了 NSLocking 協議,可以直接使用 - lock 來加鎖,使用 - unlock 來解鎖。

此外,NSConditionLock 還提供以下屬性與方法:

/// 以一個 NSInteger 的條件變數初始化
- (instancetype)initWithCondition:(NSInteger)condition;

/// 屬性:條件變數
@property (readonly) NSInteger condition;

/// 阻塞執行緒等待條件成立後加鎖
- (void)lockWhenCondition:(NSInteger)condition;

/// 嘗試加鎖,立馬返回結果。若可以加鎖則返回 YES,否則返回 NO
/// 不考慮條件變數
/// 無論結果如何,都會執行後續程式碼
- (BOOL)tryLock;

/// 條件成立時,嘗試加鎖,立馬返回。若可以加鎖則返回 YES,否則返回 NO
/// 無論結果如何,都會執行後續程式碼
- (BOOL)tryLockWhenCondition:(NSInteger)condition;

/// 阻塞執行緒,在指定時間內加鎖。若成功返回 YES,否則返回 NO
/// 不考慮條件變數,也就是 NSLocking 的 - lock 方法
- (BOOL)lockBeforeDate:(NSDate *)limit;

/// 阻塞執行緒,在指定時間內若條件變數成立則加鎖。若成功返回 YES,否則返回 NO
/// 無論結果如何,都會執行後續程式碼
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

/// 解鎖,並重新設定條件變數
- (void)unlockWithCondition:(NSInteger)condition;
複製程式碼

注意 unlockWithCondition 並不是在條件變數符合的時候解鎖,而是先解鎖,然後更新條件變數。

參考連結

多執行緒程式設計指南

iOS多執行緒:『GCD』詳盡總結

iOS多執行緒全套:執行緒生命週期,多執行緒的四種解決方案,執行緒安全問題,GCD的使用,NSOperation的使用

關於iOS多執行緒,你看我就夠了

iOS-多執行緒程式設計知識整理

iOS 多執行緒程式設計 ——iOS 開發的一座大山

Apple 開原始碼 - libdispatch

GCD原始碼吐血分析(2)

深入淺出 GCD 之 dispatch_queue

GCD全解-09-dispatch_block-GCD取消操作

GCD全解-10-dispatch_source-排程資源

NSObject - Objective-C Runtime

iOS原始碼解析: dispatch_once是如何實現的?

不再安全的 OSSpinLock

關於 @synchronized,這兒比你想知道的還要多

拋開效能,談談不該用@Synchronized的原因

clang - RewriteObjC.cpp

Apple 開原始碼 - runtime

github - mkAppleOpenSourceDownload/objc4