做者:bool周 原文連接:我所理解的 iOS 併發編程ios
不管在哪一個平臺,併發編程都是一個讓人頭疼的問題。慶幸的是,相對於服務端,客戶端的併發編程簡單了許多。這篇文章主要講述一些基於 iOS 平臺的一些併發編程相關東西,我寫博客習慣於先介紹原理,後介紹用法,畢竟對於 API 的使用,官網有更好的文檔。git
爲了便於理解,這裏先解釋一些相關概念。若是你對這些概念已經很熟悉,能夠直接跳過。github
從操做系統定義上來講,進程就是系統進行資源分配和調度的基本單位,系統建立一個線程後,會爲其分配對應的資源。在 iOS 系統中,進程能夠理解爲就是一個 App。iOS 並無提供能夠建立進程的 API,即便你調用 fork()
函數,也不能建立新的進程。因此,本文所說的併發編程,都是針對線程來講的。算法
線程是程序執行流的最小單元。通常狀況下,一個進程會有多個線程,或者至少有一個線程。一個線程有建立、就緒、運行、阻塞和死亡五種狀態。線程能夠共享進程的資源,全部的問題也是由於共享資源引發的。數據庫
操做系統引入線程的概念,是爲了使過個 CPU 更好的協調運行,充分發揮他們的並行處理能力。例如在 iOS 系統中,你能夠在主線程中進行 UI 操做,而後另啓一些線程來處理與 UI 操做無關的事情,兩件事情並行處理,速度比較快。這就是併發的大體概念。編程
按照 wiki 上面解釋:是分時操做系統分配給每一個正在運行的進程微觀上的一段CPU時間(在搶佔內核中是:從進程開始運行直到被搶佔的時間)。線程能夠被認爲是 」微進程「,所以這個概念也能夠用到線程方面。swift
通常操做系統使用時間片輪轉算法進行調度,即每次調度時,老是選擇就緒隊列的隊首進程,讓其在CPU上運行一個系統預先設置好的時間片。一個時間片內沒有完成運行的進程,返回到緒隊列末尾從新排隊,等待下一次調度。不一樣的操做系統,時間片的範圍不一致,通常都是毫秒(ms)級別。api
死鎖是因爲多個線程(進程)在執行過程當中,由於爭奪資源而形成的互相等待現象,你能夠理解爲卡主了。產生死鎖的必要條件有四個:數組
爲了便於理解,這裏舉一個例子:一座橋,同一時間只容許一輛車通過(互斥)。兩輛車 A,B 從橋的兩端開上橋,走到橋的中間。此時 A 車不願退(不可剝奪),又想佔用 B 車所佔據的道路;B 車此時也不願退,又想佔用 A 車所佔據的道路(請求和保持)。此時,A 等待 B 佔用的資源,B 等待 A 佔用的資源(環路等待),兩車僵持下去,就造成了死鎖現象。xcode
當多個線程同時訪問一塊共享資源(例如數據庫),由於時序性問題,會致使數據錯亂,這就是線程不安全。例如數據庫中某個整形字段的 value 爲 0,此時兩個線程同時對其進行寫入操做,線程 A 拿到原值爲 0,加一後變爲 1;線程 B 並非在 A 加完後拿的,而是和 A 同時拿的,加完後也是 1,加了兩次,理想值應該爲 2,可是數據庫中最終值倒是 1。實際開發場景可能要比這個複雜的多。
所謂的線程安全,能夠理解爲在多個線程操做(例如讀寫操做)這部分數據時,不會出現問題。
由於線程共享進程資源,在併發狀況下,就會出現線程安全問題。爲了解決此問題,就出現了鎖這個概念。在多線程環境下,當你訪問一些共享數據時,拿到訪問權限,給數據加鎖,在這期間其餘線程不可訪問,直到你操做完以後進行解鎖,其餘線程才能夠對其進行操做。
iOS 提供了多種鎖,ibireme 大神的 這篇文章 對這些鎖進行了性能分析,我這裏直接把圖 cp 過來了:
下面針對這些鎖,逐一分析。
ibireme 大神的文章也說了,雖然這個鎖性能最高,可是已經不安全了,建議再也不使用,這裏簡單說一下。
OSSpinLock 是一種自旋鎖,主要提供了加鎖(OSSpinLockLock
)、嘗試枷鎖(OSSpinLockTry
)和解鎖(OSSpinLockUnlock
)三個方法。對一塊資源進行加鎖時,若是嘗試加鎖失敗,不會進入睡眠狀態,而是一直進行詢問(自旋),佔用 CPU資源,不適用於較長時間的任務。在自旋期間,由於佔用 CPU 致使低優先級線程拿不到 CUP 資源,沒法完成任務並釋放鎖,從而造成了優先級反轉。
so,雖然性能很高,可是不要用了。並且 Apple 也已經將這個類比較爲 deprecate 了。
自旋鎖 & 互斥鎖 二者大致相似,區別在於:自旋鎖屬於 busy-waiting 類型鎖,嘗試加鎖失敗,會一直處於詢問狀態,佔用 CPU 資源,效率高;互斥鎖屬於 sleep-waiting 類型鎖,在嘗試失敗以後,會被阻塞,而後進行上下文切換置於等待隊列,由於有上下文切換,效率較低。 在 iOS 中 NSLock 屬於互斥鎖。
優先級反轉 :當一個高優先級任務訪問共享資源時,該資源已經被一個低優先級任務搶佔,阻塞了高優先級任務;同時,該低優先級任務被一個次高優先級的任務所搶先,從而沒法及時地釋放該臨界資源。最終使得任務優先級被倒置,發生阻塞。(引用自 wiki
關於自旋鎖的原理,bestswifter 的文章 深刻理解 iOS 開發中的鎖 這篇文章講得很好,我這裏大部分鎖的知識引用於此,建議讀一下原文。
自旋鎖是加不上就一直嘗試,也就是一個循環,直到嘗試加上鎖,僞代碼以下:
bool lock = false; // 一開始沒有鎖上,任何線程均可以申請鎖
do {
while(test_and_set(&lock); // test_and_set 是一個原子操做,嘗試加鎖
Critical section // 臨界區
lock = false; // 至關於釋放鎖,這樣別的線程能夠進入臨界區
Reminder section // 不須要鎖保護的代碼
}
複製代碼
使用 :
OSSpinLock spinLock = OS_SPINLOCK_INIT;
OSSpinLockLock(&spinLock);
// 被鎖住的資源
OSSpinLockUnlock(&spinLock);
複製代碼
dispatch_semaphore 並不屬於鎖,而是信號量。二者的區別以下:
dispatch_semaphore 使用分爲三步:create、wait 和 signal。以下:
// create
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
// thread A
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// execute task A
NSLog(@"task A");
sleep(10);
dispatch_semaphore_signal(semaphore);
});
// thread B
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// execute task B
NSLog(@"task B");
dispatch_semaphore_signal(semaphore);
});
複製代碼
執行結果:
2018-05-03 21:40:09.068586+0800 ConcurrencyTest[44084:1384262] task A
2018-05-03 21:40:19.072951+0800 ConcurrencyTest[44084:1384265] task B
複製代碼
thread A,B 是兩個異步線程,通常狀況下,各自執行本身的事件,互不干涉。可是根據 console 輸出,B 是在 A 執行完了 10s 執行以後才執行的,顯然受到阻塞。使用 dispatch_semaphore 大體執行過程這樣:建立 semaphore 時,信號量值爲 1;執行到線程 A 的 dispatch_semaphore_wait
時,信號量值減 1,變爲 0;而後執行任務 A,執行完畢後 sleep
方法阻塞當前線程 10s;與此同時,線程 B 執行到了 dispatch_semaphore_wait
,因爲信號量此時爲 0,且線程 A 中設置的爲 DISPATCH_TIME_FOREVER
,所以須要等到線程 A sleep 10s 以後,執行 dispatch_semaphore_signal
將信號量置爲 1,線程 B 的任務纔開始執行。
根據上面的描述,dispatch_semaphore 的原理大體也就瞭解了。GCD 源碼 對這些方法定義以下:
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout) {
long value = dispatch_atomic_dec2o(dsema, dsema_value);
dispatch_atomic_acquire_barrier();
if (fastpath(value >= 0)) {
return 0;
}
return _dispatch_semaphore_wait_slow(dsema, timeout);
}
static long
_dispatch_semaphore_wait_slow(dispatch_semaphore_t dsema,
dispatch_time_t timeout)
{
long orig;
again:
// Mach semaphores appear to sometimes spuriously wake up. Therefore,
// we keep a parallel count of the number of times a Mach semaphore is
// signaled (6880961).
while ((orig = dsema->dsema_sent_ksignals)) {
if (dispatch_atomic_cmpxchg2o(dsema, dsema_sent_ksignals, orig,
orig - 1)) {
return 0;
}
}
struct timespec _timeout;
int ret;
switch (timeout) {
default:
do {
uint64_t nsec = _dispatch_timeout(timeout);
_timeout.tv_sec = (typeof(_timeout.tv_sec))(nsec / NSEC_PER_SEC);
_timeout.tv_nsec = (typeof(_timeout.tv_nsec))(nsec % NSEC_PER_SEC);
ret = slowpath(sem_timedwait(&dsema->dsema_sem, &_timeout));
} while (ret == -1 && errno == EINTR);
if (ret == -1 && errno != ETIMEDOUT) {
DISPATCH_SEMAPHORE_VERIFY_RET(ret);
break;
}
// Fall through and try to undo what the fast path did to
// dsema->dsema_value
case DISPATCH_TIME_NOW:
while ((orig = dsema->dsema_value) < 0) {
if (dispatch_atomic_cmpxchg2o(dsema, dsema_value, orig, orig + 1)) {
errno = ETIMEDOUT;
return -1;
}
}
// Another thread called semaphore_signal().
// Fall through and drain the wakeup.
case DISPATCH_TIME_FOREVER:
do {
ret = sem_wait(&dsema->dsema_sem);
} while (ret != 0);
DISPATCH_SEMAPHORE_VERIFY_RET(ret);
break;
}
goto again;
}
複製代碼
以上時對 wait 方法的定義,若是你不想看代碼,能夠直接聽我說:
dispatch_semaphore_wait
方法時,若是信號量大於 0,直接返回;不然進入後續步驟。_dispatch_semaphore_wait_slow
方法根據傳入 timeout
參數不一樣,使用 switch-case 處理。semaphore_timedwait
方法進行等待,直至超時。semaphore_wait
進行等待,直到收到 singal
信號。至於 dispatch_semaphore_signal
就比較簡單了,源碼以下:
long dispatch_semaphore_signal(dispatch_semaphore_t dsema) {
dispatch_atomic_release_barrier();
long value = dispatch_atomic_inc2o(dsema, dsema_value);
if (fastpath(value > 0)) {
return 0;
}
if (slowpath(value == LONG_MIN)) {
DISPATCH_CLIENT_CRASH("Unbalanced call to dispatch_semaphore_signal()");
}
return _dispatch_semaphore_signal_slow(dsema);
}
複製代碼
_dispatch_semaphore_signal_slow
,這個方法的做用是調用內核的 semaphore_signal 函數喚醒信號量,而後返回 1。Pthreads 是 POSIX Threads 的縮寫。pthread_mutex
屬於互斥鎖,即嘗試加鎖失敗後悔阻塞線程並睡眠,會進行上下文切換。鎖的類型主要有三種:PTHREAD_MUTEX_NORMAL
、PTHREAD_MUTEX_ERRORCHECK
、PTHREAD_MUTEX_RECURSIVE
。
使用以下:
pthread_mutex_t mutex; // 定義鎖
pthread_mutexattr_t attr; // 定義 mutexattr_t 變量
pthread_mutexattr_init(&attr); // 初始化attr爲默認屬性
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // 設置鎖的屬性
pthread_mutex_init(&mutex, &attr); // 建立鎖
pthread_mutex_lock(&mutex); // 申請鎖
// 臨界區
pthread_mutex_unlock(&mutex); // 釋放鎖
複製代碼
NSLock 屬於互斥鎖,是 Objective-C 封裝的一個對象。雖然咱們不知道 Objective-C 是如何實現的,可是咱們能夠在 swift 源碼 中找到他的實現 :
...
internal var mutex = _PthreadMutexPointer.allocate(capacity: 1)
...
open func lock() {
pthread_mutex_lock(mutex)
}
open func unlock() {
pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
// Wakeup any threads waiting in lock(before:)
pthread_mutex_lock(timeoutMutex)
pthread_cond_broadcast(timeoutCond)
pthread_mutex_unlock(timeoutMutex)
#endif
}
複製代碼
能夠看出他只是將 pthread_mutex
封裝了一下。只由於比 pthread_mutex
慢一些,難道是由於方法層級之間的調用,多了幾回壓棧操做???
常規使用:
NSLock *mutexLock = [NSLock new];
[mutexLock lock];
// 臨界區
[muteLock unlock];
複製代碼
NSCondition 能夠同時起到 lock 和條件變量的做用。一樣你能夠在 swift 源碼 中找到他的實現 :
open class NSCondition: NSObject, NSLocking {
internal var mutex = _PthreadMutexPointer.allocate(capacity: 1)
internal var cond = _PthreadCondPointer.allocate(capacity: 1)
public override init() {
pthread_mutex_init(mutex, nil)
pthread_cond_init(cond, nil)
}
deinit {
pthread_mutex_destroy(mutex)
pthread_cond_destroy(cond)
mutex.deinitialize(count: 1)
cond.deinitialize(count: 1)
mutex.deallocate()
cond.deallocate()
}
open func lock() {
pthread_mutex_lock(mutex)
}
open func unlock() {
pthread_mutex_unlock(mutex)
}
open func wait() {
pthread_cond_wait(cond, mutex)
}
open func wait(until limit: Date) -> Bool {
guard var timeout = timeSpecFrom(date: limit) else {
return false
}
return pthread_cond_timedwait(cond, mutex, &timeout) == 0
}
open func signal() {
pthread_cond_signal(cond)
}
open func broadcast() {
pthread_cond_broadcast(cond)
}
open var name: String?
}
複製代碼
能夠看出,它仍是遵循 NSLocking 協議,lock 方法一樣仍是使用的 pthread_mutex
,wait 和 signal 使用的是 pthread_cond_wait
和 pthread_cond_signal
。
使用 NSCondition 是,先對要操做的臨界區加鎖,而後由於條件不知足,使用 wait 方法阻塞線程;待條件知足以後,使用 signal 方法進行通知。下面是一個 生產者-消費者的例子:
NSCondition *condition = [NSCondition new];
NSMutableArray *products = [NSMutableArray array];
// consume
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[condition lock];
while (products.count == 0) {
[condition wait];
}
[products removeObjectAtIndex:0];
[condition unlock];
});
// product
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[condition lock];
[products addObject:[NSObject new]];
[condition signal];
[condition unlock];
});
複製代碼
NSConditionLock 是經過使用 NSCondition 來實現的,遵循 NSLocking 協議,而後這是 swift 源碼 (源碼比較佔篇幅,我這裏簡化一下):
open class NSConditionLock : NSObject, NSLocking {
internal var _cond = NSCondition()
...
open func lock(whenCondition condition: Int) {
let _ = lock(whenCondition: condition, before: Date.distantFuture)
}
open func `try`() -> Bool {
return lock(before: Date.distantPast)
}
open func tryLock(whenCondition condition: Int) -> Bool {
return lock(whenCondition: condition, before: Date.distantPast)
}
open func unlock(withCondition condition: Int) {
_cond.lock()
_thread = nil
_value = condition
_cond.broadcast()
_cond.unlock()
}
open func lock(before limit: Date) -> Bool {
_cond.lock()
while _thread != nil {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
_thread = pthread_self()
_cond.unlock()
return true
}
open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
_cond.lock()
while _thread != nil || _value != condition {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
_thread = pthread_self()
_cond.unlock()
return true
}
...
}
複製代碼
能夠看出它使用了一個 NSCondition 全局變量來實現 lock 和 unlock 方法,都是一些簡單的代碼邏輯,就不詳細說了。
使用 NSConditionLock 注意:
-[unlockWithCondition:]
並非知足條件時解鎖,而是解鎖後,修改 condition 值。typedef NS_ENUM(NSInteger, CTLockCondition) {
CTLockConditionNone = 0,
CTLockConditionPlay,
CTLockConditionShow
};
- (void)testConditionLock {
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:CTLockConditionPlay];
// thread one
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[conditionLock lockWhenCondition:CTLockConditionNone];
NSLog(@"thread one");
sleep(2);
[conditionLock unlock];
});
// thread two
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
if ([conditionLock tryLockWhenCondition:CTLockConditionPlay]) {
NSLog(@"thread two");
[conditionLock unlockWithCondition:CTLockConditionShow];
NSLog(@"thread two unlocked");
} else {
NSLog(@"thread two try lock failed");
}
});
// thread three
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(2);
if ([conditionLock tryLockWhenCondition:CTLockConditionPlay]) {
NSLog(@"thread three");
[conditionLock unlock];
NSLog(@"thread three locked success");
} else {
NSLog(@"thread three try lock failed");
}
});
}
// thread four
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(4);
if ([conditionLock tryLockWhenCondition:CTLockConditionShow]) {
NSLog(@"thread four");
[conditionLock unlock];
NSLog(@"thread four unlocked success");
} else {
NSLog(@"thread four try lock failed");
}
});
}
複製代碼
而後看輸出結果 :
2018-05-05 16:34:33.801855+0800 ConcurrencyTest[97128:3100768] thread two
2018-05-05 16:34:33.802312+0800 ConcurrencyTest[97128:3100768] thread two unlocked
2018-05-05 16:34:34.804384+0800 ConcurrencyTest[97128:3100776] thread three try lock failed
2018-05-05 16:34:35.806634+0800 ConcurrencyTest[97128:3100778] thread four
2018-05-05 16:34:35.806883+0800 ConcurrencyTest[97128:3100778] thread four unlocked success
複製代碼
能夠看出,thread one 由於條件和初始化不符,加鎖失敗,未輸出 log; thread two 條件相符,解鎖成功,並修改加鎖條件;thread three 使用原來的加鎖條件,顯然沒法加鎖,嘗試加鎖失敗; thread four 使用修改後的條件,加鎖成功。
NSRecursiveLock 屬於遞歸鎖。而後這是 swift 源碼,只貼一下關鍵部分:
open class NSRecursiveLock: NSObject, NSLocking {
...
public override init() {
super.init()
#if CYGWIN
var attrib : pthread_mutexattr_t? = nil
#else
var attrib = pthread_mutexattr_t()
#endif
withUnsafeMutablePointer(to: &attrib) { attrs in
pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
pthread_mutex_init(mutex, attrs)
}
}
...
}
複製代碼
它是使用 PTHREAD_MUTEX_RECURSIVE
類型的 pthread_mutex_t
初始化的。遞歸所能夠在一個線程中重複調用,而後底層會記錄加鎖和解鎖次數,當兩者次數相同時,才能正確解鎖,釋放這塊臨界區。
使用例子:
- (void)testRecursiveLock {
NSRecursiveLock *recursiveLock = [NSRecursiveLock new];
int (^__block fibBlock)(int) = ^(int num) {
[recursiveLock lock];
if (num < 0) {
[recursiveLock unlock];
return 0;
}
if (num == 1 || num == 2) {
[recursiveLock unlock];
return num;
}
int newValue = fibBlock(num - 1) + fibBlock(num - 2);
[recursiveLock unlock];
return newValue;
};
int value = fibBlock(10);
NSLog(@"value is %d", value);
}
複製代碼
@synchronized 是犧牲性能來換取語法上的簡潔。若是你想深刻了解,建議你去讀 這篇文章。這裏說一下他的大概原理:
@synchronized 的加鎖過程,大概是這個樣子:
@try {
objc_sync_enter(obj); // lock
// 臨界區
} @finally {
objc_sync_exit(obj); // unlock
}
複製代碼
@synchronized 的存儲結構,是使用哈希表來實現的。當你傳入一個對象後,會爲這個對象分配一個鎖。鎖和對象打包成一個對象,而後和一個鎖在進行二次打包成一個對象,能夠理解爲 value;經過一個算法,根據對象的地址獲得一個值,做爲 key。而後以 key-value 的形式寫入哈希表。結構大概是這個樣子:
存儲的時候,是以哈希表結構存儲,不是我上面畫的順序存儲,上面只是一個節點而已。
@synchronized 的使用就很簡單了 :
NSMutableArray *elementArray = [NSMutableArray array];
@synchronized(elementArray) {
[elementArray addObject:[NSObject new]];
}
複製代碼
前面也說了,pthreads 是 POSIX Threads 的縮寫。這個東西通常咱們用不到,這裏簡單介紹一下。Pthreads 是POSIX的線程標準,定義了建立和操縱線程的一套API。實現POSIX 線程標準的庫常被稱做Pthreads,通常用於Unix-like POSIX 系統,如Linux、 Solaris。
NSThread
是對內核 mach kernel 中的 mach thread 的封裝,一個 NSThread
對象就是一個線程。使用頻率比較低,除了 API 的使用,沒什麼可講的。若是你已經熟悉這些 API,能夠跳過這一節了。
使用初始化方法初始化一個 NSTherad
對象,調用 -[cancel]
、-[start
、-[main]
方法對線程進行操做,通常線程執行完即銷燬,或者由於某種異常退出。
/** 使用 target 對象的中的方法做爲執行主體,能夠經過 argument 傳遞一些參數。 - (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument; /** 使用 block 對象做爲執行主體 */
- (instancetype)initWithBlock:(void (^)(void))block;
/** 類方法,上面對象方法須要調用 -[start] 方法啓動線程,下面兩個方法不須要手動啓動 */
+ (void)detachNewThreadWithBlock:(void (^)(void))block;
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
複製代碼
/** 說一下最後一個參數,這裏你至少指定一個 mode 執行 selector,若是你傳 nil 或者空數組,selector 不會執行,雖然方法定義寫了 nullable */
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
複製代碼
/** modes 參數同上一個 */
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait
複製代碼
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
複製代碼
@property (class, readonly, strong) NSThread *currentThread;
複製代碼
使用線程相關方法時,記得設置好 name,方便後面調試。同時也設置好優先級等其餘參數。
performSelector: 系列方法已經不太安全,慎用。
GCD 是基於 C 實現的一套 API,並且是開源的,若是有興趣,能夠在 這裏 down 一份源碼研究一下。GCD 是由系統幫咱們處理多線程調度,非常方便,也是使用頻率最高的。這一章節主要講解一下 GCD 的原理和使用。
在講解以前,咱們先有個概覽,看一下 GCD 爲咱們提供了那些東西:
系統所提供的 API,徹底能夠知足咱們平常開發需求了。下面就根據這些模塊分別講解一下。
GCD 爲咱們提供了兩類隊列,串行隊列 和 並行隊列。二者的區別是:
除此以外,還要解釋一個容易混淆的概念,併發和並行:
最後,還有一個概念,同步和異步:
咱們使用時,通常使用這幾個隊列:
主隊列 - dispatch_get_main_queue :一個特殊的串行隊列。在 GCD 中,方法主隊列中的任務都是在主線程執行。當咱們更新 UI 時想 dispatch 到主線程,可使用這個隊列。
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_main_queue(), ^{
// UI 相關操做
});
複製代碼
} ```
全局並行隊列 - dispatch_get_global_queue : 系統提供的一個全局並行隊列,咱們能夠經過指定參數,來獲取不一樣優先級的隊列。系統提供了四個優先級,因此也能夠認爲系統爲咱們提供了四個並行隊列,分別爲 :
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_async(queue, ^{
// 相關操做
});
複製代碼
自定義隊列 :你能夠本身定義串行或者並行隊列,來執行一些相關的任務,平時開發中也建議用自定義隊列。建立自定義隊列時,須要兩個參數。一個是隊列的名字,方便咱們再調試時查找隊列使用,命名方式採用的是反向 DNS 命名規則;一個是隊列類型,傳 NULL 或者 DISPATCH_QUEUE_SERIAL 表明串行隊列,傳 DISPATCH_QUEUE_CONCURRENT 表明並行隊列,一般狀況下,不要傳 NULL,會下降可讀性。 DISPATCH_QUEUE_SERIAL_INACTIVE 表明串行不活躍隊列,DISPATCH_QUEUE_CONCURRENT_INACTIVE 表明並行不活躍隊列,在執行 block 任務時,須要被激活。
複製代碼
dispatch_queue_t queue = dispatch_queue_create("com.bool.dispatch",DISPATCH_QUEUE_SERIAL); ```
dispatch_queue_set_specific
、dispatch_queue_get_specific
和 dispatch_get_specific
方法,爲 queue 設置關聯的 key 或者根據 key 找到關聯對象等操做。能夠說,系統爲咱們提供了 5 中不一樣的隊列,運行在主線程中的 main queue;3 個不一樣優先級的 global queue; 一個優先級更低的 background queue。除此以外,開發者能夠自定義一些串行和並行隊列,這些自定義隊列中被調度的全部 block 最終都會被放到系統全局隊列和線程池中,後面會講這部分原理。盜用一張經典圖:
咱們大多數狀況下,都是使用 dispatch_asyn()
作異步操做,由於程序原本就是順序執行,不多用到同步操做。有時候咱們會把 dispatch_syn()
當作鎖來用,以達到保護的做用。
系統維護的是一個隊列,根據 FIFO 的規則,將 dispatch 到隊列中的任務一一執行。有時候咱們想把一些任務延後執行如下,例如 App 啓動時,我想讓主線程中一個耗時的工做放在後,能夠嘗試用一下 dispatch_asyn()
,至關於把任務從新追加到了隊尾。
dispatch_async(dispatch_get_main_queue(), ^{
// 想要延後的任務
});
複製代碼
一般狀況下,咱們使用 dispatch_asyn()
是不會形成死鎖的。死鎖通常出如今使用 dispatch_syn()
的時候。例如:
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"dead lock");
});
複製代碼
想上面這樣寫,啓動就會報錯誤。如下狀況也如此:
dispatch_queue_t queue = dispatch_queue_create("com.bool.dispatch", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"dispatch asyn");
dispatch_sync(queue, ^{
NSLog(@"dispatch asyn -> dispatch syn");
});
});
複製代碼
在上面的代碼中,dispatch_asyn()
整個 block(稱做 blcok_asyn) 當作一個任務追加到串行隊列隊尾,而後開始執行。在 block_asyn 內部中,又進行了 dispatch_syn()
,想一想要執行 block_syn。由於是串行隊列,須要前一個執行完(block_asyn),再執行後面一個(block_syn);可是要執行完 block_asyn,須要執行內部的 block_syn。互相等待,造成死鎖。
現實開發中,還有更復雜的死鎖場景。不過如今編譯器很友好,咱們能在編譯執行時就檢測到了。
針對下面這幾行代碼,咱們分析一下它的底層過程:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_queue_create("com.bool.dispatch", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"dispatch asyn test");
});
}
複製代碼
建立隊列
源碼很長,但實際只有一個方法,邏輯比較清晰,以下:
/** 開發者調用的方法 */
dispatch_queue_t
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
{
return _dispatch_queue_create_with_target(label, attr,
DISPATCH_TARGET_QUEUE_DEFAULT, true);
}
/** 內部實際調用方法 */
DISPATCH_NOINLINE
static dispatch_queue_t
_dispatch_queue_create_with_target(const char *label, dispatch_queue_attr_t dqa,
dispatch_queue_t tq, bool legacy)
{
// 1.初步判斷
if (!slowpath(dqa)) {
dqa = _dispatch_get_default_queue_attr();
} else if (dqa->do_vtable != DISPATCH_VTABLE(queue_attr)) {
DISPATCH_CLIENT_CRASH(dqa->do_vtable, "Invalid queue attribute");
}
// 2.配置隊列參數
dispatch_qos_t qos = _dispatch_priority_qos(dqa->dqa_qos_and_relpri);
#if !HAVE_PTHREAD_WORKQUEUE_QOS
if (qos == DISPATCH_QOS_USER_INTERACTIVE) {
qos = DISPATCH_QOS_USER_INITIATED;
}
if (qos == DISPATCH_QOS_MAINTENANCE) {
qos = DISPATCH_QOS_BACKGROUND;
}
#endif // !HAVE_PTHREAD_WORKQUEUE_QOS
_dispatch_queue_attr_overcommit_t overcommit = dqa->dqa_overcommit;
if (overcommit != _dispatch_queue_attr_overcommit_unspecified && tq) {
if (tq->do_targetq) {
DISPATCH_CLIENT_CRASH(tq, "Cannot specify both overcommit and "
"a non-global target queue");
}
}
if (tq && !tq->do_targetq &&
tq->do_ref_cnt == DISPATCH_OBJECT_GLOBAL_REFCNT) {
// Handle discrepancies between attr and target queue, attributes win
if (overcommit == _dispatch_queue_attr_overcommit_unspecified) {
if (tq->dq_priority & DISPATCH_PRIORITY_FLAG_OVERCOMMIT) {
overcommit = _dispatch_queue_attr_overcommit_enabled;
} else {
overcommit = _dispatch_queue_attr_overcommit_disabled;
}
}
if (qos == DISPATCH_QOS_UNSPECIFIED) {
dispatch_qos_t tq_qos = _dispatch_priority_qos(tq->dq_priority);
tq = _dispatch_get_root_queue(tq_qos,
overcommit == _dispatch_queue_attr_overcommit_enabled);
} else {
tq = NULL;
}
} else if (tq && !tq->do_targetq) {
// target is a pthread or runloop root queue, setting QoS or overcommit
// is disallowed
if (overcommit != _dispatch_queue_attr_overcommit_unspecified) {
DISPATCH_CLIENT_CRASH(tq, "Cannot specify an overcommit attribute "
"and use this kind of target queue");
}
if (qos != DISPATCH_QOS_UNSPECIFIED) {
DISPATCH_CLIENT_CRASH(tq, "Cannot specify a QoS attribute "
"and use this kind of target queue");
}
} else {
if (overcommit == _dispatch_queue_attr_overcommit_unspecified) {
// Serial queues default to overcommit!
overcommit = dqa->dqa_concurrent ?
_dispatch_queue_attr_overcommit_disabled :
_dispatch_queue_attr_overcommit_enabled;
}
}
if (!tq) {
tq = _dispatch_get_root_queue(
qos == DISPATCH_QOS_UNSPECIFIED ? DISPATCH_QOS_DEFAULT : qos,
overcommit == _dispatch_queue_attr_overcommit_enabled);
if (slowpath(!tq)) {
DISPATCH_CLIENT_CRASH(qos, "Invalid queue attribute");
}
}
// 3. 初始化隊列
if (legacy) {
// if any of these attributes is specified, use non legacy classes
if (dqa->dqa_inactive || dqa->dqa_autorelease_frequency) {
legacy = false;
}
}
const void *vtable;
dispatch_queue_flags_t dqf = 0;
if (legacy) {
vtable = DISPATCH_VTABLE(queue);
} else if (dqa->dqa_concurrent) {
vtable = DISPATCH_VTABLE(queue_concurrent);
} else {
vtable = DISPATCH_VTABLE(queue_serial);
}
switch (dqa->dqa_autorelease_frequency) {
case DISPATCH_AUTORELEASE_FREQUENCY_NEVER:
dqf |= DQF_AUTORELEASE_NEVER;
break;
case DISPATCH_AUTORELEASE_FREQUENCY_WORK_ITEM:
dqf |= DQF_AUTORELEASE_ALWAYS;
break;
}
if (legacy) {
dqf |= DQF_LEGACY;
}
if (label) {
const char *tmp = _dispatch_strdup_if_mutable(label);
if (tmp != label) {
dqf |= DQF_LABEL_NEEDS_FREE;
label = tmp;
}
}
dispatch_queue_t dq = _dispatch_object_alloc(vtable,
sizeof(struct dispatch_queue_s) - DISPATCH_QUEUE_CACHELINE_PAD);
_dispatch_queue_init(dq, dqf, dqa->dqa_concurrent ?
DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
(dqa->dqa_inactive ? DISPATCH_QUEUE_INACTIVE : 0));
dq->dq_label = label;
#if HAVE_PTHREAD_WORKQUEUE_QOS
dq->dq_priority = dqa->dqa_qos_and_relpri;
if (overcommit == _dispatch_queue_attr_overcommit_enabled) {
dq->dq_priority |= DISPATCH_PRIORITY_FLAG_OVERCOMMIT;
}
#endif
_dispatch_retain(tq);
if (qos == QOS_CLASS_UNSPECIFIED) {
// legacy way of inherithing the QoS from the target
_dispatch_queue_priority_inherit_from_target(dq, tq);
}
if (!dqa->dqa_inactive) {
_dispatch_queue_inherit_wlh_from_target(dq, tq);
}
dq->do_targetq = tq;
_dispatch_object_debug(dq, "%s", __func__);
return _dispatch_introspection_queue_create(dq);
}
複製代碼
根據代碼生成的流程圖,不想看代碼直接看圖,下同:
根據流程圖,這個方法的步驟以下:
dispatch_queue_create()
方法以後,內部會調用 _dispatch_queue_create_with_target()
方法。_dispatch_object_alloc
方法申請一個 dispatch_queue_t 對象空間,dq。DISPATCH_QUEUE_WIDTH_MAX
即最大,不設限;串行的會設置爲 1。異步執行
這個版本異步執行的代碼,由於方法拆分不少,因此顯得很亂。源碼以下:
/** 開發者調用 */
void dispatch_async(dispatch_queue_t dq, dispatch_block_t work) {
dispatch_continuation_t dc = _dispatch_continuation_alloc();
uintptr_t dc_flags = DISPATCH_OBJ_CONSUME_BIT;
_dispatch_continuation_init(dc, dq, work, 0, 0, dc_flags);
_dispatch_continuation_async(dq, dc);
}
/** 內部調用,包一層,再深刻調用 */
DISPATCH_NOINLINE
void
_dispatch_continuation_async(dispatch_queue_t dq, dispatch_continuation_t dc)
{
_dispatch_continuation_async2(dq, dc,
dc->dc_flags & DISPATCH_OBJ_BARRIER_BIT);
}
/** 根據 barrier 關鍵字區別串行仍是並行,分兩支 */
DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_continuation_async2(dispatch_queue_t dq, dispatch_continuation_t dc,
bool barrier)
{
if (fastpath(barrier || !DISPATCH_QUEUE_USES_REDIRECTION(dq->dq_width))) {
// 串行
return _dispatch_continuation_push(dq, dc);
}
// 並行
return _dispatch_async_f2(dq, dc);
}
/** 並行又多了一層調用,就是這個方法 */
DISPATCH_NOINLINE
static void
_dispatch_async_f2(dispatch_queue_t dq, dispatch_continuation_t dc)
{
if (slowpath(dq->dq_items_tail)) {// 少路徑
return _dispatch_continuation_push(dq, dc);
}
if (slowpath(!_dispatch_queue_try_acquire_async(dq))) {// 少路徑
return _dispatch_continuation_push(dq, dc);
}
// 多路徑
return _dispatch_async_f_redirect(dq, dc,
_dispatch_continuation_override_qos(dq, dc));
}
/** 主要用來重定向 */
DISPATCH_NOINLINE
static void
_dispatch_async_f_redirect(dispatch_queue_t dq,
dispatch_object_t dou, dispatch_qos_t qos)
{
if (!slowpath(_dispatch_object_is_redirection(dou))) {
dou._dc = _dispatch_async_redirect_wrap(dq, dou);
}
dq = dq->do_targetq;
// Find the queue to redirect to
while (slowpath(DISPATCH_QUEUE_USES_REDIRECTION(dq->dq_width))) {
if (!fastpath(_dispatch_queue_try_acquire_async(dq))) {
break;
}
if (!dou._dc->dc_ctxt) {
dou._dc->dc_ctxt = (void *)
(uintptr_t)_dispatch_queue_autorelease_frequency(dq);
}
dq = dq->do_targetq;
}
// 同步異步最終都是調用的這個方法,將任務追加到隊列中
dx_push(dq, dou, qos);
}
... 省略一些調用層級,
/** 核心方法,經過 dc_flags 參數區分了是 group,仍是串行,仍是並行 */
DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_continuation_invoke_inline(dispatch_object_t dou, voucher_t ov,
dispatch_invoke_flags_t flags)
{
dispatch_continuation_t dc = dou._dc, dc1;
dispatch_invoke_with_autoreleasepool(flags, {
uintptr_t dc_flags = dc->dc_flags;
_dispatch_continuation_voucher_adopt(dc, ov, dc_flags);
if (dc_flags & DISPATCH_OBJ_CONSUME_BIT) { // 並行
dc1 = _dispatch_continuation_free_cacheonly(dc);
} else {
dc1 = NULL;
}
if (unlikely(dc_flags & DISPATCH_OBJ_GROUP_BIT)) { // group
_dispatch_continuation_with_group_invoke(dc);
} else { // 串行
_dispatch_client_callout(dc->dc_ctxt, dc->dc_func);
_dispatch_introspection_queue_item_complete(dou);
}
if (unlikely(dc1)) {
_dispatch_continuation_free_to_cache_limit(dc1);
}
});
_dispatch_perfmon_workitem_inc();
}
複製代碼
不想看代碼,直接看圖:
根據流程圖描述一下過程:
dispatch_async()
方法,而後內部建立了一個 _dispatch_continuation_init
隊列,將 queue、block 這些信息和這個 dc 綁定起來。這過程當中 copy 了 block。_dispatch_async_f_redirect
方法中,從新尋找依賴目標隊列,而後追加過去。_dispatch_continuation_invoke_inline
方法裏區分串行仍是並行。由於這個方法會被頻繁調用,因此定義成了內聯函數。對於串行隊列,咱們使用信號量控制,執行前信號量置爲 wait,執行完畢後發送 singal;對於調度組,咱們會在執行完以後調用 dispatch_group_leave
。同步執行
同步執行,相對來講比較簡單,源碼以下 :
/** 開發者調用 */
void dispatch_sync(dispatch_queue_t dq, dispatch_block_t work) {
if (unlikely(_dispatch_block_has_private_data(work))) {
return _dispatch_sync_block_with_private_data(dq, work, 0);
}
dispatch_sync_f(dq, work, _dispatch_Block_invoke(work));
}
/** 內部調用 */
DISPATCH_NOINLINE void dispatch_sync_f(dispatch_queue_t dq, void *ctxt, dispatch_function_t func) {
if (likely(dq->dq_width == 1)) {
return dispatch_barrier_sync_f(dq, ctxt, func);
}
// 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(dq))) {
return _dispatch_sync_f_slow(dq, ctxt, func, 0);
}
_dispatch_introspection_sync_begin(dq);
if (unlikely(dq->do_targetq->do_targetq)) {
return _dispatch_sync_recurse(dq, ctxt, func, 0);
}
_dispatch_sync_invoke_and_complete(dq, ctxt, func);
}
複製代碼
同步執行,相對來講簡單些,大致邏輯差很少。偷懶一下,就不畫圖了,直接描述:
dispatch_sync()
方法,大多數路徑,都會調用 dispatch_sync_f()
方法。dispatch_barrier_sync_f()
方法來保證原子操做。_dispatch_introspection_sync_begin
和 _dispatch_sync_invoke_and_complete
來保證同步。dispatch_after 通常用於延後執行一些任務,能夠用來代替 NSTimer,由於有時候 NSTimer 問題太多了。在後面的一章裏,我會整體講一下多線程中的問題,這裏就不詳細說了。通常咱們這樣來使用 dispatch_after :
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_queue_create("com.bool.dispatch", DISPATCH_QUEUE_SERIAL);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC * 2.0f)),queue, ^{
// 2.0 second execute
});
}
複製代碼
在作頁面過渡時,剛進入到新的頁面咱們並不會當即更新一些 view,爲了引發用戶注意,咱們會過會兒再進行更新,能夠中此 API 來完成。
源碼以下:
DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_after(dispatch_time_t when, dispatch_queue_t queue,
void *ctxt, void *handler, bool block)
{
dispatch_timer_source_refs_t dt;
dispatch_source_t ds;
uint64_t leeway, delta;
if (when == DISPATCH_TIME_FOREVER) {
#if DISPATCH_DEBUG
DISPATCH_CLIENT_CRASH(0, "dispatch_after called with 'when' == infinity");
#endif
return;
}
delta = _dispatch_timeout(when);
if (delta == 0) {
if (block) {
return dispatch_async(queue, handler);
}
return dispatch_async_f(queue, ctxt, handler);
}
leeway = delta / 10; // <rdar://problem/13447496>
if (leeway < NSEC_PER_MSEC) leeway = NSEC_PER_MSEC;
if (leeway > 60 * NSEC_PER_SEC) leeway = 60 * NSEC_PER_SEC;
// this function can and should be optimized to not use a dispatch source
ds = dispatch_source_create(&_dispatch_source_type_after, 0, 0, queue);
dt = ds->ds_timer_refs;
dispatch_continuation_t dc = _dispatch_continuation_alloc();
if (block) {
_dispatch_continuation_init(dc, ds, handler, 0, 0, 0);
} else {
_dispatch_continuation_init_f(dc, ds, ctxt, handler, 0, 0, 0);
}
// reference `ds` so that it doesn't show up as a leak
dc->dc_data = ds;
_dispatch_trace_continuation_push(ds->_as_dq, dc);
os_atomic_store2o(dt, ds_handler[DS_EVENT_HANDLER], dc, relaxed);
if ((int64_t)when < 0) {
// wall clock
when = (dispatch_time_t)-((int64_t)when);
} else {
// absolute clock
dt->du_fflags |= DISPATCH_TIMER_CLOCK_MACH;
leeway = _dispatch_time_nano2mach(leeway);
}
dt->dt_timer.target = when;
dt->dt_timer.interval = UINT64_MAX;
dt->dt_timer.deadline = when + leeway;
dispatch_activate(ds);
}
複製代碼
dispatch_after()
內部會調用 _dispatch_after()
方法,而後先判斷延遲時間。若是爲 DISPATCH_TIME_FOREVER
(永遠不執行),則會出現異常;若是爲 0 則當即執行;不然的話會建立一個 dispatch_timer_source_refs_t 結構體指針,將上下文相關信息與之關聯。而後使用 dispatch_source 相關方法,將定時器和 block 任務關聯起來。定時器時間到時,取出 block 任務開始執行。
若是咱們有一段代碼,在 App 生命週期內最好只初始化一次,這時候使用 dispatch_once 最好不過了。例如咱們單例中常常這樣用:
+ (instancetype)sharedManager {
static BLDispatchManager *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[BLDispatchManager alloc] initPrivate];
});
return sharedInstance;
}
複製代碼
還有在定義 NSDateFormatter
時使用:
- (NSString *)todayDateString {
static NSDateFormatter *formatter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
formatter = [NSDateFormatter new];
formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:8 * 3600];
formatter.dateFormat = @"yyyyMMdd";
});
return [formatter stringFromDate:[NSDate date]];
}
複製代碼
由於這是很經常使用的一個代碼片斷,因此被加在了 Xcode 的 code snippet 中。
它的源代碼以下:
/** 一個結構體,裏面爲當前的信號量、線程端口和指向下一個節點的指針 */
typedef struct _dispatch_once_waiter_s {
volatile struct _dispatch_once_waiter_s *volatile dow_next;
dispatch_thread_event_s dow_event;
mach_port_t dow_thread;
} *_dispatch_once_waiter_t;
/** 咱們調用的方法 */
void dispatch_once(dispatch_once_t *val, dispatch_block_t block) {
dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}
/** 實際執行的方法 */
DISPATCH_NOINLINE void dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func) {
#if !DISPATCH_ONCE_INLINE_FASTPATH
if (likely(os_atomic_load(val, acquire) == DLOCK_ONCE_DONE)) {
return;
}
#endif // !DISPATCH_ONCE_INLINE_FASTPATH
return dispatch_once_f_slow(val, ctxt, func);
}
DISPATCH_ONCE_SLOW_INLINE static void dispatch_once_f_slow(dispatch_once_t *val, void *ctxt, dispatch_function_t func) {
#if DISPATCH_GATE_USE_FOR_DISPATCH_ONCE
dispatch_once_gate_t l = (dispatch_once_gate_t)val;
if (_dispatch_once_gate_tryenter(l)) {
_dispatch_client_callout(ctxt, func);
_dispatch_once_gate_broadcast(l);
} else {
_dispatch_once_gate_wait(l);
}
#else
_dispatch_once_waiter_t volatile *vval = (_dispatch_once_waiter_t*)val;
struct _dispatch_once_waiter_s dow = { };
_dispatch_once_waiter_t tail = &dow, next, tmp;
dispatch_thread_event_t event;
if (os_atomic_cmpxchg(vval, NULL, tail, acquire)) {
dow.dow_thread = _dispatch_tid_self();
_dispatch_client_callout(ctxt, func);
next = (_dispatch_once_waiter_t)_dispatch_once_xchg_done(val);
while (next != tail) {
tmp = (_dispatch_once_waiter_t)_dispatch_wait_until(next->dow_next);
event = &next->dow_event;
next = tmp;
_dispatch_thread_event_signal(event);
}
} else {
_dispatch_thread_event_init(&dow.dow_event);
next = *vval;
for (;;) {
if (next == DISPATCH_ONCE_DONE) {
break;
}
if (os_atomic_cmpxchgv(vval, next, tail, &next, release)) {
dow.dow_thread = next->dow_thread;
dow.dow_next = next;
if (dow.dow_thread) {
pthread_priority_t pp = _dispatch_get_priority();
_dispatch_thread_override_start(dow.dow_thread, pp, val);
}
_dispatch_thread_event_wait(&dow.dow_event);
if (dow.dow_thread) {
_dispatch_thread_override_end(dow.dow_thread, val);
}
break;
}
}
_dispatch_thread_event_destroy(&dow.dow_event);
}
#endif
}
複製代碼
不想看代碼直接看圖 (emmm... 根據邏輯畫完圖才發現,其實這個圖也挺亂的,因此我將兩個主分支用不一樣顏色標記處理):
根據這個圖,我來表述一下主要過程:
咱們調用 dispatch_once()
方法以後,內部多數狀況下會調用 dispatch_once_f_slow()
方法,這個方法纔是真正的執行方法。
os_atomic_cmpxchg(vval, NULL, tail, acquire)
這個方法,執行過程實際是這個樣子
if (*vval == NULL) {
*vval = tail = &dow;
return true;
} else {
return false
}
複製代碼
咱們初始化的 once_token,也就是 *vval 實際是 0,因此第一次執行時是返回 true 的。if() 中的這個方法是原子操做,也就是說,若是多個線程同時調用這個方法,只有一個線程會進入 true 的分支,其餘都進入 else 分支。
這裏先說進入 true 分支。進入以後,會執行對應的 block,也就是對應的任務。而後 next 指向 *vval, *vval 標記爲 DISPATCH_ONCE_DONE
,即執行的是這樣一個過程:
next = (_dispatch_once_waiter_t)_dispatch_once_xchg_done(val);
// 實際執行時這樣的
next = *vval;
*vval = DISPATCH_ONCE_DONE;
複製代碼
而後 tail = &dow
。此時咱們發現,原來的 *vval = &dow -> next = *vval
,實際則是 next = &dow
,若是沒有其餘線程(或者調用)進入 else 分支,&dow 實際沒有改變,即 tail == tmp
。此時 while (tail != tmp)
是不會執行的,分支結束。
若是有其餘線程(或者調用)進入了 else 分支,那麼就已經生成了一個等待響應的鏈表。此時進入 &dow 已經改變,成爲了鏈表尾部,*vval 是鏈表頭部。進入 while 循環後,開始遍歷鏈表,依次發送信號進行喚起。
而後說進入 else 分支的這些調用。進入分支後,隨即進入一個死循環,直到發現 *vval 已經標記爲了 DISPATCH_ONCE_DONE
才跳出循環。
發現 *vval 不是 DISPATCH_ONCE_DONE
以後,會將這個節點追加到鏈表尾部,並調用信號量的 wait 方法,等待被喚起。
以上爲所有的執行過程。經過源碼能夠看出,使用的是 原子操做 + 信號量來保證 block 只會被執行屢次,哪怕是在多線程狀況下。
這樣一個關於 dispatch_once
遞歸調用會產生死鎖的現象,也就很好解釋了。看下面代碼:
- (void)dispatchOnceTest {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self dispatchOnceTest];
});
}
複製代碼
經過上面分析,在 block 執行完,並將 *vval 置爲 DISPATCH_ONCE_DONE
以前,其餘的調用都會進入 else 分支。第二次遞歸調用,信號量處於等待狀態,須要等到第一個 block 執行完才能被喚起;可是第一個 block 所執行的內容就是進行第二次調用,這個任務被 wait 了,也便是說 block 永遠執行不完。死鎖就這樣發生了。
有時候沒有時序性依賴的時候,咱們會用 dispatch_apply
來代替 for loop
。例如咱們下載一組圖片:
/** 使用 for loop */
- (void)downloadImages:(NSArray <NSURL *> *)imageURLs {
for (NSURL *imageURL in imageURLs) {
[self downloadImageWithURL:imageURL];
}
}
/** dispatch_apply */
- (void)downloadImages:(NSArray <NSURL *> *)imageURLs {
dispatch_queue_t downloadQueue = dispatch_queue_create("com.bool.download", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(imageURLs.count, downloadQueue, ^(size_t index) {
NSURL *imageURL = imageURLs[index];
[self downloadImageWithURL:imageURL];
});
}
複製代碼
進行替換是須要注意幾個問題:
至於原理,就不大篇幅講了。大概是這個樣子:這個方法是同步的,會阻塞當前線程,直到全部的 block 任務都完成。若是提交到併發隊列,每一個任務執行順序是不必定的。
更多時候,咱們執行下載任務,並不但願阻塞當前線程,這時咱們可使用 dispatch_group
。
當處理批量異步任務時,dispatch_group
是一個很好的選擇。針對上面說的下載圖片的例子,咱們能夠這樣作:
- (void)downloadImages:(NSArray <NSURL *> *)imageURLs {
dispatch_group_t taskGroup = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.bool.group", DISPATCH_QUEUE_CONCURRENT);
for (NSURL *imageURL in imageURLs) {
dispatch_group_enter(taskGroup);
// 下載方法是異步的
[self downloadImageWithURL:imageURL withQueue:queue completeHandler:^{
dispatch_group_leave(taskGroup);
}];
}
dispatch_group_notify(taskGroup, queue, ^{
// all task finish
});
/** 若是使用這個方法,內部執行異步任務,會當即到 dispatch_group_notify 方法中,由於是異步,系統認爲已經執行完了。因此這個方法使用很少。 */
dispatch_group_async(taskGroup, queue, ^{
})
}
複製代碼
關於原理方面,和 dispatch_async()
方法相似,前面也提到。這裏只說一段代碼:
DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_continuation_group_async(dispatch_group_t dg, dispatch_queue_t dq,
dispatch_continuation_t dc)
{
dispatch_group_enter(dg);
dc->dc_data = dg;
_dispatch_continuation_async(dq, dc);
}
複製代碼
這段代碼中,調用了 dispatch_group_enter(dg)
方法進行標記,最終都會和 dispatch_async()
走到一樣的方法裏 _dispatch_continuation_invoke_inline()
。在裏面判斷類型爲 group,執行 task,執行結束後調用 dispatch_group_leave((dispatch_group_t)dou)
,和以前的 enter 對應。
以上是 Dispatch Queues 內容的介紹,咱們平時使用 GCD 的過程當中,60% 都是使用的以上內容。
在 iOS 8 中,Apple 爲咱們提供了新的 API,Dispatch Block
相關。雖然以前咱們能夠向 dispatch 傳遞 block 參數,做爲任務,可是這裏和以前的不同。以前常常說,使用 NSOperation
建立的任務能夠 cancel,使用 GCD 不能夠。可是在 iOS 8 以後,能夠 cancel 任務了。
建立一個 block 並執行。
- (void)dispatchBlockTest {
// 不指定優先級
dispatch_block_t dsBlock = dispatch_block_create(0, ^{
NSLog(@"test block");
});
// 指定優先級
dispatch_block_t dsQosBlock = dispatch_block_create_with_qos_class(0, QOS_CLASS_USER_INITIATED, -1, ^{
NSLog(@"test block");
});
dispatch_async(dispatch_get_main_queue(), dsBlock);
dispatch_async(dispatch_get_main_queue(), dsQosBlock);
// 直接建立並執行
dispatch_block_perform(0, ^{
NSLog(@"test block");
});
複製代碼
} ```
阻塞當前任務,等 block 執行完在繼續執行。
- (void)dispatchBlockTest {
dispatch_queue_t queue = dispatch_queue_create("com.bool.block", DISPATCH_QUEUE_SERIAL);
dispatch_block_t dsBlock = dispatch_block_create(0, ^{
NSLog(@"test block");
});
dispatch_async(queue, dsBlock);
// 等到 block 執行完
dispatch_block_wait(dsBlock, DISPATCH_TIME_FOREVER);
NSLog(@"block was finished");
}
複製代碼
block 執行完後,收到通知,執行其餘任務
- (void)dispatchBlockTest {
dispatch_queue_t queue = dispatch_queue_create("com.bool.block", DISPATCH_QUEUE_SERIAL);
dispatch_block_t dsBlock = dispatch_block_create(0, ^{
NSLog(@"test block");
});
dispatch_async(queue, dsBlock);
// block 執行完收到通知
dispatch_block_notify(dsBlock, queue, ^{
NSLog(@"block was finished,do other thing");
});
NSLog(@"execute first");
}
複製代碼
對 block 進行 cancel 操做
- (void)dispatchBlockTest {
dispatch_queue_t queue = dispatch_queue_create("com.bool.block", DISPATCH_QUEUE_SERIAL);
dispatch_block_t dsBlock1 = dispatch_block_create(0, ^{
NSLog(@"test block1");
});
dispatch_block_t dsBlock2 = dispatch_block_create(0, ^{
NSLog(@"test block2");
});
dispatch_async(queue, dsBlock1);
dispatch_async(queue, dsBlock2);
// 第二個 block 將會被 cancel,不執行
dispatch_block_cancel(dsBlock2);
}
複製代碼
Dispatch Barriers 能夠理解爲調度屏障,經常使用於多線程併發讀寫操做。例如:
@interface ViewController ()
@property (nonatomic, strong) dispatch_queue_t imageQueue;
@property (nonatomic, strong) NSMutableArray *imageArray;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.imageQueue = dispatch_queue_create("com.bool.image", DISPATCH_QUEUE_CONCURRENT);
self.imageArray = [NSMutableArray array];
}
/** 保證寫入時不會有其餘操做,寫完以後到主線程更新 UI */
- (void)addImage:(UIImage *)image {
dispatch_barrier_async(self.imageQueue, ^{
[self.imageArray addObject:image];
dispatch_async(dispatch_get_main_queue(), ^{
// update UI
});
});
}
/** 這裏的 dispatch_sync 起到了 lock 的做用 */
- (NSArray <UIImage *> *)images {
__block NSArray *imagesArray = nil;
dispatch_sync(self.imageQueue, ^{
imagesArray = [self.imageArray mutableCopy];
});
return imagesArray;
}
@end
複製代碼
轉化成圖可能好理解一些:
dispatch_barrier_async()
的原理和 dispatch_async()
差很少,只不過設置的 flags 不同:
void
dispatch_barrier_async(dispatch_queue_t dq, dispatch_block_t work)
{
dispatch_continuation_t dc = _dispatch_continuation_alloc();
// 在 dispatch_async() 中只設置了 DISPATCH_OBJ_CONSUME_BIT
uintptr_t dc_flags = DISPATCH_OBJ_CONSUME_BIT | DISPATCH_OBJ_BARRIER_BIT;
_dispatch_continuation_init(dc, dq, work, 0, 0, dc_flags);
_dispatch_continuation_push(dq, dc);
}
複製代碼
後面都是 push 到隊列中,而後,獲取任務時一個死循環,在從隊列中獲取任務一個一個執行,若是判斷 flag 爲 barrier,終止循環,則單獨執行這個任務。它後面的任務放入一個隊列,等它執行完了再開始執行。
DISPATCH_ALWAYS_INLINE
static dispatch_queue_wakeup_target_t
_dispatch_queue_drain(dispatch_queue_t dq, dispatch_invoke_context_t dic,
dispatch_invoke_flags_t flags, uint64_t *owned_ptr, bool serial_drain)
{
...
for (;;) {
...
first_iteration:
dq_state = os_atomic_load(&dq->dq_state, relaxed);
if (unlikely(_dq_state_is_suspended(dq_state))) {
break;
}
if (unlikely(orig_tq != dq->do_targetq)) {
break;
}
if (serial_drain || _dispatch_object_is_barrier(dc)) {
if (!serial_drain && owned != DISPATCH_QUEUE_IN_BARRIER) {
if (!_dispatch_queue_try_upgrade_full_width(dq, owned)) {
goto out_with_no_width;
}
owned = DISPATCH_QUEUE_IN_BARRIER;
}
next_dc = _dispatch_queue_next(dq, dc);
if (_dispatch_object_is_sync_waiter(dc)) {
owned = 0;
dic->dic_deferred = dc;
goto out_with_deferred;
}
} else {
if (owned == DISPATCH_QUEUE_IN_BARRIER) {
// we just ran barrier work items, we have to make their
// effect visible to other sync work items on other threads
// that may start coming in after this point, hence the
// release barrier
os_atomic_xor2o(dq, dq_state, owned, release);
owned = dq->dq_width * DISPATCH_QUEUE_WIDTH_INTERVAL;
} else if (unlikely(owned == 0)) {
if (_dispatch_object_is_sync_waiter(dc)) {
// sync "readers" don't observe the limit
_dispatch_queue_reserve_sync_width(dq);
} else if (!_dispatch_queue_try_acquire_async(dq)) {
goto out_with_no_width;
}
owned = DISPATCH_QUEUE_WIDTH_INTERVAL;
}
next_dc = _dispatch_queue_next(dq, dc);
if (_dispatch_object_is_sync_waiter(dc)) {
owned -= DISPATCH_QUEUE_WIDTH_INTERVAL;
_dispatch_sync_waiter_redirect_or_wake(dq,
DISPATCH_SYNC_WAITER_NO_UNLOCK, dc);
continue;
}
...
}
複製代碼
關於 dispatch_source
咱們使用的少之又少,他是 BSD 系統內核功能的包裝,常常用來監測某些事件發生。例如監測斷點的使用和取消。[這裏][https://developer.apple.com/documentation/dispatch/dispatch_source_type_constants?language=objc] 介紹了能夠監測的事件:
例如咱們能夠經過下面代碼,來監測斷點的使用和取消:
@interface ViewController ()
@property (nonatomic, strong) dispatch_source_t signalSource;
@property (nonatomic, assign) dispatch_once_t signalOnceToken;
@end
@implementation ViewController
- (void)viewDidLoad {
dispatch_once(&_signalOnceToken, ^{
dispatch_queue_t queue = dispatch_get_main_queue();
self.signalSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGSTOP, 0, queue);
if (self.signalSource) {
dispatch_source_set_event_handler(self.signalSource, ^{
// 點擊一下斷點,再取消斷點,便會執行這裏。
NSLog(@"debug test");
});
dispatch_resume(self.signalSource);
}
});
}
複製代碼
還有 diapatch_after()
就是依賴 dispatch_source()
來實現的。咱們能夠本身實現一個相似的定時器:
- (void)customTimer {
dispatch_source_t timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, DISPATCH_TARGET_QUEUE_DEFAULT);
dispatch_source_set_timer(timerSource, dispatch_time(DISPATCH_TIME_NOW, 5.0 * NSEC_PER_SEC), 2.0 * NSEC_PER_SEC, 5);
dispatch_source_set_event_handler(timerSource, ^{
NSLog(@"dispatch source timer");
});
self.signalSource = timerSource;
dispatch_resume(self.signalSource);
}
複製代碼
使用 dispatch_source
時,大體過程是這樣的:咱們建立一個 source,而後加到隊列中,並調用 dispatch_resume()
方法,便會從隊列中喚起 source,執行對應的 block。下面是一個詳細的流程圖,咱們結合這張圖來講一下:
建立一個 source 對象,過程和建立 queue 相似,因此後面一些操做,和操做 queue 很相似。
dispatch_source_t
dispatch_source_create(dispatch_source_type_t dst, uintptr_t handle,
unsigned long mask, dispatch_queue_t dq)
{
dispatch_source_refs_t dr;
dispatch_source_t ds;
dr = dux_create(dst, handle, mask)._dr;
if (unlikely(!dr)) {
return DISPATCH_BAD_INPUT;
}
// 申請內存空間
ds = _dispatch_object_alloc(DISPATCH_VTABLE(source),
sizeof(struct dispatch_source_s));
// 初始化一個隊列,而後配置參數,徹底被當作一個 queue 來處理
_dispatch_queue_init(ds->_as_dq, DQF_LEGACY, 1,
DISPATCH_QUEUE_INACTIVE | DISPATCH_QUEUE_ROLE_INNER);
ds->dq_label = "source";
ds->do_ref_cnt++; // the reference the manager queue holds
ds->ds_refs = dr;
dr->du_owner_wref = _dispatch_ptr2wref(ds);
if (slowpath(!dq)) {
dq = _dispatch_get_root_queue(DISPATCH_QOS_DEFAULT, true);
} else {
_dispatch_retain((dispatch_queue_t _Nonnull)dq);
}
ds->do_targetq = dq;
if (dr->du_is_timer && (dr->du_fflags & DISPATCH_TIMER_INTERVAL)) {
_dispatch_source_set_interval(ds, handle);
}
_dispatch_object_debug(ds, "%s", __func__);
return ds;
}
複製代碼
設置 event_handler。從源碼中看出,用的是 dispatch_continuation_t
進行綁定,和以前綁定 queue 同樣,將 block copy 了一份。後面執行的時候,拿出來用。而後將這個任務 push 到隊列裏。
void
dispatch_source_set_event_handler(dispatch_source_t ds,
dispatch_block_t handler)
{
dispatch_continuation_t dc;
// 這裏實際就是在初始化 dispatch_continuation_t
dc = _dispatch_source_handler_alloc(ds, handler, DS_EVENT_HANDLER, true);
// 通過一頓操做,將任務 push 到隊列中。
_dispatch_source_set_handler(ds, DS_EVENT_HANDLER, dc);
}
複製代碼
調用 resume 方法,執行 source。通常新建立的都是暫停狀態,這裏判斷是暫停狀態,就開始喚起。
void dispatch_resume(dispatch_object_t dou) {
DISPATCH_OBJECT_TFB(_dispatch_objc_resume, dou);
if (dx_vtable(dou._do)->do_suspend) {
dx_vtable(dou._do)->do_resume(dou._do, false);
}
}
複製代碼
最後一步,是最核心的異步,喚起任務開始執行。以前的 queue 最終也是走到這樣相似的一步,能夠看返回類型都是 dispatch_queue_wakeup_target_t
,基本是沿着 queue 的邏輯一路 copy 過來。這個方法,通過一系列判斷,保證全部的 source 都會在正確的隊列上面執行;若是隊列和任務不對應,那麼就返回正確的隊列,從新派發讓任務在正確的隊列上執行。
DISPATCH_ALWAYS_INLINE
static inline dispatch_queue_wakeup_target_t
_dispatch_source_invoke2(dispatch_object_t dou, dispatch_invoke_context_t dic,
dispatch_invoke_flags_t flags, uint64_t *owned)
{
dispatch_source_t ds = dou._ds;
dispatch_queue_wakeup_target_t retq = DISPATCH_QUEUE_WAKEUP_NONE;
// 獲取當前 queue
dispatch_queue_t dq = _dispatch_queue_get_current();
dispatch_source_refs_t dr = ds->ds_refs;
dispatch_queue_flags_t dqf;
...
// timer 事件處理
if (dr->du_is_timer &&
os_atomic_load2o(ds, ds_timer_refs->dt_pending_config, relaxed)) {
dqf = _dispatch_queue_atomic_flags(ds->_as_dq);
if (!(dqf & (DSF_CANCELED | DQF_RELEASED))) {
// timer has to be configured on the kevent queue
if (dq != dkq) {
return dkq;
}
_dispatch_source_timer_configure(ds);
}
}
// 是否安裝 source
if (!ds->ds_is_installed) {
// The source needs to be installed on the kevent queue.
if (dq != dkq) {
return dkq;
}
_dispatch_source_install(ds, _dispatch_get_wlh(),
_dispatch_get_basepri());
}
// 是否暫停,由於以前判斷過,通常不可能走到這裏
if (unlikely(DISPATCH_QUEUE_IS_SUSPENDED(ds))) {
// Source suspended by an item drained from the source queue.
return ds->do_targetq;
}
// 是否在
if (_dispatch_source_get_registration_handler(dr)) {
// The source has been registered and the registration handler needs
// to be delivered on the target queue.
if (dq != ds->do_targetq) {
return ds->do_targetq;
}
// clears ds_registration_handler
_dispatch_source_registration_callout(ds, dq, flags);
}
...
if (!(dqf & (DSF_CANCELED | DQF_RELEASED)) &&
os_atomic_load2o(ds, ds_pending_data, relaxed)) {
// 有些 source 還有未完成的數據,須要經過目標隊列上的回調進行傳送;有些 source 則須要切換到管理隊列上去。
if (dq == ds->do_targetq) {
_dispatch_source_latch_and_call(ds, dq, flags);
dqf = _dispatch_queue_atomic_flags(ds->_as_dq);
prevent_starvation = dq->do_targetq ||
!(dq->dq_priority & DISPATCH_PRIORITY_FLAG_OVERCOMMIT);
if (prevent_starvation &&
os_atomic_load2o(ds, ds_pending_data, relaxed)) {
retq = ds->do_targetq;
}
} else {
return ds->do_targetq;
}
}
if ((dqf & (DSF_CANCELED | DQF_RELEASED)) && !(dqf & DSF_DEFERRED_DELETE)) {
// 已經被取消的 source 須要從管理隊列中卸載。卸載完成後,取消的 handler 須要交付到目標隊列。
if (!(dqf & DSF_DELETED)) {
if (dr->du_is_timer && !(dqf & DSF_ARMED)) {
// timers can cheat if not armed because there's nothing left
// to do on the manager queue and unregistration can happen
// on the regular target queue
} else if (dq != dkq) {
return dkq;
}
_dispatch_source_refs_unregister(ds, 0);
dqf = _dispatch_queue_atomic_flags(ds->_as_dq);
if (unlikely(dqf & DSF_DEFERRED_DELETE)) {
if (!(dqf & DSF_ARMED)) {
goto unregister_event;
}
// we need to wait for the EV_DELETE
return retq ? retq : DISPATCH_QUEUE_WAKEUP_WAIT_FOR_EVENT;
}
}
if (dq != ds->do_targetq && (_dispatch_source_get_event_handler(dr) ||
_dispatch_source_get_cancel_handler(dr) ||
_dispatch_source_get_registration_handler(dr))) {
retq = ds->do_targetq;
} else {
_dispatch_source_cancel_callout(ds, dq, flags);
dqf = _dispatch_queue_atomic_flags(ds->_as_dq);
}
prevent_starvation = false;
}
if (_dispatch_unote_needs_rearm(dr) &&
!(dqf & (DSF_ARMED|DSF_DELETED|DSF_CANCELED|DQF_RELEASED))) {
// 須要在管理隊列進行 rearm 的
if (dq != dkq) {
return dkq;
}
if (unlikely(dqf & DSF_DEFERRED_DELETE)) {
// 若是咱們能夠直接註銷,不須要 resume
goto unregister_event;
}
if (unlikely(DISPATCH_QUEUE_IS_SUSPENDED(ds))) {
// 若是 source 已經暫停,不須要在管理隊列 rearm
return ds->do_targetq;
}
if (prevent_starvation && dr->du_wlh == DISPATCH_WLH_ANON) {
return ds->do_targetq;
}
if (unlikely(!_dispatch_source_refs_resume(ds))) {
goto unregister_event;
}
if (!prevent_starvation && _dispatch_wlh_should_poll_unote(dr)) {
_dispatch_event_loop_drain(KEVENT_FLAG_IMMEDIATE);
}
}
return retq;
}
複製代碼
還有一些其餘的方法,這裏就不介紹了。有興趣的能夠看源碼,太多了。
咱們可使用 Dispatch I/O 快速讀取一些文件,例如這樣 :
- (void)readFile {
NSString *filePath = @"/.../青花瓷.m";
dispatch_queue_t queue = dispatch_queue_create("com.bool.readfile", DISPATCH_QUEUE_SERIAL);
dispatch_fd_t fd = open(filePath.UTF8String, O_RDONLY,0);
dispatch_io_t fileChannel = dispatch_io_create(DISPATCH_IO_STREAM, fd, queue, ^(int error) {
close(fd);
});
NSMutableData *fileData = [NSMutableData new];
dispatch_io_set_low_water(fileChannel, SIZE_MAX);
dispatch_io_read(fileChannel, 0, SIZE_MAX, queue, ^(bool done, dispatch_data_t _Nullable data, int error) {
if (error == 0 && dispatch_data_get_size(data) > 0) {
[fileData appendData:(NSData *)data];
}
if (done) {
NSString *str = [[NSString alloc] initWithData:fileData encoding:NSUTF8StringEncoding];
NSLog(@"read file completed, string is :\n %@",str);
}
});
}
複製代碼
輸出結果:
ConcurrencyTest[41479:5357296] read file completed, string is :
天青色等煙雨 而我在等你
月色被打撈起 暈開告終局
複製代碼
若是讀取大文件,咱們能夠進行切片讀取,將文件分割多個片,放在異步線程中併發執行,這樣會比較快一些。
關於源碼,簡單看了一下,調度邏輯和以前的任務相似。而後讀寫操做,是調用的一些底層接口實現,這裏就偷懶一下不詳細說了。使用 Dispatch I/O,多數狀況下是爲了併發讀取一個大文件,提升讀取速度。
上面已經講了概覽圖中的大部分東西,還有一些未講述,這裏簡單描述一下:
dispatch_object。GCD 用 C 函數實現的對象,不能經過集成 dispatch 類實現,也不能用 alloc 方法初始化。GCD 針對 dispatch_object 提供了一些接口,咱們使用這些接口能夠處理一些內存事件、取消和暫停操做、定義上下文和處理日誌相關工做。dispatch_object 必需要手動管理內存,不遵循垃圾回收機制。
dispatch_time。在 GCD 中使用的時間對象,能夠建立自定義時間,也可使用 DISPATCH_TIME_NOW
、DISPATCH_TIME_FOREVER
這兩個系統給出的時間。
以上爲 GCD 相關知識,此次使用的源碼版本爲最新版本 —— 912.30.4.tar.gz,和以前看的版本代碼差距很大,由於代碼量的增長,新版本代碼比較亂,不過基本原理仍是差很少的。曾經我一度認爲,最上面的是最新版本...
Operations 也是咱們在併發編程中經常使用的一套 API,根據 官方文檔 劃分的結構以下圖:
其中 NSBlockOperation
和 NSInvocationOperation
是基於 NSOperation
的子類化實現。相對於 GCD,Operations 的原理要稍微好理解一些,下面就將用法和原理介紹一下。
每個 operation 能夠認爲是一個 task。NSOperation
本事是一個抽象類,使用前需子類化。幸運的是,Apple 爲咱們實現了兩個子類:NSInvocationOperation
、NSBlockOperation
。咱們也能夠本身去定義一個 operation。下面介紹一下基本使用:
建立一個 NSInvocationOperation
對象並在當前線程執行.
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(log) object:nil];
[invocationOperation start];
複製代碼
建立一個 NSBlockOperation
對象並執行 (每一個 block 不必定會在當前線程,也不必定在同一線程執行).
NSBlockOperation *blockOpeartion = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"block operation");
}];
// 能夠添加多個 block
[blockOpeartion addExecutionBlock:^{
NSLog(@"other block opeartion");
}];
[blockOpeartion start];
複製代碼
自定義一個 Operation。當咱們不須要操做狀態的時候,只須要實現 main()
方法便可。須要操做狀態的後面再說.
@interface BLOpeartion : NSOperation
@end
@implementation BLOpeartion
- (void)main {
NSLog(@"BLOperation main method");
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
BLOperation *blOperation = [BLOperation new];
[blOperation start];
}
複製代碼
每一個 operation 之間設置依賴.
NSBlockOperation *blockOpeartion1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"block operation1");
}];
NSBlockOperation *blockOpeartion2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"block operation2");
}];
// 2 須要在 1 執行完以後再執行。
[blockOpeartion2 addDependency:blockOpeartion1];
複製代碼
與隊列相關的使用,後面再說.
NSOperation
內置了一個強大的狀態機,一個 operation 從初始化到執行完畢這一輩子命週期,對應了各類狀態。下面是在 WWDC 2015 Advanced NSOperations 出現的一張圖:
operation 一開始是 Pending 狀態,表明即將進入 Ready;進入 Ready 以後,表明任務能夠執行;而後進入 Executing 狀態;最後執行完成,進入 Finished 狀態。過程當中,除了 Finished 狀態,在其餘幾個狀態中均可以進行 Cancelled。
NSOperation
並無開源。可是 swift 開源了,在 swift 中它叫 Opeartion
,咱們能夠在 這裏 找到他的源碼。我這裏 copy 了一份:
open class Operation : NSObject {
let lock = NSLock()
internal weak var _queue: OperationQueue?
// 默認幾個狀態都是 false
internal var _cancelled = false
internal var _executing = false
internal var _finished = false
internal var _ready = false
// 用一個集合來保存依賴它的對象
internal var _dependencies = Set<Operation>()
// 初始化一些 dispatch_group 對象,來管理 operation 以及其依賴對象的 執行。
#if DEPLOYMENT_ENABLE_LIBDISPATCH
internal var _group = DispatchGroup()
internal var _depGroup = DispatchGroup()
internal var _groups = [DispatchGroup]()
#endif
public override init() {
super.init()
#if DEPLOYMENT_ENABLE_LIBDISPATCH
_group.enter()
#endif
}
internal func _leaveGroups() {
// assumes lock is taken
#if DEPLOYMENT_ENABLE_LIBDISPATCH
_groups.forEach() { $0.leave() }
_groups.removeAll()
_group.leave()
#endif
}
// 默認實現的 start 方法中,執行 main 方法,線程安全,下同。執行先後設置 _executing。
open func start() {
if !isCancelled {
lock.lock()
_executing = true
lock.unlock()
main()
lock.lock()
_executing = false
lock.unlock()
}
finish()
}
// 默認實現的 finish 方法中,標記 _finished 狀態。
internal func finish() {
lock.lock()
_finished = true
_leaveGroups()
lock.unlock()
if let queue = _queue {
queue._operationFinished(self)
}
...
}
// main 方法默認空,須要子類去實現。
open func main() { }
// 調用 cancel 方法後,只是標記狀態,具體操做在 main 中,調用 cancel 後也被認爲是 finish。
open func cancel() {
lock.lock()
_cancelled = true
lock.unlock()
}
/** 幾個狀態的 get 方法,省略 */
...
// 是否爲異步任務,默認爲 false。這個方法在 OC 中永遠不會去實現
open var isAsynchronous: Bool {
return false
}
// 設置依賴,即將 operation 放到集合中
open func addDependency(_ op: Operation) {
lock.lock()
_dependencies.insert(op)
op.lock.lock()
#if DEPLOYMENT_ENABLE_LIBDISPATCH
_depGroup.enter()
op._groups.append(_depGroup)
#endif
op.lock.unlock()
lock.unlock()
}
...
// 默認隊列優先級爲 normal
open var queuePriority: QueuePriority = .normal
public var completionBlock: (() -> Void)?
open func waitUntilFinished() {
#if DEPLOYMENT_ENABLE_LIBDISPATCH
_group.wait()
#endif
}
// 線程優先級
open var threadPriority: Double = 0.5
/// - Note: Quality of service is not directly supported here since there are not qos class promotions available outside of darwin targets.
open var qualityOfService: QualityOfService = .default
open var name: String?
internal func _waitUntilReady() {
#if DEPLOYMENT_ENABLE_LIBDISPATCH
_depGroup.wait()
#endif
_ready = true
}
}
複製代碼
代碼很簡單,具體過程能夠直接看註釋,就不另說了。除此以外,咱們能夠看出,Operation
總不少方法造做都加了鎖,說明這個類是線程安全的,當咱們對 NSOperation
進行子類化時,重寫方法要注意線程暗轉問題。
NSOperation
的不少花式操做,都是結合着 NSOperationQueue
進行的。咱們在使用的時候,也是二者結合着使用。下面對其進行詳細分析。
start
方法去執行,operation 會自動執行。suspended
屬性來暫停或者啓動還未執行的 operation。-[cancelAllOperations]
方法來取消隊列中的任務。mainQueue
方法來回到主隊列(主線程);能夠經過 currentQueue
方法來獲取當前隊列。使用例子:
- (void)testOperationQueue {
NSOperationQueue *operationQueue = [NSOperationQueue new];
// 設置最大併發數量爲 3
[operationQueue setMaxConcurrentOperationCount:3];
NSInvocationOperation *invocationOpeartion = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(log) object:nil];
[operationQueue addOperation:invocationOpeartion];
[operationQueue addOperationWithBlock:^{
NSLog(@"block operation");
// 回到主線程執行任務
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"execute in main thread");
}];
}];
// 暫停還未開始執行的任務
operationQueue.suspended = YES;
// 取消全部任務
[operationQueue cancelAllOperations];
}
複製代碼
有一個問題要特別說明一下:
NSOperationQueue
和 GCD 中的隊列不一樣。GCD 中的隊列是遵循 FIFO 原則,先加入隊列的先執行;NSOperationQueue
中的任務,根據誰先進入到 Ready
狀態,誰先執行。若是有多個任務同時達到 Ready
狀態,那麼根據優先級來執行。
例以下面的任務中,4 先到達了 Ready
狀態,4 先執行。並非按照 1,2,3... 順序執行。
咱們依然是在 swift 中找到相關源碼,而後來進行分析:
// 默認最大併發數量爲 int 最大值
public extension OperationQueue {
public static let defaultMaxConcurrentOperationCount: Int = Int.max
}
// 使用一個 list 來保存各個優先級的 operation。調用其中的方法對 operation 進行增刪等操做。
internal struct _OperationList {
var veryLow = [Operation]()
var low = [Operation]()
var normal = [Operation]()
var high = [Operation]()
var veryHigh = [Operation]()
var all = [Operation]()
mutating func insert(_ operation: Operation) { ... }
mutating func remove(_ operation: Operation) { ... }
mutating func dequeue() -> Operation? { ... }
var count: Int {
return all.count
}
func map<T>(_ transform: (Operation) throws -> T) rethrows -> [T] {
return try all.map(transform)
}
}
open class OperationQueue: NSObject {
...
// 使用一個信號量的來控制併發數量
var __concurrencyGate: DispatchSemaphore?
var __underlyingQueue: DispatchQueue? {
didSet {
let key = OperationQueue.OperationQueueKey
oldValue?.setSpecific(key: key, value: nil)
__underlyingQueue?.setSpecific(key: key, value: Unmanaged.passUnretained(self))
}
}
...
internal var _underlyingQueue: DispatchQueue {
lock.lock()
if let queue = __underlyingQueue {
lock.unlock()
return queue
} else {
...
// 信號量的值根據最大併發數量來肯定。每當執行一個任務,wait 信號量減一,signal 信號量加一,當信號量爲0時,一直等待,直接大於 0 纔會正常執行。
if maxConcurrentOperationCount == 1 {
attr = []
__concurrencyGate = DispatchSemaphore(value: 1)
} else {
attr = .concurrent
if maxConcurrentOperationCount != OperationQueue.defaultMaxConcurrentOperationCount {
__concurrencyGate = DispatchSemaphore(value:maxConcurrentOperationCount)
}
}
let queue = DispatchQueue(label: effectiveName, attributes: attr)
if _suspended {
queue.suspend()
}
__underlyingQueue = queue
lock.unlock()
return queue
}
}
#endif
...
// 出隊列,每一個任務執行時拿出隊列執行
internal func _dequeueOperation() -> Operation? {
lock.lock()
let op = _operations.dequeue()
lock.unlock()
return op
}
open func addOperation(_ op: Operation) {
addOperations([op], waitUntilFinished: false)
}
// 主要執行方法。先判斷 operation 是否 ready,處於 ready 後判斷是否 cancel。沒有 cancel 則執行。
internal func _runOperation() {
if let op = _dequeueOperation() {
if !op.isCancelled {
op._waitUntilReady()
if !op.isCancelled {
op.start()
}
}
}
}
// 將任務加到隊列中。若是不指定任務優先級,執行的還快一些。不然須要對不一樣優先級進行劃分,而後執行
open func addOperations(_ ops: [Operation], waitUntilFinished wait: Bool) {
#if DEPLOYMENT_ENABLE_LIBDISPATCH
var waitGroup: DispatchGroup?
if wait {
waitGroup = DispatchGroup()
}
#endif
lock.lock()
// 將 operation 依依加入 list,根據優先級保存到不一樣數組中
ops.forEach { (operation: Operation) -> Void in
operation._queue = self
_operations.insert(operation)
}
lock.unlock()
// 遍歷執行,使用了 diapatch group,控制 enter 和 leave
ops.forEach { (operation: Operation) -> Void in
#if DEPLOYMENT_ENABLE_LIBDISPATCH
if let group = waitGroup {
group.enter()
}
// 經過信號量來控制併發數量
let block = DispatchWorkItem(flags: .enforceQoS) { () -> Void in
if let sema = self._concurrencyGate {
sema.wait()
self._runOperation()
sema.signal()
} else {
self._runOperation()
}
if let group = waitGroup {
group.leave()
}
}
_underlyingQueue.async(group: queueGroup, execute: block)
#endif
}
#if DEPLOYMENT_ENABLE_LIBDISPATCH
if let group = waitGroup {
group.wait()
}
#endif
}
internal func _operationFinished(_ operation: Operation) { ... }
open func addOperation(_ block: @escaping () -> Swift.Void) { ... }
// 返回值不必定準確
open var operations: [Operation] { ... }
// 返回值不必定準確
open var operationCount: Int { ... }
open var maxConcurrentOperationCount: Int = OperationQueue.defaultMaxConcurrentOperationCount
// suppend 屬性的 get & set 方法。默認不暫停
internal var _suspended = false
open var isSuspended: Bool { ... }
...
// operation 在獲取系統資源時的優先級
open var qualityOfService: QualityOfService = .default
// 依次調用每一個 operation 的 cancel 方法
open func cancelAllOperations() { ... }
open func waitUntilAllOperationsAreFinished() {
#if DEPLOYMENT_ENABLE_LIBDISPATCH
queueGroup.wait()
#endif
}
static let OperationQueueKey = DispatchSpecificKey<Unmanaged<OperationQueue>>()
// 經過使用 GCD 中的 getSpecific 方法獲取當前隊列
open class var current: OperationQueue? {
#if DEPLOYMENT_ENABLE_LIBDISPATCH
guard let specific = DispatchQueue.getSpecific(key: OperationQueue.OperationQueueKey) else {
if _CFIsMainThread() {
return OperationQueue.main
} else {
return nil
}
}
return specific.takeUnretainedValue()
#else
return nil
#endif
}
// 定義主隊列,最大併發數量爲 1,獲取主隊列時將這個值返回
private static let _main = OperationQueue(_queue: .main, maxConcurrentOperations: 1)
open class var main: OperationQueue { ... }
}
複製代碼
代碼很長,可是簡單,能夠直接經過註釋來理解了。這裏屢一下:
Ready
狀態且沒被 Cancel
的依次執行。concurrencyGate
這個信號量來控制併發數量。每當執行一個任務,wait 信號量減一,signal 信號量加一,當信號量爲0時,一直等待,直接大於 0 纔會正常執行。以前說了自定義普通的 NSOperation
,只須要重寫 main
方法就能夠了,可是由於咱們沒有處理併發狀況,線程執行結束操做,KVO 機制,因此這種普通的不建議用來作併發任務。下面講一下如何自定義並行的 NSOperation
。
必需要實現的一些方法:
start
方法,在你想要執行的線程中調用此方法。不須要調用 super 方法。main
方法,在 start
方法中調用,任務主體。isExecuting
方法,是否正在執行,要實現 KVO 機制。isConcurrent
方法,已經棄用,由 isAsynchronous
來代替。isAsynchronous
方法,在併發任務中,須要返回 YES。@interface BLOperation ()
@property (nonatomic, assign) BOOL executing;
@property (nonatomic, assign) BOOL finished;
@end
@implementation BLOperation
@synthesize executing;
@synthesize finished;
- (instancetype)init {
self = [super init];
if (self) {
executing = NO;
finished = NO;
}
return self;
}
- (void)start {
if ([self isCancelled]) {
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)main {
NSLog(@"main begin");
@try {
@autoreleasepool {
NSLog(@"custom operation");
NSLog(@"currentThread = %@", [NSThread currentThread]);
NSLog(@"mainThread = %@", [NSThread mainThread]);
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
} @catch (NSException *exception) {
NSLog(@"exception is %@", exception);
}
NSLog(@"main end");
}
- (BOOL)isExecuting {
return executing;
}
- (BOOL)isFinished {
return finished;
}
- (BOOL)isAsynchronous {
return YES;
}
@end
複製代碼
關於 NSBlockOpeartion
,主要實現了 main
方法,而後用一個數組保存加進來的其餘 block,源碼以下:
open class BlockOperation: Operation {
typealias ExecutionBlock = () -> Void
internal var _block: () -> Void
internal var _executionBlocks = [ExecutionBlock]()
public init(block: @escaping () -> Void) {
_block = block
}
override open func main() {
lock.lock()
let block = _block
let executionBlocks = _executionBlocks
lock.unlock()
block()
executionBlocks.forEach { $0() }
}
open func addExecutionBlock(_ block: @escaping () -> Void) {
lock.lock()
_executionBlocks.append(block)
lock.unlock()
}
open var executionBlocks: [() -> Void] {
lock.lock()
let blocks = _executionBlocks
lock.unlock()
return blocks
}
}
複製代碼
關於 NSOperation
的相關東西,到此結束。
相對於 API 的使用和基本原理的瞭解,我認爲最重要的仍是這一部分。畢竟咱們仍是要拿這些東西來開發的。併發編程中有不少坑,這裏簡單介紹一些。
咱們都知道,NSNotification
在哪一個線程 post,最終就會在哪一個線程執行。若是咱們不是在主線程 post 的,可是卻在主線程接收的,並且咱們指望 selector 在主線程執行。這時候咱們須要注意下,在 selector 須要 dispatch 到主線程才能夠。固然你也可使用 addObserverForName:object:queue:usingBlock:
來指定執行 block 的 queue。
@implementation BLPostNotification
- (void)postNotification {
dispatch_queue_t queue = dispatch_queue_create("com.bool.post.notification", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
// 從非主線程發送通知 (通知名字最好定義成一個常量)
[[NSNotificationCenter defaultCenter] postNotificationName:@"downloadImage" object:nil];
});
}
@end
@implementation ImageViewController
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(show) name:@"downloadImage" object:nil];
}
- (void)showImage {
// 須要 dispatch 到主線程更新 UI
dispatch_async(dispatch_get_main_queue(), ^{
// update UI
});
}
@end
複製代碼
使用 NSTimer
時,在哪一個線程生成的 timer,就在哪一個線程銷燬,不然會有意想不到的結果。官方這樣描述的:
However, for a repeating timer, you must invalidate the timer object yourself by calling its invalidate method. Calling this method requests the removal of the timer from the current run loop; as a result, you should always call the invalidate method from the same thread on which the timer was installed.
@interface BLTimerTest ()
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation BLTimerTest
- (instancetype)init {
self = [super init];
if (self) {
_queue = dispatch_queue_create("com.bool.timer.test", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)installTimer {
dispatch_async(self.queue, ^{
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0f repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"test timer");
}];
});
}
- (void)clearTimer {
dispatch_async(self.queue, ^{
if ([self.timer isValid]) {
[self.timer invalidate];
self.timer = nil;
}
});
}
@end
複製代碼
在開發中,咱們常用 dispatch_once
,可是遞歸調用會形成死鎖。例以下面這樣:
- (void)dispatchOnceTest {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self dispatchOnceTest];
});
}
複製代碼
至於爲何會死鎖,上文介紹 Dispatch Once 的時候已經說明了,這裏就很少作介紹了。提醒一下使用的時候要注意,不要形成遞歸調用。
在使用 dispatch_group
的時候,dispatch_group_enter(taskGroup)
和 dispatch_group_leave(taskGroup)
必定要成對,不然也會出現崩潰。大多數狀況下咱們都會注意,可是有時候可能會疏忽。例如多層 for loop 時 :
- (void)testDispatchGroup {
NSString *path = @"";
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *folderList = [fileManager contentsOfDirectoryAtPath:path error:nil];
dispatch_group_t taskGroup = dispatch_group_create();
for (NSString *folderName in folderList) {
dispatch_group_enter(taskGroup);
NSString *folderPath = [@"path" stringByAppendingPathComponent:folderName];
NSArray *fileList = [fileManager contentsOfDirectoryAtPath:folderPath error:nil];
for (NSString *fileName in fileList) {
dispatch_async(_queue, ^{
// 異步任務
dispatch_group_leave(taskGroup);
});
}
}
}
複製代碼
上面的 dispatch_group_enter(taskGroup)
在第一層 for loop 中,而 dispatch_group_leave(taskGroup)
在第二層 for loop 中,二者的關係是一對多,很容形成崩潰。有時候嵌套層級太多,很容易忽略這個問題。
關於 iOS 併發編程,就總結到這裏。後面若是有一些 best practices 我會更新進來。另外,由於文章比較長,可能會出現一個錯誤,歡迎指正,我會對此加以修改。