作者:bool周 原文連結:我所理解的 iOS 併發程式設計
無論在哪個平臺,併發程式設計都是一個讓人頭疼的問題。慶幸的是,相對於服務端,客戶端的併發程式設計簡單了許多。這篇文章主要講述一些基於 iOS 平臺的一些併發程式設計相關東西,我寫部落格習慣於先介紹原理,後介紹用法,畢竟對於 API 的使用,官網有更好的文件。
一些原理性的東西
為了便於理解,這裡先解釋一些相關概念。如果你對這些概念已經很熟悉,可以直接跳過。
1.程式
從作業系統定義上來說,程式就是系統進行資源分配和排程的基本單位,系統建立一個執行緒後,會為其分配對應的資源。在 iOS 系統中,程式可以理解為就是一個 App。iOS 並沒有提供可以建立程式的 API,即使你呼叫 fork()
函式,也不能建立新的程式。所以,本文所說的併發程式設計,都是針對執行緒來說的。
2.執行緒
執行緒是程式執行流的最小單元。一般情況下,一個程式會有多個執行緒,或者至少有一個執行緒。一個執行緒有建立、就緒、執行、阻塞和死亡五種狀態。執行緒可以共享程式的資源,所有的問題也是因為共享資源引起的。
3.併發
作業系統引入執行緒的概念,是為了使過個 CPU 更好的協調執行,充分發揮他們的並行處理能力。例如在 iOS 系統中,你可以在主執行緒中進行 UI 操作,然後另啟一些執行緒來處理與 UI 操作無關的事情,兩件事情並行處理,速度比較快。這就是併發的大致概念。
4.時間片
按照 wiki 上面解釋:是分時作業系統分配給每個正在執行的程式微觀上的一段CPU時間(在搶佔核心中是:從程式開始執行直到被搶佔的時間)。執行緒可以被認為是 ”微程式“,因此這個概念也可以用到執行緒方面。
一般作業系統使用時間片輪轉演算法進行排程,即每次排程時,總是選擇就緒佇列的隊首程式,讓其在CPU上執行一個系統預先設定好的時間片。一個時間片內沒有完成執行的程式,返回到緒佇列末尾重新排隊,等待下一次排程。不同的作業系統,時間片的範圍不一致,一般都是毫秒(ms)級別。
4.死鎖
死鎖是由於多個執行緒(程式)在執行過程中,因為爭奪資源而造成的互相等待現象,你可以理解為卡主了。產生死鎖的必要條件有四個:
- 互斥條件 : 指程式對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個程式佔用。如果此時還有其它程式請求資源,則請求者只能等待,直至佔有資源的程式用畢釋放。
- 請求和保持條件 : 指程式已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它程式佔有,此時請求程式阻塞,但又對自己已獲得的其它資源保持不放。
- 不可剝奪條件 : 指程式已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。
- 環路等待條件 : 指在發生死鎖時,必然存在一個程式——資源的環形鏈,即程式集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1佔用的資源;P1正在等待P2佔用的資源,……,Pn正在等待已被P0佔用的資源。
為了便於理解,這裡舉一個例子:一座橋,同一時間只允許一輛車經過(互斥)。兩輛車 A,B 從橋的兩端開上橋,走到橋的中間。此時 A 車不肯退(不可剝奪),又想佔用 B 車所佔據的道路;B 車此時也不肯退,又想佔用 A 車所佔據的道路(請求和保持)。此時,A 等待 B 佔用的資源,B 等待 A 佔用的資源(環路等待),兩車僵持下去,就形成了死鎖現象。
5.執行緒安全
當多個執行緒同時訪問一塊共享資源(例如資料庫),因為時序性問題,會導致資料錯亂,這就是執行緒不安全。例如資料庫中某個整形欄位的 value 為 0,此時兩個執行緒同時對其進行寫入操作,執行緒 A 拿到原值為 0,加一後變為 1;執行緒 B 並不是在 A 加完後拿的,而是和 A 同時拿的,加完後也是 1,加了兩次,理想值應該為 2,但是資料庫中最終值卻是 1。實際開發場景可能要比這個複雜的多。
所謂的執行緒安全,可以理解為在多個執行緒操作(例如讀寫操作)這部分資料時,不會出現問題。
Lock
因為執行緒共享程式資源,在併發情況下,就會出現執行緒安全問題。為了解決此問題,就出現了鎖這個概念。在多執行緒環境下,當你訪問一些共享資料時,拿到訪問許可權,給資料加鎖,在這期間其他執行緒不可訪問,直到你操作完之後進行解鎖,其他執行緒才可以對其進行操作。
iOS 提供了多種鎖,ibireme 大神的 這篇文章 對這些鎖進行了效能分析,我這裡直接把圖 cp 過來了:
下面針對這些鎖,逐一分析。
1.OSSpinLock
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);
複製程式碼
2.dispatch_semaphore
dispatch_semaphore 並不屬於鎖,而是訊號量。兩者的區別如下:
- 鎖是用於執行緒互斥操作,一個執行緒鎖住了某個資源,其他執行緒都無法訪問,直到整個執行緒解鎖;訊號量用於執行緒同步,一個執行緒完成了某個動作通過訊號量告訴別的執行緒,別的執行緒再進行操作。
- 鎖的作用域是執行緒之間;訊號量的作用域是執行緒和程式之間。
- 訊號量有時候可以充當鎖的作用,初次之前還有其他作用。
- 如果轉化為數值,鎖可以認為只有 0 和 1;訊號量可以大於零和小於零,有多個值。
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 處理。- 如果傳入的是 DISPATCH_TIME_NOW 引數,將訊號量加 1 並立即返回。
- 如果傳入的是一個超時時間,呼叫系統的
semaphore_timedwait
方法進行等待,直至超時。 - 如果傳入的是 DISPATCH_TIME_FOREVER 引數,呼叫系統的
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);
}
複製程式碼
- 現將訊號量加 1,大於 0 直接返回。
- 小於 0 返回
_dispatch_semaphore_signal_slow
,這個方法的作用是呼叫核心的 semaphore_signal 函式喚醒訊號量,然後返回 1。
3.pthread_mutex
Pthreads 是 POSIX Threads 的縮寫。pthread_mutex
屬於互斥鎖,即嘗試加鎖失敗後悔阻塞執行緒並睡眠,會進行上下文切換。鎖的型別主要有三種:PTHREAD_MUTEX_NORMAL
、PTHREAD_MUTEX_ERRORCHECK
、PTHREAD_MUTEX_RECURSIVE
。
- PTHREAD_MUTEX_NORMAL,普通鎖,當一個執行緒加鎖以後,其餘請求鎖的執行緒將形成一個等待佇列,並在解鎖後按優先順序獲得鎖。這種鎖策略保證了資源分配的公平性。
- PTHREAD_MUTEX_ERRORCHECK,檢錯鎖,如果同一個執行緒請求同一個鎖,則返回 EDEADLK。否則和 PTHREAD_MUTEX_NORMAL 相同。
- 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); // 釋放鎖
複製程式碼
4.NSLock
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];
複製程式碼
4.NSCondition & NSConditionLock
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 注意:
- 初始化 NSConditionLock 會設定一個 condition,只有滿足這個 condition 才能加鎖。
-[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 使用修改後的條件,加鎖成功。
5. NSRecursiveLock
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);
}
複製程式碼
6. @synchronized
@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
前面也說了,pthreads 是 POSIX Threads 的縮寫。這個東西一般我們用不到,這裡簡單介紹一下。Pthreads 是POSIX的執行緒標準,定義了建立和操縱執行緒的一套API。實現POSIX 執行緒標準的庫常被稱作Pthreads,一般用於Unix-like POSIX 系統,如Linux、 Solaris。
NSThread
NSThread
是對核心 mach kernel 中的 mach thread 的封裝,一個 NSThread
物件就是一個執行緒。使用頻率比較低,除了 API 的使用,沒什麼可講的。如果你已經熟悉這些 API,可以跳過這一節了。
1.初始化執行緒執行一個 task
使用初始化方法初始化一個 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;
複製程式碼
2.在主執行緒執行一個 task
/** 說一下最後一個引數,這裡你至少指定一個 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;
複製程式碼
3.在其他執行緒執行一個 task
/** 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
複製程式碼
4.在後臺執行緒執行一個 task
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
複製程式碼
5.獲取當前執行緒
@property (class, readonly, strong) NSThread *currentThread;
複製程式碼
使用執行緒相關方法時,記得設定好 name,方便後面除錯。同時也設定好優先順序等其他引數。
performSelector: 系列方法已經不太安全,慎用。
Grand Central Dispatch (GCD)
GCD 是基於 C 實現的一套 API,而且是開源的,如果有興趣,可以在 這裡 down 一份原始碼研究一下。GCD 是由系統幫我們處理多執行緒排程,很是方便,也是使用頻率最高的。這一章節主要講解一下 GCD 的原理和使用。
在講解之前,我們先有個概覽,看一下 GCD 為我們提供了那些東西:
系統所提供的 API,完全可以滿足我們日常開發需求了。下面就根據這些模組分別講解一下。
1. Dispatch Queue
GCD 為我們提供了兩類佇列,序列佇列 和 並行佇列。兩者的區別是:
- 序列佇列中,按照 FIFO 的順序執行任務,前面一個任務執行完,後面一個才開始執行。
- 並行佇列中,也是按照 FIFO 的順序執行任務,只要前一個被拿去執行,繼而後面一個就開始執行,後面的任務無需等到前面的任務執行完再開始執行。
除此之外,還要解釋一個容易混淆的概念,併發和並行:
- 併發:是指單獨部分可以同時執行,但是需要系統決定怎樣發生。
- 並行:兩個任務互不干擾,同時執行。單核裝置,系統需要通過切換上下文來實現併發;多核裝置,系統可以通過並行來執行併發任務。
最後,還有一個概念,同步和非同步:
- 同步 : 同步執行的任務會阻塞當前執行緒。
- 非同步 : 非同步執行的任務不會阻塞當前執行緒。是否開啟新的執行緒,由系統管理。如果當前有空閒的執行緒,使用當前執行緒執行這個非同步任務;如果沒有空閒的執行緒,而且執行緒數量沒有達到系統最大,則開啟新的執行緒;如果執行緒數量已經達到系統最大,則需要等待其他執行緒中任務執行完畢。
佇列
我們使用時,一般使用這幾個佇列:
-
主佇列 - dispatch_get_main_queue :一個特殊的序列佇列。在 GCD 中,方法主佇列中的任務都是在主執行緒執行。當我們更新 UI 時想 dispatch 到主執行緒,可以使用這個佇列。
- (void)viewDidLoad { [super viewDidLoad]; dispatch_async(dispatch_get_main_queue(), ^{ // UI 相關操作 }); 複製程式碼
} ```
-
全域性並行佇列 - dispatch_get_global_queue : 系統提供的一個全域性並行佇列,我們可以通過指定引數,來獲取不同優先順序的佇列。系統提供了四個優先順序,所以也可以認為系統為我們提供了四個並行佇列,分別為 :
- DISPATCH_QUEUE_PRIORITY_HIGH
- DISPATCH_QUEUE_PRIORITY_DEFAULT
- DISPATCH_QUEUE_PRIORITY_LOW
- DISPATCH_QUEUE_PRIORITY_BACKGROUND
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 最終都會被放到系統全域性佇列和執行緒池中,後面會講這部分原理。盜用一張經典圖:
同步 VS 非同步
我們大多數情況下,都是使用 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()
方法。 - 然後進行初步判斷,多數情況下,我們是不會傳佇列型別的,都是穿 NULL,所以這裡是個 slowpath。如果傳了引數,但是不是規定的佇列型別,系統會認為你是個智障,並丟擲錯誤。
- 然後初始化一些配置項。主要是 target_queue,overcommit 項和 qos。target_queue 是依賴的目標佇列,像任何佇列提交的任務(block),最終都會放到目標佇列中執行;支援 overcommit 時,每當想佇列提交一個任務時,都會開一個新的執行緒處理,這樣是為了避免單一執行緒任務太多而過載;qos 是佇列優先順序,之前已經說過。
- 然後進入判斷分支。普通的序列佇列的目標佇列,就是一個支援 overcommit 的全域性佇列(對應 else 分支);當前 tq 物件的引用計數為 DISPATCH_OBJECT_GLOBAL_REFCNT (永遠不會釋放)時,且還沒有目標佇列時,才可以設定 overcommit 項,而且當優先順序為 DISPATCH_QOS_UNSPECIFIED 時,需要重置 tq (對應 if 分支);其他情況(else if 分支)。
- 然後配置佇列的標識,以方便在除錯時找到自己的那個佇列。
- 使用
_dispatch_object_alloc
方法申請一個 dispatch_queue_t 物件空間,dq。 - 根據傳入的資訊(並行 or 序列;活躍 or 非活躍)來初始化這個佇列。並行佇列的 width 會設定為
DISPATCH_QUEUE_WIDTH_MAX
即最大,不設限;序列的會設定為 1。 - 將上面獲得配置項,目標佇列,是否支援 overcommit,優先順序和 dq 繫結。
- 返回這個佇列。返回去還輸出了一句資訊,便於除錯。
非同步執行
這個版本非同步執行的程式碼,因為方法拆分很多,所以顯得很亂。原始碼如下:
/** 開發者呼叫 */
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。 - 然後經過了幾個層次的呼叫,主要為了區分並行還是序列。
- 如果是序列(這種情況比較常見,所以是 fastpath),直接就 dx_push 了,其實就是講任務追加到一個連結串列裡面。
- 如果是並行,需要做重定向。之前我們說過,放到佇列中的任務,最終都會以各種形式追加到目標佇列裡面。在
_dispatch_async_f_redirect
方法中,重新尋找依賴目標佇列,然後追加過去。 - 經過一系列呼叫,我們會在
_dispatch_continuation_invoke_inline
方法裡區分序列還是並行。因為這個方法會被頻繁呼叫,所以定義成了行內函數。對於序列佇列,我們使用訊號量控制,執行前訊號量置為 wait,執行完畢後傳送 singal;對於排程組,我們會在執行完之後呼叫dispatch_group_leave
。 - 底層的執行緒池,是使用 pthread 維護的,所以最終都會使用 pthread 來處理這些任務。
同步執行
同步執行,相對來說比較簡單,原始碼如下 :
/** 開發者呼叫 */
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
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 任務開始執行。
dispatch_once
如果我們有一段程式碼,在 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
有時候沒有時序性依賴的時候,我們會用 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];
});
}
複製程式碼
進行替換是需要注意幾個問題:
- 任務之間沒有時序性依賴,誰先執行都可以。
- 一般在併發佇列,併發執行任務時,才替換。序列佇列替換沒有意義。
- 如果陣列中資料很少,或者每個任務執行時間很短,替換也沒有意義。強行進行併發的消耗,可能比使用 for loop 還要多,並不能得到優化。
至於原理,就不大篇幅講了。大概是這個樣子:這個方法是同步的,會阻塞當前執行緒,直到所有的 block 任務都完成。如果提交到併發佇列,每個任務執行順序是不一定的。
更多時候,我們執行下載任務,並不希望阻塞當前執行緒,這時我們可以使用 dispatch_group
。
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% 都是使用的以上內容。
2. Dispatch Block
在 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); } 複製程式碼
3. Dispatch Barriers
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;
}
...
}
複製程式碼
4. Dispatch Source
關於 dispatch_source
我們使用的少之又少,他是 BSD 系統核心功能的包裝,經常用來監測某些事件發生。例如監測斷點的使用和取消。[這裡][https://developer.apple.com/documentation/dispatch/dispatch_source_type_constants?language=objc] 介紹了可以監測的事件:
- DISPATCH_SOURCE_TYPE_DATA_ADD : 自定義事件
- DISPATCH_SOURCE_TYPE_DATA_OR : 自定義事件
- DISPATCH_SOURCE_TYPE_MACH_RECV : MACH 埠接收事件
- DISPATCH_SOURCE_TYPE_MACH_SEND : MACH 埠傳送事件
- DISPATCH_SOURCE_TYPE_PROC : 程式相關事件
- DISPATCH_SOURCE_TYPE_READ : 檔案讀取事件
- DISPATCH_SOURCE_TYPE_SIGNAL : 訊號相關事件
- DISPATCH_SOURCE_TYPE_TIMER : 定時器相關事件
- DISPATCH_SOURCE_TYPE_VNODE : 檔案屬性修改事件
- DISPATCH_SOURCE_TYPE_WRITE : 檔案寫入事件
- DISPATCH_SOURCE_TYPE_MEMORYPRESSURE : 記憶體壓力事件
例如我們可以通過下面程式碼,來監測斷點的使用和取消:
@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; } 複製程式碼
還有一些其他的方法,這裡就不介紹了。有興趣的可以看原始碼,太多了。
5. Dispatch I/O
我們可以使用 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,多數情況下是為了併發讀取一個大檔案,提高讀取速度。
6. Other
上面已經講了概覽圖中的大部分東西,還有一些未講述,這裡簡單描述一下:
-
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
Operations 也是我們在併發程式設計中常用的一套 API,根據 官方文件 劃分的結構如下圖:
其中 NSBlockOperation
和 NSInvocationOperation
是基於 NSOperation
的子類化實現。相對於 GCD,Operations 的原理要稍微好理解一些,下面就將用法和原理介紹一下。
1. NSOperation
基本使用
每一個 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
進行子類化時,重寫方法要注意執行緒暗轉問題。
2. NSOperationQueue
NSOperation
的很多花式操作,都是結合著 NSOperationQueue
進行的。我們在使用的時候,也是兩者結合著使用。下面對其進行詳細分析。
基本用法
- operation 放到 queue 中不用在手動呼叫
start
方法去執行,operation 會自動執行。 - queue 可以設定最大併發數,當併發數量設定為 1 時,為序列佇列;預設併發數量為無限大。
- queue 可以通過設定
suspended
屬性來暫停或者啟動還未執行的 operation。 - queue 可以通過呼叫
-[cancelAllOperations]
方法來取消佇列中的任務。 - queue 可以通過
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 { ... }
}
複製程式碼
程式碼很長,但是簡單,可以直接通過註釋來理解了。這裡屢一下:
- 將每個 operation 加入到佇列時,會根據優先順序將 operation 分類存入 list 中,根據優先順序執行。如果都不設定優先順序,執行起來比較快一些。
- 加入到佇列,會遍歷每個 operation,取出進入
Ready
狀態且沒被Cancel
的依次執行。 - 通過
concurrencyGate
這個訊號量來控制併發數量。每當執行一個任務,wait 訊號量減一,signal 訊號量加一,當訊號量為0時,一直等待,直接大於 0 才會正常執行。 - 每個方法中基本都加了鎖,來保證執行緒安全。
自定義 NSOperation
之前說了自定義普通的 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 的使用和基本原理的瞭解,我認為最重要的還是這一部分。畢竟我們還是要拿這些東西來開發的。併發程式設計中有很多坑,這裡簡單介紹一些。
1. NSNotification 與多執行緒問題
我們都知道,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
複製程式碼
2. NSTimer 與多執行緒問題
使用 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
複製程式碼
3. Dispatch Once 死鎖問題
在開發中,我們經常使用 dispatch_once
,但是遞迴呼叫會造成死鎖。例如下面這樣:
- (void)dispatchOnceTest {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self dispatchOnceTest];
});
}
複製程式碼
至於為什麼會死鎖,上文介紹 Dispatch Once 的時候已經說明了,這裡就不多做介紹了。提醒一下使用的時候要注意,不要造成遞迴呼叫。
4. Dispatch Group 問題
在使用 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 我會更新進來。另外,因為文章比較長,可能會出現一個錯誤,歡迎指正,我會對此加以修改。