GCD 存在於一個 名爲 libdispatch 的類庫中,這個蘋果官方的類庫提供了在 iOS 和 OS X 多核設備執行併發代碼的支持。html
GCD 能夠經過延遲可能須要花費大量時間的任務,並讓他們在後臺(background)運行,從而提升應用的響應速度。git
相對於線程和鎖來講,GCD 提供了一個更加易用的模板,從而避免發生併發問題(concurrency bug)。
對於相似單例(singletons)模式,GCD能夠用來優化咱們的代碼。github
串行和並行描述的是任務之間如何運行。
串行任務每一次僅執行一個
並行任務能夠多個同時執行數組
同步方法僅會在一個任務完成後返回。
異步方法會當即返回,它會讓這個任務執行完成,但不會等待任務完成。所以,異步方法不會阻塞當前線程。安全
指的是不能併發執行的一段代碼(不能被兩個線程同時訪問)。這麼作一般是由於一個共享數據(shared resource),例如一個變量,被併發進程訪問後,進程之間可能都會對這個變量產生影響,可能會產生數據污染。網絡
當兩個(多個)線程同時訪問共享的數據時,會發生爭用情形。第一個線程讀取了一個變量。第二個線程也讀取了這個變量的值。而後這兩個線程同時操做了該變量,此時他們會發生競爭來看哪一個線程會最後寫入這個變量。最後被寫入的值將會被保留下來。多線程
兩個(多個)線程都要等待對方完成某個操做才能進行下一步,這時會發生死鎖。例如,兩個線程各自鎖定了一個變量,而後他們想要鎖定對方鎖定的那個變量,這就產生了死鎖。併發
一段線程安全的代碼(一個線程安全的對象),能夠同時被多個線程或併發的任務調用,不會產生問題。非線程安全的只能按次序被訪問(調用)。舉例來說,NSDictionary 是線程安全的,NSMutableDictionary 是非線程安全的。app
注意:全部的 Mutable 對象都是非線程安全的,全部的 Immutable 對象都是線程安全的。根本緣由是,Mutable 對象是能夠被修改的,Immutable 對象時不能被修改的。使用 Mutable 對象要注意,必定要用同步鎖來同步訪問(@synchronized)。異步
互斥鎖的優勢:可以防止多線程搶奪形成的數據安全的問題。
缺點:實現互斥鎖會消耗大量的資源。
同時,還有原子屬性(atomic)也能夠實現加鎖。
atomic:原子屬性,爲 setter 方法加鎖
nonatomic:非原子屬性,不會爲 setter 加鎖
注意:全部屬性都聲明爲 nonatomic, 客戶端應儘可能避免多線程爭奪同一資源。
當一個進程(process)中有多個線程(threads)來回切換時,context switch 用來記錄執行狀態。這樣的進程和通常的多線程進程沒有太大差異,但會產生一些額外的開銷。
並行是基於多核設備的,並行 必定是併發,但併發不必定是並行。
GCD 提供了 dispatch queue 用來處理代碼塊,這些隊列會管理你提供給GCD 的 tasks 而後按先進先出的順序執行這些 tasks。
全部的 dispatch queue 都是線程安全的,咱們可使用多個線程同時訪問一個 queue。當理解了 dispatch queue 如何爲咱們的代碼提供線程安全,GCD 的優勢就很容易理解了。最關鍵的是要選擇正確的 dispatch queue 和 dispatching function 來註冊咱們的代碼塊。
串行隊列中的任務(Tasks in serial queue),每次僅執行一個。執行順序是,每一個任務僅會當上一個正在執行的任務結束後纔開始執行。咱們沒法知道一個任務結束時到下一個任務開始的時間。
這些任務的執行時間是由 GCD 控制的,GCD 可以保證的是,每次僅執行一個任務,這些任務的執行順序是他們被添加進隊列 (queue)的順序。
併發隊列中的任務會按照咱們添加任務的順序來執行,但不保證完成了一個以後纔開始下一個任務。這些任務可能以任何順序結束,咱們也沒法知道距下個任務(block)開始須要的時間,也不知道在某個時間段有多少個任務在執行。這些所有交由 GCD 控制。
何時開始執行一個任務(block)徹底由 GCD 控制。若是兩個任務的執行時間有重疊,將有 GCD 來決定是否要將一種一個任務交給另外一個空閒狀態的核心處理,仍是使用一個 context switch 來在兩個 block 之間切換執行。
GCD 至少提供了5 種不一樣的 queues 。
(1) 首先,系統爲咱們提供了一個特別的串行隊列—— main queue。和其餘串行隊列同樣,main queue 中的任務也是每次僅執行一個。main queue 保證了隊列中全部的任務都會在主線程中進行。這個隊列就是用來給 UIView 發送消息,或者發送一條通知。
(2) 系統也提供了一些併發隊列(concurrent queue),咱們成爲 Global Dispatch Queues。目前有四種全局隊列,他們分別有不一樣的優先級:background, low, default, high。須要注意的時,蘋果的API也會使用這些隊列,因此這些隊列不會被咱們添加的任務獨佔。
(3) 咱們也能夠建立本身的自定義串行隊列或者併發隊列。這意味着失少有五個(種)隊列能夠由咱們使用:main queue, 四種 global dispatch queue, 還有咱們自定義的隊列。
注意:GCD 的重點在於,咱們要選擇正確的隊列調度功能(queue dispatching function),來把咱們的任務提交到隊列中。
原始代碼:
UIImage *overlayImage = [self faceOverlayImageFromImage:_image]; [self fadeInNewImage:overlayImage];
修改成:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1 UIImage *overlayImage = [self faceOverlayImageFromImage:_image]; dispatch_async(dispatch_get_main_queue(), ^{ // 2 [self fadeInNewImage:overlayImage]; // 3 }); }); }
註釋1:首先咱們將主線程的任務移到了一個全局隊列中。由於這裏是 dispatch_async,這意味着這個代碼塊(block)被異步提交(submitted)。這樣就會使得 viewDidload 更快執行完成。面部識別的代碼就會稍晚被執行完(面部識別的代碼由於由GCD控制,咱們無得知到底何時可以執行完成)。
註釋2:此時人臉識別所在的 block 已經執行完成,咱們生成了一個新的 UIImage。由於咱們須要用獲得的 UIImage 來更新UIImageView,因此咱們在主線程上提交了一個 block。
注意:全部UIKit 相關的操做都應該在主線程上執行。
註釋3:更新 UI
自定義串聯隊列(custom serial queue)
當你但願在後臺按順序執行一些任務,而且追蹤這些任務。這種方式消除了資源爭奪現象。
主隊列(Main Queue)
當咱們完成了一個併發隊列中的某個工做後,須要更新 UI,咱們一般使用 main queue。實現main queue 須要將一個 block 寫在 另外一個 block 中。還有,若是咱們已經在主隊列(main queue)中,而後還調用 dispatch_async 指向main queue,咱們就會獲得保證——新的 task 在當前方法完成後必定會被執行。
併發隊列
一般非UI展現/更新的代碼,都用併發隊列。
下面代碼的目的是,在viewDidAppear 後,根據狀況爲用戶顯示一個提示(爲了引發用戶的注意,這個提示延遲 1s出現)
-(void)showOrHideNavPrompt { NSUInteger count = [[PhotoManager sharedManager] photos].count; double delayInSeconds = 1.0; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ // 2 if (!count) { [self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"]; } else { [self.navigationItem setPrompt:nil]; } }); }
註釋1:咱們聲明瞭須要被 delay 的時間。
註釋2:咱們在等待了 delay 的這段時間後,異步的將這個block 添加到主線程中。
dispatchafter 其實至關於一個延時的 dispatchasync。一樣咱們仍然不能控制 block 中執行的時間,當 dispatch_after 已經返回,咱們也沒法取消執行 block 中的任務。
dispatch_after 的使用場景:
Custom Serial Queue: 要當心將 dispatch_after 使用在一個 custom serial queue 上。通常仍是用在 main queue 上。
Main queue :一般 dispatchafter 都用在 mainqueue 上
Concurrent queue : 也要當心使用!
關於單例,咱們一般都會關心——單例不是線程安全的。基於單例的使用狀況,這個關心是很合理的:單例的 instance 常常被多個 ViewController 同時訪問。
接下來,咱們在一個單例(singleton instance)本身製造一個競爭狀況(race condition)。
本來代碼以下:
+(instancetype)sharedManager { static PhotoManager *sharedPhotoManager = nil; if (!sharedPhotoManager) { sharedPhotoManager = [[PhotoManager alloc] init]; sharedPhotoManager->_photosArray = [NSMutableArray array]; } return sharedPhotoManager; }
上面的代碼很是簡單,建立並初始化了一個單例。
須要注意的是,上面的 if 語句不是線程安全的。若是咱們屢次調用這個方法,有可能某一個線(Thread A)程會在 sharedPhotoManager 初始化以前就進入 if 語句中,而且 switch 在此時也未能出現。而後,另外一個線程(Thread B)進入 if 語句,初始化這個單例,而後退出。
當系統 context switch 切換到 Thread A ,咱們會再次初始化這個單例,而後退出。這樣咱們就有了兩個單例。這是咱們不但願發生的。
咱們能夠改變上面的代碼,迫使這種極端狀況發生以便咱們觀察。
PhotoManager.m 中
+(instancetype)sharedManager { static PhotoManager *sharedPhotoManager = nil; if (!sharedPhotoManager) { [NSThread sleepForTimeInterval:2]; sharedPhotoManager = [[PhotoManager alloc] init]; NSLog(@"Singleton has memory address at: %@", sharedPhotoManager); [NSThread sleepForTimeInterval:2]; sharedPhotoManager->_photosArray = [NSMutableArray array]; } return sharedPhotoManager; }
上面的代碼中經過 NSThread 的 sleep 方法,強制使 context switch 出現。
AppDelegate.m 中
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [PhotoManager sharedManager]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [PhotoManager sharedManager]; });
這裏建立了多個異步併發,實例化單例,而後製造了一個 race condition。
咱們能夠看到輸出結果:
能夠看到,Log 出三個單例內存地址在這裏是不一樣的。
問題的緣由是,初始化PhotoManager 這部分做爲一個 critical section,應該僅被執行一次,可實際卻被執行了屢次。雖然上面的狀況是咱們強制出現的,可是仍是有可能不當心形成這個問題。
注意:剛剛在運行中,三次編譯兩次都直接 crash,僅僅上面一次log 出上面的結果。這個問題的緣由是:系統事件是不受咱們控制的。若是出現了線程問題,由於問題難以重現,因此也很是難Debug。
爲了糾正這個問題,實例化的代碼應該僅被執行一次,由於 if 語句中的代碼做爲一個 critical section。這裏就引出了咱們要使用的 dispatch_once。
將上面初始化的代碼替換爲
+(instancetype)sharedManager { static PhotoManager *sharedPhotoManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [NSThread sleepForTimeInterval:2]; sharedPhotoManager = [[PhotoManager alloc] init]; NSLog(@"Singleton has memory address at: %@", sharedPhotoManager); [NSThread sleepForTimeInterval:2]; sharedPhotoManager->_photosArray = [NSMutableArray array]; }); return sharedPhotoManager; }
能夠看到,以前的問題已經沒有再出現了。咱們把上面的代碼調整一下
+(instancetype)sharedManager { static PhotoManager *sharedPhotoManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedPhotoManager = [[PhotoManager alloc] init]; sharedPhotoManager->_photosArray = [NSMutableArray array]; }); return sharedPhotoManager; }
dispatchonce 在一種線程安全的模式下,執行而且只執行一個 block。在有一個線程已經在這個 critical section,若是有其餘線程嘗試訪問 dispatchonce 中的 section,這些線程會一直被 block 直到 critical section 執行完成。
須要注意的是,這個作法實現了實例的線程安全,並無實現類的線程安全。若是須要,在類中還須要添加其餘的 critical block,例如咱們須要操做重要的內部數據的時候。這些咱們可使用其餘的方法來實現線程安全,例如 同步一個數據的訪問(synchronising access to data),咱們在後面會提到。
實例的線程安全並不只僅是單例要處理的惟一問題。若是單例屬性是一個 mutable 對象,咱們就須要考慮這個這個對象是不是線程安全的。關於對象是不是線程安全,咱們能夠參照蘋果提供的一份文檔:點擊連接
舉例來講,NSMutableArray 就不是線程安全的。
雖然不少線程均可以從 NSMutableArray 的實例當即訪問,可是當它正在某個線程被訪問的時候還讓其餘線程來訪問顯然是不安全的。咱們並無防止這種狀況的發生。
仍是 PhotoManager.m 中
-(void)addPhoto:(Photo *)photo { if (photo) { [_photosArray addObject:photo]; dispatch_async(dispatch_get_main_queue(), ^{ [self postContentAddedNotification]; }); } }
上面是一個 write 方法,給 photoArray 添加一個對象。
-(NSArray *)photos { return [NSArray arrayWithArray:_photosArray]; }
這裏是一個 read 方法。爲了防止調用者改變這個數組,它給調用者生成了一個 photoArray 的 immutable 的copy。但咱們仍須要注意,當一個線程調用 read 方法時,要防止另外一個線程調用 write 方法。這個問題是軟件開發的經典問題 Reader-Writer Problem。GCG經過使用 dispatch barriers ,製造了一個 Reader-Writer lock 提供了一個很是好的解決方式。
Dispatch barrier 的做用是,在併發隊列中,提供一種相似串行的瓶頸。使用 dispatch barrier 能夠確保被提交的 block 在一個特定時間段內被這個 queue 惟一執行。這意味着,全部以前提交到隊列中的 block 必需要在 barrier block 執行前執行完成。
當輪到這個特定的block 的時候,這個 barrier 會執行這個 block,並確保這個 queue 在這個block 執行期間不執行其餘的 block。一旦完成,這個queue 就會按原來的設定繼續執行。GCD 提供了同步和異步兩種 barrier 功能。
圖大體說明了 barrier 功能的效果。
能夠看到隊列中的通常操做和普通的併發隊列並無太大區別,可是當執行到barrier block 的時候,這個隊列就開始向一個 串行隊列了。當這個 barrier block 執行完成以後,這個隊列又開始恢復到併發隊列。
Custom serial queue
:不要在這裏使用 barrier,這已是一個 serial queue,使用 barrier 毫無心義。Global Concurrent Queue
:在這裏要當心使用,由於其餘系統操做可能會使用這個隊列,因此使用 barrier 將 Global Concurrent Queue 獨佔存在風險。Custom Concurrent Queue
:放心使用;
如上面所說,由於惟一比較好的 barrier 使用方式是在 custom concurrent Queue 中使用,因此咱們須要本身建立一個併發隊列來處理 barrier ,以便將 read 和 write 方法獨立開來。
在 PhotoManager.m 中,添加一個 dispatch 的 property:
@interface PhotoManager () @property (nonatomic,strong,readonly) NSMutableArray *photosArray; @property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue; ///< Add this @end
修改 addPhoto 方法:
-(void)addPhoto:(Photo *)photo { if (photo) { // 1 dispatch_barrier_async(self.concurrentPhotoQueue, ^{ // 2 [_photosArray addObject:photo]; // 3 dispatch_async(dispatch_get_main_queue(), ^{ // 4 [self postContentAddedNotification]; }); }); } }
註釋1:檢查 photo 是否存在
註釋2:用咱們的自定義 queue 添加寫操做,當 critical section 在稍後執行的時候,這將是隊列中惟一被執行的 task。
註釋3:將對象添加到數組。由於這是一個 barrier block,不會有其餘的 block 同時和這個block 在 concurrentPhotoQueue 中運行。
註釋4:最後發送一個通知,廣播圖片已經添加完成。由於這個通知包含 UI 相關的代碼,因此須要從 main thread 中發出,因此在這裏咱們調度另外一個異步任務到主線程,以完成發送通知。
上面的代碼處理了寫方法,咱們還須要實現 photos 的讀方法,還有對 concurrentPhotoQueue 實例化。
爲了保證寫方法那邊的線程安全,在讀方法這邊咱們也須要作一些處理。雖然咱們須要從 photos 方法得到返回值,但不能異步調度到這個線程,由於它不必定會photo 方法 return以前結束完成。
這種狀況,dispatch_sync 就是很是好的選擇。
dispatchsync 以同步方式將代碼塊提交,而且等待 block 完成才返回。咱們可使用 dispatchsync 來追蹤 dispatch barrier block,或者當咱們須要等待某個操做結束,以獲得這個 block 處理後的數據。若是是第二種狀況,咱們一般會看到一個 _block 變量寫在dispatchsync 的外面,以便使用被 dispatch_sync 中的block 處理後的數據。
可是,使用 dispatchsync 的時候仍是要至關當心。若是咱們吧 dispatchsync 使用在咱們當前正在運行的隊列上,會致使死鎖,由於這個調用會等到 block 結束後執行,可是這個 block 不能結束(由於都不能開始),由於當前的任務也沒有結束。因此咱們必需要當心咱們要從哪一個隊列調用,也要當心將哪一個隊列傳入。
Custom Serial Queue:必定要很是當心; 若是咱們已經在一個隊列中運行,而後調用了 dispatch_sync 指向當前這個隊列,會形成死鎖。
Main Queue:一樣要很是當心;和上面緣由同樣,要注意死鎖。
Concurrent Queue:通常都放心使用;無論是和 dispatch_barrier 協同使用,仍是等待一個任務結束以便下一步處理,都是合理的選擇。
仍是在 PhotoManager.m 中,修改 photos 讀方法
-(NSArray *)photos { __block NSArray *array; // 1 dispatch_sync(self.concurrentPhotoQueue, ^{ // 2 array = [NSArray arrayWithArray:_photosArray]; // 3 }); return array; }
註釋1:__block 關鍵字容許 block 中的對象是 mutable 的。若是沒有這個關鍵字, block 中的 array將會變爲只讀,代碼將會沒法編譯。
註釋2:同步調度到 concurrentPhotoQueue 來進行讀方法。
註釋3:存儲 photo 數組並返回。
注意:若是咱們但願在一個 block 外面聲明一個對象,在 block 內給它賦值,咱們就須要在聲明的時候加上 __block 關鍵字,這個關鍵字容許咱們修改 block 內的對象。不然將沒法編譯。
最後咱們須要實例化 concurrentPhotoQueue 屬性,修改 sharedManager 方法實例化這個隊列:
+(instancetype)sharedManager { static PhotoManager *sharedPhotoManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedPhotoManager = [[PhotoManager alloc] init]; sharedPhotoManager->_photosArray = [NSMutableArray array]; // ADD THIS: sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue", DISPATCH_QUEUE_CONCURRENT); }); return sharedPhotoManager; }
上面的代碼使用 dispatchqueuecreate 將concurrentPhotoQueue 初始化爲一個併發隊列。這一個參數通常寫成反 DNS 形式,最好確保有必定意義,以便 Debug。第二個參數指定咱們但願隊列是串行的仍是併發的。
注意:上面的 dispatchqueuecreate 方法中,咱們能夠看到常常有人傳 0 或 NULL 做爲第二個參數。可是這種作法是過期的,最好仍是說明咱們的參數。
如今,PhotoManager 單例已是線程安全的了。無論咱們怎麼讀寫這些圖片,咱們均可以確保這些都會以相對安全的方式完成。
咱們先來看下面的示例代碼:
-(void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock { __block NSError *error; for (NSInteger i = 0; i < 3; i++) { NSURL *url; switch (i) { case 0: url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; break; case 1: url = [NSURL URLWithString:kSuccessKidURLString]; break; case 2: url = [NSURL URLWithString:kLotsOfFacesURLString]; break; default: break; } Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) { if (_error) { error = _error; } }]; [[PhotoManager sharedManager] addPhoto:photo]; } if (completionBlock) { completionBlock(error); } }
在這個方法的末尾,調用了 completionBlock 。實際這樣寫是有問題的,並不能保證上面的下載圖片方法可以當即完成。由於 initwithURL : withCompletionBlock :
這個方法是異步方法,因此下載尚未完成,這個方法就返回了。可是 downloadPhotosWithCompletionBlock
這個方法卻以同步的方式,在方法最後調用了 completion block
。
正確的作法應該是:downloadPhotosWithCompletionBlock
僅應該在圖片下載的異步方法initwithURL : withCompletionBlock :
完成並調用了它的 completion block
,downloadPhotosWithCompletionBlock
再去調用本身的 completion block
。
可是,問題來了——下載圖片的方法是異步方法,前文章裏咱們已經說過,異步任務雖然也是按照提交(submit)的順序開始的,可是一旦開始執行,咱們就沒法控制這個任務何時被執行完成——這些都由 GCD 來控制。
也許咱們會想到使用一些全局的 bool 型變量來記錄下載的狀態,這種作法不是說不行,但的確是比較笨的辦法。
幸運的是 GCD 爲咱們提供了 dispatch groups,使用 dispatch groups 來監控異步任務的完成狀況在合適不過了。
Dispatch groups 會在一組任務完成以後通知咱們。這些任務能夠是同步的也能夠是異步的,甚至這些任務不在同一個隊列裏也可以被追蹤(監控)。當一組任務完成以後,dispatch groups 也能夠按同步或異步的方式來通知咱們。由於不一樣隊列的任務也能夠被追蹤,須要有一個 dispatchgroupt 的實例來追蹤不一樣隊列中的不一樣任務。
GCD 提供了兩種方式來實現追蹤
這個功能用來阻塞當前的線程,一直到這個任務組(group)的全部任務結束,或者直到一個 timeout 出現。dispatch_group_wait
是一個同步功能。咱們能夠將這個功能應用在以前有問題的代碼中:
-(void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1 __block NSError *error; dispatch_group_t downloadGroup = dispatch_group_create(); // 2 for (NSInteger i = 0; i < 3; i++) { NSURL *url; switch (i) { case 0: url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; break; case 1: url = [NSURL URLWithString:kSuccessKidURLString]; break; case 2: url = [NSURL URLWithString:kLotsOfFacesURLString]; break; default: break; } dispatch_group_enter(downloadGroup); // 3 Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) { if (_error) { error = _error; } dispatch_group_leave(downloadGroup); // 4 }]; [[PhotoManager sharedManager] addPhoto:photo]; } dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER); // 5 dispatch_async(dispatch_get_main_queue(), ^{ // 6 if (completionBlock) { // 7 completionBlock(error); } }); }); }
註釋1:由於咱們使用了同步功能 dispatchgroupwait,它阻塞了當前的線程。咱們將這整段代碼都包含在了一個 dispatchasync 當中,就是爲了不這個主線程在這裏不被 dispatchgroup_wait 所阻塞。
註釋2:建立一個新的 dispatch group,它的做用有點像一個用來記錄未完成的任務的計數器。
註釋3:dispatchgroupenter 用來通知一組任務開始了。在使用中須要注意的是,咱們使用了多少個 dispatchgroupenter ,一般就要使用多少個 dispatchgroupleave , 不然可能會形成一些很是奇怪的 bug。
註釋4:通知這一組(group)任務已經完成了。
註釋5:dispatchgroupwait ,開始等待,一直到這組任務所有結束,或者知道任務出現了 timeout。若是發生了 timeout,dispatchgroupwait 會返回一個非零結果。咱們能夠用這個結果來判斷任務是否是超時。可是,若是咱們向上面的代碼寫的同樣,使用了 DISPATCHTIMEFOREVER ,這個任務就永遠不會超時,dispatchgroupwait 就會一直等待直到任務完成。
註釋6:此時咱們就能知道,全部的圖片下載任務要麼是下載完成了,或者是超時了。而後咱們就能回調到主線程,調用 downloadPhotosWithCompletionBlock 這個方法的 completion block。
註釋7:檢查 completion block 是否爲空,若是不爲空,返回。
上面的代碼如今看起來效果還不錯,可是咱們最好仍是要避免阻塞線程的操做(同步方式)。接下來咱們會重寫上面的方法,當下載完成的時候,使用異步通知的方式,使咱們可以知道下載結束了。
咱們先來了解一下 dispatch group 的使用場景:
自定義穿行隊列(Custom serial queue):比較適合使用通知形式。
主隊列(Main queue):可使用但必定要當心,有可能咱們提交的任務會阻塞主線程。
併發隊列(Concurrent queue):無論是使用通知形式仍是 dispatchgroupwait, 都是很好的選擇。
接下來咱們介紹 dispatch groups 的另外一種使用方式
上面的代碼的確實現咱們須要的效果了,可是咱們能夠看到,咱們必須 dispatch async 到另外一個隊列而後才能使用 dispatchgroupwait 。咱們能夠嘗試使用另外一種方法,修改上面的代碼:
-(void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock { // 1 __block NSError *error; dispatch_group_t downloadGroup = dispatch_group_create(); for (NSInteger i = 0; i < 3; i++) { NSURL *url; switch (i) { case 0: url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; break; case 1: url = [NSURL URLWithString:kSuccessKidURLString]; break; case 2: url = [NSURL URLWithString:kLotsOfFacesURLString]; break; default: break; } dispatch_group_enter(downloadGroup); // 2 Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) { if (_error) { error = _error; } dispatch_group_leave(downloadGroup); // 3 }]; [[PhotoManager sharedManager] addPhoto:photo]; } dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ // 4 if (completionBlock) { completionBlock(error); } }); }
註釋1:在這裏咱們就不用像使用 dispatchgroupwait 時候那樣把整個方法都放在 dispatch async 裏,由於咱們在這裏並無阻塞主線程。
註釋2:和上面同樣,表示一個 block 進入了這個group。
註釋3:一個 block 離開了 group。
註釋4:這段代碼會在以前的 dispatch group 中所有執行完時執行。以後咱們就須要執行 completion block 了,在這裏咱們要制定咱們以後但願在那個隊列來執行 completion block,在這裏就是 main queue 了。
關於
void dispatch_group_notify ( dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block );
做用是:當以前提交到 group 的任務(block)完成時,把一個 block 提交到一個隊列。
參數1:咱們要追蹤的 dispatch group
參數2:當 group 完成以後,咱們用來提交 block 的隊列。
參數3:要被提交的 block。
咱們來看一下上面的代碼,方法中有一個 for 循環,用來下載三張不一樣的圖片。咱們能夠嘗試一下可否對這個 for 循環使用併發,加速下載。
在這裏咱們就能夠嘗試使用 dispatch_apply。
dispatchapply 的做用有點像一個用來併發執行不一樣的迭代(iterations)的for 循環。dispatchapply 這個方法是同步的,因此和通常的 for 循環同樣,dispatch_apply 只會在執行完成以後纔會返回。
須要注意的是,dispatchapply 中的迭代數量也不宜太多,過多的迭代可能會形成每一個迭代的碎片時間、消耗累積,影響使用。在這裏 dispatchapply 中的迭代總數須要咱們在編碼過程當中嘗試、優化。
在哪裏使用 dispatch_apply 比較合適呢?
自定義串行隊列(Custom serial queue):不適用,由於 dispatch_apply 是同步方法,仍是老老實實使用 for 循環把。
主隊列(Main queue):跟上面同樣,一樣不適用。
併發隊列(Concurrent queue):適用 dispatch_apply,特別是當咱們須要追蹤任務完成狀況。
咱們再次修改上面的代碼:
-(void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock { __block NSError *error; dispatch_group_t downloadGroup = dispatch_group_create(); dispatch_apply(3, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t i) { NSURL *url; switch (i) { case 0: url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; break; case 1: url = [NSURL URLWithString:kSuccessKidURLString]; break; case 2: url = [NSURL URLWithString:kLotsOfFacesURLString]; break; default: break; } dispatch_group_enter(downloadGroup); Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) { if (_error) { error = _error; } dispatch_group_leave(downloadGroup); }]; [[PhotoManager sharedManager] addPhoto:photo]; }); dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ if (completionBlock) { completionBlock(error); } }); }
修改後,以前的 for 循環就能夠併發執行了。在上面的調用 dispatch_apply 時,第一個參數是迭代的數量,第二個參數是真正執行任務的隊列,第三個就是要提交的 block。
須要注意的是,咱們已經把 add photo 的方法修改成線程安全,可是獲得圖片的順序仍是不必定的,由於這取決於 dispatch_apply 中哪個迭代先執行完成。
運行後咱們會發現,屢次點擊從網絡加載圖片,偶爾下載速度會有一點提高。
但事實上,使用 dispatch_apply 來實現這個效果並不太值得,緣由是:
由於線程並行的緣由,多多少少會增長運行開銷。
在這裏咱們也能體會到,使用 dispatch_apply 替換 for 循環之後,效果並無太大提高。因此在這裏花時間顯得有些不太值得。
一般來講,代碼優化會使得代碼變得更復雜,也讓別人更難讀懂咱們的代碼。因此優化前要考慮到這一點。
除了上面介紹過的這些經常使用功能外,GCD 還有一些不是那麼經常使用的功能,但在某些場合使用也許會十分方便。
在 Xcode 中測試是在 XCTestCase 的一個子類上執行。測試方法的方法名一般是本來的方法簽名前加一個 test。測試是在主線程上進行的。
一旦一個測試方法運行完成,XCTest 方法會認爲這個方法已經完成而且運行下一個 test。這就意味着,若是一個方法中包含異步代碼,頗有可能在運行下一個方法的時候,當前方法還在繼續運行。
一般網絡請求相關代碼都是異步的,加上 test 一般都會在測試方法執行完成後就結束,這會使得網絡請求代碼很難測試。可是,咱們能夠在 test 方法中阻塞主線程一直到網絡請求完成。
這裏須要注意的是,是否使用這種方式進行測試你們仍是有分歧的,有些人認爲這種方式沒有很好的遵循集成測試的設定。可是若是這種方式對咱們有用,不妨一試。
在 test 方法中,本來代碼以下:
-(void)downloadImageURLWithString:(NSString *)URLString { NSURL *url = [NSURL URLWithString:URLString]; __block BOOL isFinishedDownloading = NO; __unused Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *error) { if (error) { XCTFail(@"%@ failed. %@", URLString, error); } isFinishedDownloading = YES; }]; while (!isFinishedDownloading) {} }
用上面的方法測試並非很合理。方法末尾的 while 循環等待 isFinishedDownloading 在 completion block 中變爲true。運行 test,爲了讓現象更加明顯咱們能夠開啓手機開發者模式裏的網絡控制,把網絡狀態調整爲 very bad。打開 debug navigation, 在我這裏,能夠很是明顯的看到 CPU 使用率保持在 95% 高居不下,內存使用也從 6點几上漲到了 8 點幾。
這種實現方法叫作自旋鎖。形成這個現象的緣由是,while 循環在網絡請求結束以前,不間斷的檢查 isFinishedDownloading 中的值。
信號燈是一個很是 old-school 的概念。若是想了解更多一些,能夠百度「哲學家就餐問題」。
信號燈容許咱們控制多個消費者對有限的資源的訪問。咱們能夠把信號燈想象成一個餐廳的服務員,咱們的餐廳只有兩個位子,那麼每次咱們最多就只能容許兩位客人進去用餐,那麼剩下的客人就要排隊等位子用餐(First in first out)。
咱們來嘗試使用 semaphores,將上面的代碼替換爲:
-(void)downloadImageURLWithString:(NSString *)URLString { // 1 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); NSURL *url = [NSURL URLWithString:URLString]; __unused Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *error) { if (error) { XCTFail(@"%@ failed. %@", URLString, error); } // 2 dispatch_semaphore_signal(semaphore); }]; // 3 dispatch_time_t timeoutTime = dispatch_time(DISPATCH_TIME_NOW, kDefaultTimeoutLengthInNanoSeconds); if (dispatch_semaphore_wait(semaphore, timeoutTime)) { XCTFail(@"%@ timed out", URLString); } }
咱們來看看這段代碼的做用。
註釋1:建立 semaphore,參數表示 semaphore 初始的值。這個數字表示能夠直接訪問信號燈,而不用先讓信號燈增加的對象數量。(讓一個信號燈增加就是給一個信號燈發信號)
註釋2:在這裏咱們通知這個信號燈,咱們不在須要這個資源了。這裏讓信號燈增加,而且代表,信號燈能夠接受其餘對象的訪問了。
註釋3:等待信號燈發信號,並設置一個 timeout 的時間。這裏會阻塞當前的線程,知道 semaphore 被通知。若是超時會返回一個非零的結果(non-zero result)。
再次運行 test 能夠看到,CPU 使用率一直是 0,在十秒後返回了一個超時錯誤。
Dispatch source 簡單說是一個一些 low-level 功能的集合,能夠幫助咱們監控 Unix 信號、文件描述、VFS 節點等不太經常使用的東西。上面說的這些東西我本身也沒怎麼用過,在文中就不作介紹了,咱們能夠嘗試一種特殊的方式來使用如下 dispatch source。
咱們來看一下關於 dispatch source 的建立方法:
dispatch_source_t dispatch_source_create( dispatch_source_type_t type, uintptr_t handle, unsigned long mask, dispatch_queue_t queue);
第一個參數 dispatchsourcetype_t,這是最重要的參數。關於這個方法在官方文檔的詳細介紹:建立一個新的 dispatch source 來監控 low-level 的系統對象,而且自動註冊一個 handle block 到 dispatch queue 來對事件做出迴應。
參數 type:dispatch source 的類型,必須是這個列表中常量。
參數 handle:用來進行監控。這個參數取決於上面的 type 參數。
參數 mask:一個標誌的 mask,用來肯定須要哪些事件。一樣也受 type 的指揮。
參數 queue:事件操做 block 要被提交到的 queue。
返回值是一個 dispatch source 對象,若是沒有建立成功的話返回爲空。
這裏要監聽的是一個 DISPATCHSOURCETYPE_SIGNAL。
dispatch source 會監聽當前的進程,等待信號。handle 是一個int 類型的信號數字。Mask 暫時沒有用到,先傳0。
在這裏咱們能夠看到一個 Unix signal 列表,在這裏咱們會監聽 SIGSTOP 信號。這個信號會在進程收到了一個不可避免的暫停指令後發出。當咱們使用 LLDB debugger 來 debug 咱們的 APP 的時候,其實也是發送了這個信號。
咱們在 PhotoCollectionViewController.m 添加下面的代碼:
-(void)viewDidLoad { [super viewDidLoad]; // 1 #if DEBUG // 2 dispatch_queue_t queue = dispatch_get_main_queue(); // 3 static dispatch_source_t source = nil; // 4 __typeof(self) __weak weakSelf = self; // 5 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 6 source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGSTOP, 0, queue); // 7 if (source) { // 8 dispatch_source_set_event_handler(source, ^{ // 9 NSLog(@"Hi, I am: %@", weakSelf); }); dispatch_resume(source); // 10 } }); #endif // The other stuff
咱們來一步步看一下上面代碼的做用:
註釋1:這段代碼最好是在 Debug 模式下使用。
註釋2:聲明 queue,注意這裏的 queue 就是主隊列。
註釋3:聲明 dispatch_source。
註釋4:使用 weakSelf 是爲了保證沒有 retain 循環。在這裏使用 weakSelf 也不是必定須要的,由於 PhotoCollectionViewController 這個類在 App 的整個聲明週期都會存在。可是,若是咱們在使用過程當中有一些類(View Controller)會disappear,weakSelf 任然能保證沒有 retain 循環。
註釋5:使用 dispatch_once ,讓 dispatch source 只執行一次。
註釋6:給 source 賦值,參考上面咱們對 dispatchsourcecreate 方法的介紹。第二個參數表示,咱們要監聽 SIGSTOP 信號。
註釋7:若是使用了不正確的參數,dispatchsourcecreate 是不能執行成功的。因此咱們在這裏進行檢查。
註釋8:dispatchsourceseteventhandler 會在咱們收到監聽的信號時調用。
註釋10:默認狀況下,全部的 dispatch sources 是暫停狀態的。咱們在這裏讓 source 恢復,以便開始監控其餘事件。
如今運行程序,而後在 Debugger 中暫停,而後再繼續運行。
咱們能看到在點擊繼續時候,控制檯 log 了上面的文字。如今咱們的代碼可以檢查到進入了 debug 狀態。
咱們能夠用這個方法來 debug 對象,並在 resume 的時候顯示數據。還有一個有趣的使用方法:咱們能夠用它來作堆棧追蹤工具,在 debugger 裏面找到咱們想要操做的對象。
使用這個方法咱們能夠隨時中止 debugger,而後讓代碼在咱們要執行的位置執行。
例如,咱們能夠在上面代碼 NSLog 位置設置一個斷點,暫停一下,而後繼續,應用就會觸發咱們剛剛添加的斷點。在這裏咱們就能夠訪問當前這個類的變量了。
如今在 debugger 中輸入
po [[weakSelf navigationItem] setPrompt:@"HAHAH!"]
能夠看到咱們在沒有改變代碼的前提下,Navigation Bar 上的文字被改變了。
使用這種方法,咱們能夠直接更新當前 UI,查詢當前類的值,甚至執行一個方法,restart 應用。
Bingo!