我所理解的 iOS 併發編程

做者:bool周 原文連接:我所理解的 iOS 併發編程ios

不管在哪一個平臺,併發編程都是一個讓人頭疼的問題。慶幸的是,相對於服務端,客戶端的併發編程簡單了許多。這篇文章主要講述一些基於 iOS 平臺的一些併發編程相關東西,我寫博客習慣於先介紹原理,後介紹用法,畢竟對於 API 的使用,官網有更好的文檔。git

一些原理性的東西

爲了便於理解,這裏先解釋一些相關概念。若是你對這些概念已經很熟悉,能夠直接跳過。github

1.進程

從操做系統定義上來講,進程就是系統進行資源分配和調度的基本單位,系統建立一個線程後,會爲其分配對應的資源。在 iOS 系統中,進程能夠理解爲就是一個 App。iOS 並無提供能夠建立進程的 API,即便你調用 fork() 函數,也不能建立新的進程。因此,本文所說的併發編程,都是針對線程來講的算法

2.線程

線程是程序執行流的最小單元。通常狀況下,一個進程會有多個線程,或者至少有一個線程。一個線程有建立、就緒、運行、阻塞和死亡五種狀態。線程能夠共享進程的資源,全部的問題也是由於共享資源引發的。數據庫

3.併發

操做系統引入線程的概念,是爲了使過個 CPU 更好的協調運行,充分發揮他們的並行處理能力。例如在 iOS 系統中,你能夠在主線程中進行 UI 操做,而後另啓一些線程來處理與 UI 操做無關的事情,兩件事情並行處理,速度比較快。這就是併發的大體概念。編程

4.時間片

按照 wiki 上面解釋:是分時操做系統分配給每一個正在運行的進程微觀上的一段CPU時間(在搶佔內核中是:從進程開始運行直到被搶佔的時間)。線程能夠被認爲是 」微進程「,所以這個概念也能夠用到線程方面。swift

通常操做系統使用時間片輪轉算法進行調度,即每次調度時,老是選擇就緒隊列的隊首進程,讓其在CPU上運行一個系統預先設置好的時間片。一個時間片內沒有完成運行的進程,返回到緒隊列末尾從新排隊,等待下一次調度。不一樣的操做系統,時間片的範圍不一致,通常都是毫秒(ms)級別。api

4.死鎖

死鎖是因爲多個線程(進程)在執行過程當中,由於爭奪資源而形成的互相等待現象,你能夠理解爲卡主了。產生死鎖的必要條件有四個:數組

  • 互斥條件 : 指進程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程佔用。若是此時還有其它進程請求資源,則請求者只能等待,直至佔有資源的進程用畢釋放。
  • 請求和保持條件 : 指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程佔有,此時請求進程阻塞,但又對本身已得到的其它資源保持不放。
  • 不可剝奪條件 : 指進程已得到的資源,在未使用完以前,不能被剝奪,只能在使用完時由本身釋放。
  • 環路等待條件 : 指在發生死鎖時,必然存在一個進程——資源的環形鏈,即進程集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1佔用的資源;P1正在等待P2佔用的資源,……,Pn正在等待已被P0佔用的資源。

爲了便於理解,這裏舉一個例子:一座橋,同一時間只容許一輛車通過(互斥)。兩輛車 A,B 從橋的兩端開上橋,走到橋的中間。此時 A 車不願退(不可剝奪),又想佔用 B 車所佔據的道路;B 車此時也不願退,又想佔用 A 車所佔據的道路(請求和保持)。此時,A 等待 B 佔用的資源,B 等待 A 佔用的資源(環路等待),兩車僵持下去,就造成了死鎖現象xcode

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_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 的形式寫入哈希表。結構大概是這個樣子:

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

@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_specificdispatch_queue_get_specificdispatch_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_NOWDISPATCH_TIME_FOREVER 這兩個系統給出的時間。

以上爲 GCD 相關知識,此次使用的源碼版本爲最新版本 —— 912.30.4.tar.gz,和以前看的版本代碼差距很大,由於代碼量的增長,新版本代碼比較亂,不過基本原理仍是差很少的。曾經我一度認爲,最上面的是最新版本...

Operations

Operations 也是咱們在併發編程中經常使用的一套 API,根據 官方文檔 劃分的結構以下圖:

其中 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 出現的一張圖:

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

參考文獻

  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
相關文章
相關標籤/搜索