我所理解的 iOS 併發程式設計

boolchow發表於2018-06-10

作者: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 過來了:

我所理解的 iOS 併發程式設計

下面針對這些鎖,逐一分析。

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_NORMALPTHREAD_MUTEX_ERRORCHECKPTHREAD_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_waitpthread_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 的形式寫入雜湊表。結構大概是這個樣子:

我所理解的 iOS 併發程式設計

儲存的時候,是以雜湊表結構儲存,不是我上面畫的順序儲存,上面只是一個節點而已。

@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 為我們提供了那些東西:

我所理解的 iOS 併發程式設計

系統所提供的 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_specificdispatch_queue_get_specificdispatch_get_specific 方法,為 queue 設定關聯的 key 或者根據 key 找到關聯物件等操作。

可以說,系統為我們提供了 5 中不同的佇列,執行在主執行緒中的 main queue;3 個不同優先順序的 global queue; 一個優先順序更低的 background queue。除此之外,開發者可以自定義一些序列和並行佇列,這些自定義佇列中被排程的所有 block 最終都會被放到系統全域性佇列和執行緒池中,後面會講這部分原理。盜用一張經典圖:

我所理解的 iOS 併發程式設計

同步 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);
}
複製程式碼

根據程式碼生成的流程圖,不想看程式碼直接看圖,下同:

我所理解的 iOS 併發程式設計

根據流程圖,這個方法的步驟如下:

  • 開發者呼叫 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();
}
複製程式碼

不想看程式碼,直接看圖:

我所理解的 iOS 併發程式設計

根據流程圖描述一下過程:

  • 首先開發者呼叫 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... 根據邏輯畫完圖才發現,其實這個圖也挺亂的,所以我將兩個主分支用不同顏色標記處理):

我所理解的 iOS 併發程式設計

根據這個圖,我來表述一下主要過程:

  • 我們呼叫 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
複製程式碼

轉化成圖可能好理解一些:

我所理解的 iOS 併發程式設計

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。下面是一個詳細的流程圖,我們結合這張圖來說一下:

我所理解的 iOS 併發程式設計

  • 建立一個 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_NOWDISPATCH_TIME_FOREVER 這兩個系統給出的時間。

以上為 GCD 相關知識,這次使用的原始碼版本為最新版本 —— 912.30.4.tar.gz,和之前看的版本程式碼差距很大,因為程式碼量的增加,新版本程式碼比較亂,不過基本原理還是差不多的。曾經我一度認為,最上面的是最新版本...

Operations

Operations 也是我們在併發程式設計中常用的一套 API,根據 官方文件 劃分的結構如下圖:

我所理解的 iOS 併發程式設計

其中 NSBlockOperationNSInvocationOperation 是基於 NSOperation 的子類化實現。相對於 GCD,Operations 的原理要稍微好理解一些,下面就將用法和原理介紹一下。

1. NSOperation

基本使用

每一個 operation 可以認為是一個 task。NSOperation 本事是一個抽象類,使用前需子類化。幸運的是,Apple 為我們實現了兩個子類:NSInvocationOperationNSBlockOperation。我們也可以自己去定義一個 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 出現的一張圖:

我所理解的 iOS 併發程式設計

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... 順序執行。

我所理解的 iOS 併發程式設計

基本原理

我們依然是在 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 我會更新進來。另外,因為文章比較長,可能會出現一個錯誤,歡迎指正,我會對此加以修改。

參考文獻

  1. 深入理解 iOS 開發中的鎖
  2. 關於 @synchronized,這兒比你想知道的還要多
  3. 深入理解 GCD
  4. GCD原始碼分析2 —— dispatch_once篇
  5. GCD原始碼分析6 —— dispatch_source篇
  6. Dispatch
  7. Task Management - Operation
  8. swift-corelibs-foundation
  9. Advanced NSOperations

相關文章