多線程

https://github.com/iOS-Mayday/heji/blob/main/GCD%E9%9D%A2%E8%AF%95%E8%A6%81%E7%82%B9.md

一、線程概述

? ? 有些程序是一條直線,起點(diǎn)到終點(diǎn)——如簡(jiǎn)單的hello world,運(yùn)行打印完,它的生命周期便結(jié)束了,像曇花一現(xiàn)。

? ? 有些程序是一個(gè)圓,直到循環(huán)將它切斷——像操作系統(tǒng),一直運(yùn)行,直到你關(guān)機(jī)。

? ??一個(gè)運(yùn)行著的程序就是一個(gè)進(jìn)程或者叫做一個(gè)任務(wù),一個(gè)進(jìn)程至少包含一個(gè)線程,線程就是程序的執(zhí)行流。

? ??Mac和IOS中的程序啟動(dòng),創(chuàng)建好一個(gè)進(jìn)程的同時(shí),一個(gè)線程便開(kāi)始運(yùn)作,這個(gè)線程叫做主線程。主線程在程序中的位置和其他線程不同,它是其他線程最終的父線程,且所有的界面的顯示操作即AppKit或UIKit的操作必須在主線程進(jìn)行。

? ? 系統(tǒng)中每一個(gè)進(jìn)程都有自己獨(dú)立的虛擬內(nèi)存空間,而同一個(gè)進(jìn)程中的多個(gè)線程則公用進(jìn)程的內(nèi)存空間。

? ? 每創(chuàng)建一個(gè)新的進(jìn)程,都需要一些內(nèi)存(如每個(gè)線程有自己的stack空間)和消耗一定的CPU時(shí)間。

? ? 當(dāng)多個(gè)進(jìn)程對(duì)同一個(gè)資源出現(xiàn)爭(zhēng)奪的時(shí)候需要注意線程安全問(wèn)題。

? ? 創(chuàng)建線程:創(chuàng)建一個(gè)新的線程就是給進(jìn)程增加一個(gè)執(zhí)行流,所以新建一個(gè)線程需要提供一個(gè)函數(shù)或者方法作為線程的進(jìn)口。

? ? 概要提示:

? ? iPhone中的線程應(yīng)用并不是無(wú)節(jié)制的,官方給出的資料顯示,iPhone OS下的主線程的堆棧大小是1M,第二個(gè)線程開(kāi)始就是512KB,并且該值不能通過(guò)編譯器開(kāi)關(guān)或線程API函數(shù)來(lái)更改,只有主線程有直接修改UI的能力。

二、簡(jiǎn)介

? ? iOS有三種多線程編程的技術(shù),分別是:

? (一)NSThread

? (二)Cocoa NSOperation

? (三)GCD(全稱:Grand Central Dispatch)


? ? 這三種編程方式從上到下,抽象度層次是從低到高的,抽象度越高的使用越簡(jiǎn)單,也是Apple最推薦使用的。


三、三種方式的優(yōu)缺點(diǎn)


1)NSThread:

? ? 優(yōu)點(diǎn):NSThread 比其他兩個(gè)輕量級(jí)

? ? 缺點(diǎn):需要自己管理線程的生命周期,線程同步。線程同步對(duì)數(shù)據(jù)的加鎖會(huì)有一定的系統(tǒng)開(kāi)銷(xiāo)。


? ? NSThread實(shí)現(xiàn)的技術(shù)有下面三種:

? ??一般使用cocoa thread 技術(shù)。


? ? 2)Cocoa NSOperation

? ? 優(yōu)點(diǎn):不需要關(guān)心線程管理,數(shù)據(jù)同步的事情,可以把精力放在自己需要執(zhí)行的操作上。

? ? Cocoa operation 相關(guān)的類是 NSOperation ,NSOperationQueue。

? ? NSOperation是個(gè)抽象類,使用它必須用它的子類,可以實(shí)現(xiàn)它或者使用它定義好的兩個(gè)子類:NSInvocationOperation 和 NSBlockOperation。

? ? 創(chuàng)建NSOperation子類的對(duì)象,把對(duì)象添加到NSOperationQueue隊(duì)列里執(zhí)行。


? ? 3)GCD

? ? Grand Central Dispatch (GCD)是Apple開(kāi)發(fā)的一個(gè)多核編程的解決方法。在iOS4.0開(kāi)始之后才能使用。

? ? GCD是一個(gè)替代諸如NSThread, NSOperationQueue, NSInvocationOperation等技術(shù)的很高效和強(qiáng)大的技術(shù)?,F(xiàn)在的iOS系統(tǒng)都升級(jí)到7了,所以不用擔(dān)心該技術(shù)不能使用。


?線程之間的通訊

? ? 利用NSObject的一些類方法就可以做到。

? ??在應(yīng)用程序主線程中做事情:

1- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;2- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;

? ??在指定線程中做事情:

1- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;2- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;

? ??在當(dāng)前線程中做事情:

1- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;2- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)array;

? ??取消發(fā)送給當(dāng)前線程的某個(gè)消息:

1?cancelPreviousPerformRequestsWithTarget:?2?cancelPreviousPerformRequestsWithTarget:selector:object:

四、三種編程技術(shù)的使用


(一)NSThread的使用

?NSThread有兩種創(chuàng)建方式:

1- (id)initWithTarget:(id)target selector:(SEL)selectorobject:(id)argument 2+ (void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument

第一個(gè)是實(shí)例方法,第二個(gè)是類方法。使用方式如下:

11、[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:nil];? 232、NSThread* myThread = [[NSThread alloc] initWithTarget:self? 4? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? selector:@selector(doSomething:)? 5object:nil];? 6[myThread start];

參數(shù)的意義:

selector:線程執(zhí)行的方法,這個(gè)selector只能有一個(gè)參數(shù),而且不能有返回值。

target:selector消息發(fā)送的對(duì)象

object:傳輸給target的唯一參數(shù),也可以是nil


第一種方式會(huì)直接創(chuàng)建線程并且開(kāi)始運(yùn)行線程,第二種方式是先創(chuàng)建線程對(duì)象,然后再運(yùn)行線程操作,在運(yùn)行線程操作前可以設(shè)置線程的優(yōu)先級(jí)等線程信息。


不顯式創(chuàng)建線程的方法:

1?[Obj performSelectorInBackground:@selector(doSomething) withObject:nil];?


下載圖片的例子:

新建SingleViewApp項(xiàng)目,并在xib文件上放置一個(gè)imageView控件。按住control鍵拖到viewController.h文件中創(chuàng)建imageView IBOutlet?ViewController.m中實(shí)現(xiàn):?

?Code

線程間通訊

線程下載完圖片后怎么通知主線程更新界面呢?

1?[self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];?


performSelectorOnMainThread是NSObject的方法,除了可以更新主線程的數(shù)據(jù)外,還可以更新其他線程的比如:

1?performSelector:onThread:withObject:waitUntilDone:


運(yùn)行下載圖片:


線程同步

我們演示一個(gè)經(jīng)典的賣(mài)票的例子來(lái)講NSThread的線程同步:?

1#import 2 3@class ViewController;? 4 5@interfaceAppDelegate : UIResponder 6{? ? 7int tickets;? ? 8int count;? ? 9NSThread* ticketsThreadone;? 10NSThread* ticketsThreadtwo;? 11NSCondition* ticketsCondition;? 12NSLock *theLock;? 13}? 14@property (strong, nonatomic) UIWindow *window;? 1516@property (strong, nonatomic) ViewController *viewController;? 1718@end

1- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions? ? 2{? ? 3 4tickets =100;? ? 5count =0;? ? 6theLock = [[NSLock alloc] init];? ? 7// 鎖對(duì)象? ? 8ticketsCondition = [[NSCondition alloc] init];? ? 9ticketsThreadone = [[NSThread alloc] initWithTarget:self selector:@selector(run)object:nil];? 10[ticketsThreadone setName:@"Thread-1"];? 11? ? [ticketsThreadone start];1213ticketsThreadtwo = [[NSThread alloc] initWithTarget:self selector:@selector(run)object:nil];? 14[ticketsThreadtwo setName:@"Thread-2"];? 15? ? [ticketsThreadtwo start]; 1617self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];? 18// Override point for customization after application launch.? 19self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];? 20self.window.rootViewController = self.viewController;? 21? ? [self.window makeKeyAndVisible];? 22return YES;? 23}? 2425- (void)run{? 26while (TRUE) {? 27// 上鎖? 28//? ? ? ? [ticketsCondition lock];? 29[theLocklock];? 30if(tickets >=0){? 31[NSThread sleepForTimeInterval:0.09];? 32count =100- tickets;? 33NSLog(@"當(dāng)前票數(shù)是:%d,售出:%d,線程名:%@",tickets,count,[[NSThread currentThread] name]);? 34tickets--;? 35}else{? 36break;? 37? ? ? ? }? 38? ? ? ? [theLock unlock];? 39//? ? ? ? [ticketsCondition unlock];? 40? ? }? 41}

?如果沒(méi)有線程同步的lock,賣(mài)票數(shù)可能是-1.加上lock加上lock之后線程同步保證了數(shù)據(jù)的正確性。

上面例子我使用了兩種鎖,一種NSCondition ,一種是:NSLock。 NSCondition我已經(jīng)注釋了。

線程的順序執(zhí)行

他們都可以通過(guò)[ticketsCondition signal]; 發(fā)送信號(hào)的方式,在一個(gè)線程喚醒另外一個(gè)線程的等待。比如:

?View Code

wait是等待,我加了一個(gè) 線程3 去喚醒其他兩個(gè)線程鎖中的wait


其他同步

我們可以使用指令 @synchronized 來(lái)簡(jiǎn)化 NSLock的使用,這樣我們就不必顯示編寫(xiě)創(chuàng)建NSLock,加鎖并解鎖相關(guān)代碼。

1- (void)doSomeThing:(id)anObj 2{ 3? ? @synchronized(anObj) 4? ? { 5// Everything between the braces is protected by the @synchronized directive. 6? ? } 7}

還有其他的一些鎖對(duì)象,比如:循環(huán)鎖NSRecursiveLock,條件鎖NSConditionLock,分布式鎖NSDistributedLock等等,可以自己看官方文檔學(xué)習(xí)。

NSThread下載圖片的例子代碼:http://download.csdn.net/detail/totogo2010/4591149

(二)Cocoa Operation的使用

NSOperation實(shí)例封裝了需要執(zhí)行的操作和執(zhí)行操作所需的數(shù)據(jù),并且能夠以并發(fā)或非并發(fā)的方式執(zhí)行這個(gè)操作。NSOperation本身是抽象基類,因此必須使用它的子類,使用NSOperation子類的方式有2種:

1> Foundation框架提供了兩個(gè)具體子類直接供我們使用:NSInvocationOperation和NSBlockOperation

2> 自定義子類繼承NSOperation,實(shí)現(xiàn)內(nèi)部相應(yīng)的方法

執(zhí)行操作:

NSOperation調(diào)用start方法即可開(kāi)始執(zhí)行操作,NSOperation對(duì)象默認(rèn)按同步方式執(zhí)行,也就是在調(diào)用start方法的那個(gè)線程中直接執(zhí)行。NSOperation對(duì)象的isConcurrent方法會(huì)告訴我們這個(gè)操作相對(duì)于調(diào)用start方法的線程,是同步還是異步執(zhí)行。isConcurrent方法默認(rèn)返回NO,表示操作與調(diào)用線程同步執(zhí)行。

取消操作:

operation開(kāi)始執(zhí)行之后, 默認(rèn)會(huì)一直執(zhí)行操作直到完成,我們也可以調(diào)用cancel方法中途取消操作。

1?[operation cancel];?

監(jiān)聽(tīng)操作的執(zhí)行:

如果我們想在一個(gè)NSOperation執(zhí)行完畢后做一些事情,就調(diào)用NSOperation的setCompletionBlock方法來(lái)設(shè)置想做的事情。

1operation.completionBlock = ^() {? 2NSLog(@"執(zhí)行完畢");? 3};45或者67[operation setCompletionBlock:^() {? 8NSLog(@"執(zhí)行完畢");? 9}];

1)NSInvocationOperation

基于一個(gè)對(duì)象和selector來(lái)創(chuàng)建操作。如果你已經(jīng)有現(xiàn)有的方法來(lái)執(zhí)行需要的任務(wù),就可以使用這個(gè)類。

創(chuàng)建并執(zhí)行操作:

1// 這個(gè)操作是:調(diào)用self的run方法2NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run)object:nil];3// 開(kāi)始執(zhí)行任務(wù)(同步執(zhí)行)4[operation start];


例子:

這里同樣,我們實(shí)現(xiàn)一個(gè)下載圖片的例子。新建一個(gè)Single View app,拖放一個(gè)ImageView控件到xib界面。

實(shí)現(xiàn)代碼如下:

1#import"ViewController.h" 2#definekURL @"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"? ? 3 4@interface ViewController ()? ? 5 6@end 7 8@implementation ViewController? ? 910- (void)viewDidLoad? 11{? 12? ? [super viewDidLoad];? 13NSInvocationOperation *operation = [[NSInvocationOperation alloc]initWithTarget:self? 14? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? selector:@selector(downloadImage:)? 15object:kURL];? 1617NSOperationQueue *queue = [[NSOperationQueue alloc]init];? 18? ? [queue addOperation:operation];? 19// Do any additional setup after loading the view, typically from a nib.? 20} 2122-(void)downloadImage:(NSString *)url{? 23NSLog(@"url:%@", url);? 24NSURL *nsUrl = [NSURL URLWithString:url];? 25NSData *data = [[NSData alloc]initWithContentsOfURL:nsUrl];? 26UIImage * image = [[UIImage alloc]initWithData:data];? 27? ? [self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];? 28}? 2930-(void)updateUI:(UIImage*) image{? 31self.imageView.image = image;? 32}

代碼注釋:

1.viewDidLoad方法里可以看到我們用NSInvocationOperation建了一個(gè)后臺(tái)線程,并且放到NSOperationQueue中。后臺(tái)線程執(zhí)行downloadImage方法。

2.downloadImage 方法處理下載圖片的邏輯。下載完成后用performSelectorOnMainThread執(zhí)行主線程updateUI方法。updateUI 并把下載的圖片顯示到圖片控件中。

運(yùn)行可以看到下載圖片顯示在界面上。

2)NSBlockOperation

能夠并發(fā)地執(zhí)行一個(gè)或多個(gè)block對(duì)象,所有相關(guān)的block都執(zhí)行完之后,操作才算完成。

創(chuàng)建并執(zhí)行操作:

1NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^(){2NSLog(@"執(zhí)行了一個(gè)新的操作,線程:%@", [NSThread currentThread]);3}];4// 開(kāi)始執(zhí)行任務(wù)(這里還是同步執(zhí)行)5[operation start];

通過(guò)addExecutionBlock方法添加block操作:

1NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^(){ 2? ? NSLog(@"執(zhí)行第1次操作,線程:%@", [NSThread currentThread]); 3}]; 4 5[operation addExecutionBlock:^() { 6? ? NSLog(@"又執(zhí)行了1個(gè)新的操作,線程:%@", [NSThread currentThread]); 7}]; 8 9[operation addExecutionBlock:^() {10? ? NSLog(@"又執(zhí)行了1個(gè)新的操作,線程:%@", [NSThread currentThread]);11}];1213[operation addExecutionBlock:^() {14? ? NSLog(@"又執(zhí)行了1個(gè)新的操作,線程:%@", [NSThread currentThread]);15}];1617// 開(kāi)始執(zhí)行任務(wù)18[operation start];

打印信息如下:

12013-02-0221:38:46.102thread[4602:c07] 又執(zhí)行了1個(gè)新的操作,線程:{name = (null), num =1}22013-02-0221:38:46.102thread[4602:3f03] 又執(zhí)行了1個(gè)新的操作,線程:{name = (null), num =5}32013-02-0221:38:46.102thread[4602:1b03] 執(zhí)行第1次操作,線程:{name = (null), num =3}42013-02-0221:38:46.102thread[4602:1303] 又執(zhí)行了1個(gè)新的操作,線程:{name = (null), num =4}

可以看出,這4個(gè)block是并發(fā)執(zhí)行的,也就是在不同線程中執(zhí)行的,num屬性可以看成是線程的id。

3)自定義NSOperation

如果NSInvocationOperation和NSBlockOperation對(duì)象不能滿足需求, 你可以直接繼承NSOperation, 并添加任何你想要的行為。繼承所需的工作量主要取決于你要實(shí)現(xiàn)非并發(fā)還是并發(fā)的NSOperation。定義非并發(fā)的NSOperation要簡(jiǎn)單許多,只需要重載-(void)main這個(gè)方法,在這個(gè)方法里面執(zhí)行主任務(wù),并正確地響應(yīng)取消事件; 對(duì)于并發(fā)NSOperation, 你必須重寫(xiě)NSOperation的多個(gè)基本方法進(jìn)行實(shí)現(xiàn)(這里暫時(shí)先介紹非并發(fā)的NSOperation)。

非并發(fā)的NSOperation:

比如叫做DownloadOperation,用來(lái)下載圖片。

1> 繼承NSOperation,重寫(xiě)main方法,執(zhí)行主任務(wù)

DownloadOperation.h

1#import 2@protocol DownloadOperationDelegate; 3 4@interface DownloadOperation : NSOperation 5// 圖片的url路徑 6@property (nonatomic, copy) NSString *imageUrl; 7// 代理 8@property (nonatomic, retain)iddelegate; 910- (id)initWithUrl:(NSString *)urldelegate:(id)delegate;11@end1213// 圖片下載的協(xié)議14@protocolDownloadOperationDelegate 15- (void)downloadFinishWithImage:(UIImage *)image;16@end

DownloadOperation.m

1#import"DownloadOperation.h" 2 3@implementation DownloadOperation 4@synthesizedelegate= _delegate; 5@synthesizeimageUrl = _imageUrl; 6 7// 初始化 8- (id)initWithUrl:(NSString *)urldelegate:(id)delegate { 9if(self = [super init]) {10self.imageUrl = url;11self.delegate=delegate;12? ? }13return self;14}15// 釋放內(nèi)存16- (void)dealloc {17? ? [super dealloc];18? ? [_delegate release];19? ? [_imageUrl release];20}2122// 執(zhí)行主任務(wù)23- (void)main {24// 新建一個(gè)自動(dòng)釋放池,如果是異步執(zhí)行操作,那么將無(wú)法訪問(wèn)到主線程的自動(dòng)釋放池25? ? @autoreleasepool {26// ....27? ? }28}29@end

2> 正確響應(yīng)取消事件

operation開(kāi)始執(zhí)行之后,會(huì)一直執(zhí)行任務(wù)直到完成,或者顯式地取消操作。取消可能發(fā)生在任何時(shí)候,甚至在operation執(zhí)行之前。盡管NSOperation提供了一個(gè)方法,讓?xiě)?yīng)用取消一個(gè)操作,但是識(shí)別出取消事件則是我們自己的事情。如果operation直接終止, 可能無(wú)法回收所有已分配的內(nèi)存或資源。因此operation對(duì)象需要檢測(cè)取消事件,并優(yōu)雅地退出執(zhí)行

NSOperation對(duì)象需要定期地調(diào)用isCancelled方法檢測(cè)操作是否已經(jīng)被取消,如果返回YES(表示已取消),則立即退出執(zhí)行。不管是自定義NSOperation子類,還是使用系統(tǒng)提供的兩個(gè)具體子類,都需要支持取消。isCancelled方法本身非常輕量,可以頻繁地調(diào)用而不產(chǎn)生大的性能損失。

以下地方可能需要調(diào)用isCancelled:

* 在執(zhí)行任何實(shí)際的工作之前

* 在循環(huán)的每次迭代過(guò)程中,如果每個(gè)迭代相對(duì)較長(zhǎng)可能需要調(diào)用多次

* 代碼中相對(duì)比較容易中止操作的任何地方

DownloadOperation的main方法實(shí)現(xiàn)如下:

1- (void)main { 2// 新建一個(gè)自動(dòng)釋放池,如果是異步執(zhí)行操作,那么將無(wú)法訪問(wèn)到主線程的自動(dòng)釋放池 3? ? @autoreleasepool { 4if(self.isCancelled)return; 5 6// 獲取圖片數(shù)據(jù) 7NSURL *url = [NSURL URLWithString:self.imageUrl]; 8NSData *imageData = [NSData dataWithContentsOfURL:url]; 910if (self.isCancelled) {11url = nil;12imageData = nil;13return;14? ? ? ? }1516// 初始化圖片17UIImage *image = [UIImage imageWithData:imageData];1819if (self.isCancelled) {20image = nil;21return;22? ? ? ? }2324if([self.delegate respondsToSelector:@selector(downloadFinishWithImage:)]) {25// 把圖片數(shù)據(jù)傳回到主線程26[(NSObject *)self.delegate performSelectorOnMainThread:@selector(downloadFinishWithImage:) withObject:image waitUntilDone:NO];27? ? ? ? }28? ? }29}

如何控制線程池中的線程數(shù)?

隊(duì)列里可以加入很多個(gè)NSOperation, 可以把NSOperationQueue看作一個(gè)線程池,可往線程池中添加操作(NSOperation)到隊(duì)列中。線程池中的線程可看作消費(fèi)者,從隊(duì)列中取走操作,并執(zhí)行它。


通過(guò)下面的代碼設(shè)置:

1?[queue setMaxConcurrentOperationCount:5];


線程池中的線程數(shù),也就是并發(fā)操作數(shù)。默認(rèn)情況下是-1,-1表示沒(méi)有限制,這樣會(huì)同時(shí)運(yùn)行隊(duì)列中的全部的操作。


(三)GCD的使用

?Grand Central Dispatch 簡(jiǎn)稱(GCD)是蘋(píng)果公司開(kāi)發(fā)的技術(shù),以優(yōu)化的應(yīng)用程序支持多核心處理器和其他的對(duì)稱多處理系統(tǒng)的系統(tǒng)。這建立在任務(wù)并行執(zhí)行的線程池模式的基礎(chǔ)上的。它首次發(fā)布在Mac OS X 10.6 ,iOS 4及以上也可用。

GCD 是 libdispatch 的市場(chǎng)名稱,而 libdispatch 作為 Apple 的一個(gè)庫(kù),為并發(fā)代碼在多核硬件(跑 iOS 或 OS X )上執(zhí)行提供有力支持。它具有以下優(yōu)點(diǎn):

1.GCD 能通過(guò)推遲昂貴計(jì)算任務(wù)并在后臺(tái)運(yùn)行它們來(lái)改善你的應(yīng)用的響應(yīng)性能。

2.GCD 提供一個(gè)易于使用的并發(fā)模型而不僅僅只是鎖和線程,以幫助我們避開(kāi)并發(fā)陷阱。

3.GCD 具有在常見(jiàn)模式(例如單例)上用更高性能的原語(yǔ)優(yōu)化你的代碼的潛在能力。


先回顧幾個(gè)線程相關(guān)的概念:

Serial vs. Concurrent 串行 vs. 并發(fā)

這些術(shù)語(yǔ)描述當(dāng)任務(wù)相對(duì)于其它任務(wù)被執(zhí)行,任務(wù)串行執(zhí)行就是每次只有一個(gè)任務(wù)被執(zhí)行,任務(wù)并發(fā)執(zhí)行就是在同一時(shí)間可以有多個(gè)任務(wù)被執(zhí)行。?


Synchronous vs. Asynchronous 同步 vs. 異步

在 GCD 中,這些術(shù)語(yǔ)描述當(dāng)一個(gè)函數(shù)相對(duì)于另一個(gè)任務(wù)完成,此任務(wù)是該函數(shù)要求 GCD 執(zhí)行的。一個(gè)同步函數(shù)只在完成了它預(yù)定的任務(wù)后才返回。一個(gè)異步函數(shù),剛好相反,會(huì)立即返回,預(yù)定的任務(wù)會(huì)完成但不會(huì)等它完成。因此,一個(gè)異步函數(shù)不會(huì)阻塞當(dāng)前線程去執(zhí)行下一個(gè)函數(shù)。


Critical Section 臨界區(qū)

每個(gè)進(jìn)程中訪問(wèn)臨界資源的那段代碼稱為臨界區(qū)(Critical Section)(臨界資源是一次僅允許一個(gè)進(jìn)程使用的共享資源)。每次只準(zhǔn)許一個(gè)進(jìn)程進(jìn)入臨界區(qū),進(jìn)入后不允許其他進(jìn)程進(jìn)入。不論是硬件臨界資源,還是軟件臨界資源,多個(gè)進(jìn)程必須互斥地對(duì)它進(jìn)行訪問(wèn)。


Race Condition 競(jìng)態(tài)條件

當(dāng)兩個(gè)線程競(jìng)爭(zhēng)同一資源時(shí),如果對(duì)資源的訪問(wèn)順序敏感(即線程訪問(wèn)資源的順序會(huì)導(dǎo)致不同的結(jié)果),就稱存在競(jìng)態(tài)條件。導(dǎo)致競(jìng)態(tài)條件發(fā)生的代碼區(qū)稱作臨界區(qū)。在臨界區(qū)中使用適當(dāng)?shù)耐骄涂梢员苊飧?jìng)態(tài)條件。


Deadlock 死鎖

兩個(gè)(有時(shí)更多)東西——在大多數(shù)情況下,是線程——所謂的死鎖是指它們都卡住了,并等待對(duì)方完成或執(zhí)行其它操作。第一個(gè)不能完成是因?yàn)樗诘却诙€(gè)的完成。但第二個(gè)也不能完成,因?yàn)樗诘却谝粋€(gè)的完成。


Thread Safe 線程安全

線程安全的代碼能在多線程或并發(fā)任務(wù)中被安全的調(diào)用,而不會(huì)導(dǎo)致任何問(wèn)題(數(shù)據(jù)損壞,崩潰,等)。線程不安全的代碼在某個(gè)時(shí)刻只能在一個(gè)上下文中運(yùn)行。一個(gè)線程安全代碼的例子是 NSDictionary 。你可以在同一時(shí)間在多個(gè)線程中使用它而不會(huì)有問(wèn)題。另一方面,NSMutableDictionary 就不是線程安全的,應(yīng)該保證一次只能有一個(gè)線程訪問(wèn)它。


Context Switch 上下文切換

一個(gè)上下文切換指當(dāng)你在單個(gè)進(jìn)程里切換執(zhí)行不同的線程時(shí)存儲(chǔ)與恢復(fù)執(zhí)行狀態(tài)的過(guò)程。這個(gè)過(guò)程在編寫(xiě)多任務(wù)應(yīng)用時(shí)很普遍,但會(huì)帶來(lái)一些額外的開(kāi)銷(xiāo)。


Concurrency vs Parallelism 并發(fā)與并行

并發(fā)行和并行性的區(qū)別可以用饅頭做比喻。前者相當(dāng)于一個(gè)人同時(shí)吃三個(gè)饅頭和三個(gè)人同時(shí)吃一個(gè)饅頭。

并發(fā)性(Concurrence):指兩個(gè)或兩個(gè)以上的事件或活動(dòng)在同一時(shí)間間隔內(nèi)發(fā)生。并發(fā)的實(shí)質(zhì)是物理CPU(也可以多個(gè)物理CPU) 在若干道程序之間多路復(fù)用,并發(fā)性是對(duì)有限物理資源強(qiáng)制行使多用戶共享以提高效率。

并行性(parallelism)指兩個(gè)或兩個(gè)以上事件或活動(dòng)在同一時(shí)刻發(fā)生。在多道程序環(huán)境下,并行性使多個(gè)程序同一時(shí)刻可在不同CPU上同時(shí)執(zhí)行。

區(qū)別:一個(gè)處理器同時(shí)處理多個(gè)任務(wù)和多個(gè)處理器或者是多核的處理器同時(shí)處理多個(gè)不同的任務(wù)。

前者是邏輯上的同時(shí)發(fā)生(simultaneous),而后者是物理上的同時(shí)發(fā)生。

兩者的聯(lián)系:并行的事件或活動(dòng)一定是并發(fā)的,但反之并發(fā)的事件或活動(dòng)未必是并行的。并行性是并發(fā)性的特例,而并發(fā)性是并行性的擴(kuò)展。

Queues 隊(duì)列

GCD 提供有 dispatch queues 來(lái)處理代碼塊,這些隊(duì)列管理你提供給 GCD 的任務(wù)并用 FIFO 順序執(zhí)行這些任務(wù)。這就保證了第一個(gè)被添加到隊(duì)列里的任務(wù)會(huì)是隊(duì)列中第一個(gè)開(kāi)始的任務(wù),而第二個(gè)被添加的任務(wù)將第二個(gè)開(kāi)始,如此直到隊(duì)列的終點(diǎn)。

所有的調(diào)度隊(duì)列(dispatch queues)自身都是線程安全的,你能從多個(gè)線程并行的訪問(wèn)它們。 GCD 的優(yōu)點(diǎn)是顯而易見(jiàn)的,即當(dāng)你了解了調(diào)度隊(duì)列如何為你自己代碼的不同部分提供線程安全。關(guān)于這一點(diǎn)的關(guān)鍵是選擇正確類型的調(diào)度隊(duì)列和正確的調(diào)度函數(shù)來(lái)提交你的工作。

Serial Queues 串行隊(duì)列

這些任務(wù)的執(zhí)行時(shí)機(jī)受到 GCD 的控制;唯一能確保的事情是 GCD 一次只執(zhí)行一個(gè)任務(wù),并且按照我們添加到隊(duì)列的順序來(lái)執(zhí)行。

由于在串行隊(duì)列中不會(huì)有兩個(gè)任務(wù)并發(fā)運(yùn)行,因此不會(huì)出現(xiàn)同時(shí)訪問(wèn)臨界區(qū)的風(fēng)險(xiǎn);相對(duì)于這些任務(wù)來(lái)說(shuō),這就從競(jìng)態(tài)條件下保護(hù)了臨界區(qū)。所以如果訪問(wèn)臨界區(qū)的唯一方式是通過(guò)提交到調(diào)度隊(duì)列的任務(wù),那么你就不需要擔(dān)心臨界區(qū)的安全問(wèn)題了。

Concurrent Queues 并發(fā)隊(duì)列

注意 Block 1,2 和 3 都立馬開(kāi)始運(yùn)行,一個(gè)接一個(gè)。在 Block 0 開(kāi)始后,Block 1等待了好一會(huì)兒才開(kāi)始。同樣, Block 3 在 Block 2 之后才開(kāi)始,但它先于 Block 2 完成。

在并發(fā)隊(duì)列中的任務(wù)能得到的保證是它們會(huì)按照被添加的順序開(kāi)始執(zhí)行,但這就是全部的保證了。任務(wù)可能以任意順序完成,你不會(huì)知道何時(shí)開(kāi)始運(yùn)行下一個(gè)任務(wù),或者任意時(shí)刻有多少 Block 在運(yùn)行。再說(shuō)一遍,這完全取決于 GCD 。

何時(shí)開(kāi)始一個(gè) Block 完全取決于 GCD 。如果一個(gè) Block 的執(zhí)行時(shí)間與另一個(gè)重疊,也是由 GCD 來(lái)決定是否將其運(yùn)行在另一個(gè)不同的核心上,如果那個(gè)核心可用,否則就用上下文切換的方式來(lái)執(zhí)行不同的 Block 。

設(shè)計(jì):

GCD的工作原理是:讓程序平行排隊(duì)的特定任務(wù),根據(jù)可用的處理資源,安排他們?cè)谌魏慰捎玫奶幚砥骱诵纳蠄?zhí)行任務(wù)。


一個(gè)任務(wù)可以是一個(gè)函數(shù)(function)或者是一個(gè)block。 GCD的底層依然是用線程實(shí)現(xiàn),不過(guò)這樣可以讓程序員不用關(guān)注實(shí)現(xiàn)的細(xì)節(jié)。


GCD中的FIFO隊(duì)列稱為dispatch queue,它可以保證先進(jìn)來(lái)的任務(wù)先得到執(zhí)行。


dispatch queue分為下面三種:

Serial:又稱為private dispatch queues,同時(shí)只執(zhí)行一個(gè)任務(wù)。Serial queue通常用于同步訪問(wèn)特定的資源或數(shù)據(jù)。當(dāng)你創(chuàng)建多個(gè)Serial queue時(shí),雖然它們各自是同步執(zhí)行的,但Serial queue與Serial queue之間是并發(fā)執(zhí)行的。

Concurrent:又稱為global dispatch queue,可以并發(fā)地執(zhí)行多個(gè)任務(wù),但是執(zhí)行完成的順序是隨機(jī)的。

Main dispatch queue:它是全局可用的serial queue,它是在應(yīng)用程序主線程上執(zhí)行任務(wù)的。


Queue Types 隊(duì)列類型

首先,系統(tǒng)提供給你一個(gè)叫做 主隊(duì)列(main queue) 的特殊隊(duì)列。和其它串行隊(duì)列一樣,這個(gè)隊(duì)列中的任務(wù)一次只能執(zhí)行一個(gè)。然而,它能保證所有的任務(wù)都在主線程執(zhí)行,而主線程是唯一可用于更新 UI 的線程。這個(gè)隊(duì)列就是用于發(fā)生消息給 UIView 或發(fā)送通知的。


系統(tǒng)同時(shí)提供給你好幾個(gè)并發(fā)隊(duì)列。它們叫做 全局調(diào)度隊(duì)列(Global Dispatch Queues) 。目前的四個(gè)全局隊(duì)列有著不同的優(yōu)先級(jí):background、low、default 以及 high。要知道,Apple 的 API 也會(huì)使用這些隊(duì)列,所以你添加的任何任務(wù)都不會(huì)是這些隊(duì)列中唯一的任務(wù)。


最后,你也可以創(chuàng)建自己的串行隊(duì)列或并發(fā)隊(duì)列。這就是說(shuō),至少有五個(gè)隊(duì)列任你處置:主隊(duì)列、四個(gè)全局調(diào)度隊(duì)列,再加上任何你自己創(chuàng)建的隊(duì)列。

接下來(lái)我們來(lái)了解GCD的使用:

1、常用的方法dispatch_async

為了避免界面在處理耗時(shí)的操作時(shí)卡死,比如讀取網(wǎng)絡(luò)數(shù)據(jù),IO,數(shù)據(jù)庫(kù)讀寫(xiě)等,我們會(huì)在另外一個(gè)線程中處理這些操作,然后通知主線程更新界面。

用GCD實(shí)現(xiàn)這個(gè)流程的操作比前面介紹的NSThread ?NSOperation的方法都要簡(jiǎn)單。代碼框架結(jié)構(gòu)如下:

1dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{? 2// 耗時(shí)的操作? 3dispatch_async(dispatch_get_main_queue(), ^{? 4// 更新界面? 5? ? });? 6});

如果這樣還不清晰的話,那我們還是用上兩篇博客中的下載圖片為例子,代碼如下:

1dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{? ? 2NSURL * url = [NSURL URLWithString:@"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"];? ? 3NSData * data = [[NSData alloc]initWithContentsOfURL:url];? ? 4UIImage *image = [[UIImage alloc]initWithData:data];? ? 5if(data != nil) {? ? 6dispatch_async(dispatch_get_main_queue(), ^{? ? 7self.imageView.image = image;? ? 8? ? ? ? });? ? 9? ? }? 10});

運(yùn)行會(huì)顯示下載的圖片。

是不是代碼比NSThread 、NSOperation簡(jiǎn)潔很多,而且GCD會(huì)自動(dòng)根據(jù)任務(wù)在多核處理器上分配資源,優(yōu)化程序。

系統(tǒng)給每一個(gè)應(yīng)用程序提供了三個(gè)concurrent dispatch queues。這三個(gè)并發(fā)調(diào)度隊(duì)列是全局的,它們只有優(yōu)先級(jí)的不同。因?yàn)槭侨值?,我們不需要去?chuàng)建。我們只需要通過(guò)使用函數(shù)dispath_get_global_queue去得到隊(duì)列,如下:

1?dispatch_queue_t globalQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,?0);

這里也用到了系統(tǒng)默認(rèn)就有一個(gè)串行隊(duì)列main_queue:

1?dispatch_queue_t mainQ = dispatch_get_main_queue();

2、dispatch_group_async的使用

dispatch_group_async可以實(shí)現(xiàn)監(jiān)聽(tīng)一組任務(wù)是否完成,完成后得到通知執(zhí)行其他的操作。這個(gè)方法很有用,比如你執(zhí)行三個(gè)下載任務(wù),當(dāng)三個(gè)任務(wù)都下載完成后你才通知界面說(shuō)完成的了。下面是一段例子代碼:

1dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);? ? 2dispatch_group_t group = dispatch_group_create();? ? 3dispatch_group_async(group, queue, ^{? ? 4[NSThread sleepForTimeInterval:1];? ? 5NSLog(@"group1");? ? 6});? ? 7dispatch_group_async(group, queue, ^{? ? 8[NSThread sleepForTimeInterval:2];? ? 9NSLog(@"group2");? 10});? 11dispatch_group_async(group, queue, ^{? 12[NSThread sleepForTimeInterval:3];? 13NSLog(@"group3");? 14});? 15dispatch_group_notify(group, dispatch_get_main_queue(), ^{? 16NSLog(@"updateUi");? 17});? 18dispatch_release(group);

dispatch_group_async是異步的方法,運(yùn)行后可以看到打印結(jié)果:

12012-09-2516:04:16.737gcdTest[43328:11303] group1 22012-09-2516:04:17.738gcdTest[43328:12a1b] group2 32012-09-2516:04:18.738gcdTest[43328:13003] group3 42012-09-2516:04:18.739gcdTest[43328:f803] updateUi

每隔一秒打印一個(gè),當(dāng)?shù)谌齻€(gè)任務(wù)執(zhí)行后,upadteUi被打印。

3、dispatch_barrier_async的使用

dispatch_barrier_async是在前面的任務(wù)執(zhí)行結(jié)束后它才執(zhí)行,而且它后面的任務(wù)等它執(zhí)行完成之后才會(huì)執(zhí)行

例子代碼如下:

1dispatch_queue_t queue = dispatch_queue_create("gcdtest.rongfzh.yc", DISPATCH_QUEUE_CONCURRENT);? ? 2dispatch_async(queue, ^{? ? 3[NSThread sleepForTimeInterval:2];? ? 4NSLog(@"dispatch_async1");? ? 5});? ? 6dispatch_async(queue, ^{? ? 7[NSThread sleepForTimeInterval:4];? ? 8NSLog(@"dispatch_async2");? ? 9});? 10dispatch_barrier_async(queue, ^{? 11NSLog(@"dispatch_barrier_async");? 12[NSThread sleepForTimeInterval:4];? 1314});? 15dispatch_async(queue, ^{? 16[NSThread sleepForTimeInterval:1];? 17NSLog(@"dispatch_async3");? 18});

打印結(jié)果:

12012-09-2516:20:33.967gcdTest[45547:11203] dispatch_async1 22012-09-2516:20:35.967gcdTest[45547:11303] dispatch_async2 32012-09-2516:20:35.967gcdTest[45547:11303] dispatch_barrier_async 42012-09-2516:20:40.970gcdTest[45547:11303] dispatch_async3

請(qǐng)注意執(zhí)行的時(shí)間,可以看到執(zhí)行的順序如上所述。

4、dispatch_apply?

執(zhí)行某個(gè)代碼片段N次。

1?dispatch_apply(5, globalQ, ^(size_t index) {?2?//?執(zhí)行5次?3?});?


5、dispatch_after

?使用dispatch_after延后工作。

1- (void)showOrHideNavPrompt? 2{? 3NSUInteger count = [[PhotoManager sharedManager] photos].count;? 4doubledelayInSeconds =1.0;? 5dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));// 1? 6dispatch_after(popTime, dispatch_get_main_queue(), ^(void){// 2? 7if(!count) {? 8[self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"];? 9}else { 10? ? ? ? ? ? [self.navigationItem setPrompt:nil]; 11? ? ? ? } 12? ? }); 13}

編譯并運(yùn)行應(yīng)用。應(yīng)該有一個(gè)輕微地延遲,這有助于抓住用戶的注意力并展示所要做的事情。


dispatch_after 工作起來(lái)就像一個(gè)延遲版的 dispatch_async 。你依然不能控制實(shí)際的執(zhí)行時(shí)間,且一旦 dispatch_after 返回也就不能再取消它。主隊(duì)列是使用 dispatch_after 的好選擇,在其他隊(duì)列上使用要小心。


四、GCD實(shí)例

既然本教程的目標(biāo)是優(yōu)化且安全的使用 GCD 調(diào)用來(lái)自不同線程的代碼,那么你將從一個(gè)近乎完成的叫做 GooglyPuff 的項(xiàng)目入手。

GooglyPuff 是一個(gè)沒(méi)有優(yōu)化,線程不安全的應(yīng)用,它使用 Core Image 的人臉檢測(cè) API 來(lái)覆蓋一對(duì)曲棍球眼睛到被檢測(cè)到的人臉上。對(duì)于基本的圖像,可以從相機(jī)膠卷選擇,或用預(yù)設(shè)好的URL從互聯(lián)網(wǎng)下載。


點(diǎn)擊此處下載項(xiàng)目


完成項(xiàng)目下載之后,將其解壓到某個(gè)方便的目錄,再用 Xcode 打開(kāi)它并編譯運(yùn)行。這個(gè)應(yīng)用看起來(lái)如下圖所示:

注意當(dāng)你選擇 Le Internet 選項(xiàng)下載圖片時(shí),一個(gè) UIAlertView 過(guò)早地彈出。你將在本系列教程地第二部分修復(fù)這個(gè)問(wèn)題。


這個(gè)項(xiàng)目中有四個(gè)有趣的類:

1. PhotoCollectionViewController:它是應(yīng)用開(kāi)始的第一個(gè)視圖控制器。它用縮略圖展示所有選定的照片。

2. PhotoDetailViewController:它執(zhí)行添加曲棍球眼睛到圖像上的邏輯,并用一個(gè) UIScrollView 來(lái)顯示結(jié)果圖片。

3. Photo:這是一個(gè)類簇,它根據(jù)一個(gè) NSURL 的實(shí)例或一個(gè) ALAsset 的實(shí)例來(lái)實(shí)例化照片。這個(gè)類提供一個(gè)圖像、縮略圖以及從 URL 下載的狀態(tài)。

4. PhotoManager:它管理所有 Photo 的實(shí)例.


用 dispatch_async 處理后臺(tái)任務(wù)

回到應(yīng)用并從你的相機(jī)膠卷添加一些照片或使用 Le Internet 選項(xiàng)下載一些。


注意在按下 PhotoCollectionViewController 中的一個(gè) UICollectionViewCell 到生成一個(gè)新的 PhotoDetailViewController 之間花了多久時(shí)間;你會(huì)注意到一個(gè)明顯的滯后,特別是在比較慢的設(shè)備上查看很大的圖。


在重載 UIViewController 的 viewDidLoad 時(shí)容易加入太多雜波(too much clutter),這通常會(huì)引起視圖控制器出現(xiàn)前更長(zhǎng)的等待。如果可能,最好是卸下一些工作放到后臺(tái),如果它們不是絕對(duì)必須要運(yùn)行在加載時(shí)間里。


打開(kāi) PhotoDetailViewController 并用下面的實(shí)現(xiàn)替換 viewDidLoad:

1- (void)viewDidLoad? 2{? ? 3? ? [super viewDidLoad];? 4NSAssert(_image,@"Image not set; required to use view controller");? 5self.photoImageView.image = _image;? 6 7//Resize if neccessary to ensure it's not pixelated? 8if(_image.size.height <= self.photoImageView.bounds.size.height && 9_image.size.width <= self.photoImageView.bounds.size.width) { 10? ? ? ? [self.photoImageView setContentMode:UIViewContentModeCenter]; 11? ? } 1213dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0), ^{// 1 14UIImage *overlayImage = [self faceOverlayImageFromImage:_image]; 15dispatch_async(dispatch_get_main_queue(), ^{// 2 16[self fadeInNewImage:overlayImage];// 3 17? ? ? ? }); 18? ? }); 19}

下面來(lái)說(shuō)明上面的新代碼所做的事:

1. 你首先將工作從主線程移到全局線程。因?yàn)檫@是一個(gè) dispatch_async() ,Block 會(huì)被異步地提交,意味著調(diào)用線程地執(zhí)行將會(huì)繼續(xù)。這就使得 viewDidLoad 更早地在主線程完成,讓加載過(guò)程感覺(jué)起來(lái)更加快速。同時(shí),一個(gè)人臉檢測(cè)過(guò)程會(huì)啟動(dòng)并將在稍后完成。

2. 在這里,人臉檢測(cè)過(guò)程完成,并生成了一個(gè)新的圖像。既然你要使用此新圖像更新你的 UIImageView ,那么你就添加一個(gè)新的 Block 到主線程。記住——你必須總是在主線程訪問(wèn) UIKit 的類。

3. 最后,你用 fadeInNewImage: 更新 UI ,它執(zhí)行一個(gè)淡入過(guò)程切換到新的曲棍球眼睛圖像。

編譯并運(yùn)行你的應(yīng)用;選擇一個(gè)圖像然后你會(huì)注意到視圖控制器加載明顯變快,曲棍球眼睛稍微在之后就加上了。這給應(yīng)用帶來(lái)了不錯(cuò)的效果,和之前的顯示差別巨大。


進(jìn)一步,如果你試著加載一個(gè)超大的圖像,應(yīng)用不會(huì)在加載視圖控制器上“掛住”,這就使得應(yīng)用具有很好伸縮性。


正如之前提到的, dispatch_async 添加一個(gè) Block 都隊(duì)列就立即返回了。任務(wù)會(huì)在之后由 GCD 決定執(zhí)行。當(dāng)你需要在后臺(tái)執(zhí)行一個(gè)基于網(wǎng)絡(luò)或 CPU 緊張的任務(wù)時(shí)就使用 dispatch_async ,這樣就不會(huì)阻塞當(dāng)前線程。


下面是一個(gè)關(guān)于在 dispatch_async 上如何以及何時(shí)使用不同的隊(duì)列類型的快速指導(dǎo):

1. 自定義串行隊(duì)列:當(dāng)你想串行執(zhí)行后臺(tái)任務(wù)并追蹤它時(shí)就是一個(gè)好選擇。這消除了資源爭(zhēng)用,因?yàn)槟阒酪淮沃挥幸粋€(gè)任務(wù)在執(zhí)行。注意若你需要來(lái)自某個(gè)方法的數(shù)據(jù),你必須內(nèi)聯(lián)另一個(gè) Block 來(lái)找回它或考慮使用 dispatch_sync。

2. 主隊(duì)列(串行):這是在一個(gè)并發(fā)隊(duì)列上完成任務(wù)后更新 UI 的共同選擇。要這樣做,你將在一個(gè) Block 內(nèi)部編寫(xiě)另一個(gè) Block 。以及,如果你在主隊(duì)列調(diào)用 dispatch_async 到主隊(duì)列,你能確保這個(gè)新任務(wù)將在當(dāng)前方法完成后的某個(gè)時(shí)間執(zhí)行。

3. 并發(fā)隊(duì)列:這是在后臺(tái)執(zhí)行非 UI 工作的共同選擇。


使用 dispatch_after 延后工作

稍微考慮一下應(yīng)用的 UX 。是否用戶第一次打開(kāi)應(yīng)用時(shí)會(huì)困惑于不知道做什么?你是這樣嗎? :]


如果用戶的 PhotoManager 里還沒(méi)有任何照片,那么顯示一個(gè)提示會(huì)是個(gè)好主意!然而,你同樣要考慮用戶的眼睛會(huì)如何在主屏幕上瀏覽:如果你太快的顯示一個(gè)提示,他們的眼睛還徘徊在視圖的其它部分上,他們很可能會(huì)錯(cuò)過(guò)它。


顯示提示之前延遲一秒鐘就足夠捕捉到用戶的注意,他們此時(shí)已經(jīng)第一次看過(guò)了應(yīng)用。


添加如下代碼到到 PhotoCollectionViewController.m 中 showOrHideNavPrompt 的廢止實(shí)現(xiàn)里:

1- (void)showOrHideNavPrompt? 2{? 3NSUInteger count = [[PhotoManager sharedManager] photos].count;? 4doubledelayInSeconds =1.0;? 5dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));// 1? 6dispatch_after(popTime, dispatch_get_main_queue(), ^(void){// 2? 7if(!count) {? 8[self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"];? 9}else { 10? ? ? ? ? ? [self.navigationItem setPrompt:nil]; 11? ? ? ? } 12? ? }); 13}

showOrHideNavPrompt 在 viewDidLoad 中執(zhí)行,以及 UICollectionView 被重新加載的任何時(shí)候。按照注釋數(shù)字順序看看:

1. 你聲明了一個(gè)變量指定要延遲的時(shí)長(zhǎng)。

2. 然后等待 delayInSeconds 給定的時(shí)長(zhǎng),再異步地添加一個(gè) Block 到主線程。


編譯并運(yùn)行應(yīng)用。應(yīng)該有一個(gè)輕微地延遲,這有助于抓住用戶的注意力并展示所要做的事情。


dispatch_after 工作起來(lái)就像一個(gè)延遲版的 dispatch_async 。你依然不能控制實(shí)際的執(zhí)行時(shí)間,且一旦 dispatch_after 返回也就不能再取消它。


不知道何時(shí)適合使用 dispatch_after ?

1. 自定義串行隊(duì)列:在一個(gè)自定義串行隊(duì)列上使用 dispatch_after 要小心。你最好堅(jiān)持使用主隊(duì)列。

2. 主隊(duì)列(串行):是使用 dispatch_after 的好選擇;Xcode 提供了一個(gè)不錯(cuò)的自動(dòng)完成模版。

3. 并發(fā)隊(duì)列:在并發(fā)隊(duì)列上使用 dispatch_after 也要小心;你會(huì)這樣做就比較罕見(jiàn)。還是在主隊(duì)列做這些操作吧。


讓你的單例線程安全

單例,不論喜歡還是討厭,它們?cè)?iOS 上的流行情況就像網(wǎng)上的貓。 :]


一個(gè)常見(jiàn)的擔(dān)憂是它們常常不是線程安全的。這個(gè)擔(dān)憂十分合理,基于它們的用途:?jiǎn)卫31欢鄠€(gè)控制器同時(shí)訪問(wèn)。


單例的線程擔(dān)憂范圍從初始化開(kāi)始,到信息的讀和寫(xiě)。PhotoManager 類被實(shí)現(xiàn)為單例——它在目前的狀態(tài)下就會(huì)被這些問(wèn)題所困擾。要看看事情如何很快地失去控制,你將在單例實(shí)例上創(chuàng)建一個(gè)控制好的競(jìng)態(tài)條件。


導(dǎo)航到 PhotoManager.m 并找到 sharedManager ;它看起來(lái)如下:

1+ (instancetype)sharedManager? ? 2{ 3staticPhotoManager *sharedPhotoManager = nil; 4if(!sharedPhotoManager) { 5sharedPhotoManager = [[PhotoManager alloc] init]; 6sharedPhotoManager->_photosArray = [NSMutableArray array]; 7? ? } 8return sharedPhotoManager; 9}

當(dāng)前狀態(tài)下,代碼相當(dāng)簡(jiǎn)單;你創(chuàng)建了一個(gè)單例并初始化一個(gè)叫做 photosArray 的 NSMutableArray 屬性。


然而,if 條件分支不是線程安全的;如果你多次調(diào)用這個(gè)方法,有一個(gè)可能性是在某個(gè)線程(就叫它線程A)上進(jìn)入 if 語(yǔ)句塊并可能在 sharedPhotoManager 被分配內(nèi)存前發(fā)生一個(gè)上下文切換。然后另一個(gè)線程(線程B)可能進(jìn)入 if ,分配單例實(shí)例的內(nèi)存,然后退出。


當(dāng)系統(tǒng)上下文切換回線程A,你會(huì)分配另外一個(gè)單例實(shí)例的內(nèi)存,然后退出。在那個(gè)時(shí)間點(diǎn),你有了兩個(gè)單例的實(shí)例——很明顯這不是你想要的(譯者注:這還能叫單例嗎?)!


要強(qiáng)制這個(gè)(競(jìng)態(tài))條件發(fā)生,替換 PhotoManager.m 中的 sharedManager 為下面的實(shí)現(xiàn):

1+ (instancetype)sharedManager? ? 2{? 3staticPhotoManager *sharedPhotoManager = nil;? 4if(!sharedPhotoManager) {? 5[NSThread sleepForTimeInterval:2];? 6sharedPhotoManager = [[PhotoManager alloc] init];? 7NSLog(@"Singleton has memory address at: %@", sharedPhotoManager);? 8[NSThread sleepForTimeInterval:2];? 9sharedPhotoManager->_photosArray = [NSMutableArray array]; 10? ? } 11return sharedPhotoManager; 12}

上面的代碼中你用 NSThread 的 sleepForTimeInterval: 類方法來(lái)強(qiáng)制發(fā)生一個(gè)上下文切換。


打開(kāi) AppDelegate.m 并添加如下代碼到 application:didFinishLaunchingWithOptions: 的最開(kāi)始處:

1dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0), ^{ 2? ? [PhotoManager sharedManager]; 3}); 45dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0), ^{ 6? ? [PhotoManager sharedManager]; 7});

這里創(chuàng)建了多個(gè)異步并發(fā)調(diào)用來(lái)實(shí)例化單例,然后引發(fā)上面描述的競(jìng)態(tài)條件。


編譯并運(yùn)行項(xiàng)目;查看控制臺(tái)輸出,你會(huì)看到多個(gè)單例被實(shí)例化,如下所示:

注意到這里有好幾行顯示著不同地址的單例實(shí)例。這明顯違背了單例的目的,對(duì)吧?


這個(gè)輸出向你展示了臨界區(qū)被執(zhí)行多次,而它只應(yīng)該執(zhí)行一次?,F(xiàn)在,固然是你自己強(qiáng)制這樣的狀況發(fā)生,但你可以想像一下這個(gè)狀況會(huì)怎樣在無(wú)意間發(fā)生。


注意:基于其它你無(wú)法控制的系統(tǒng)事件,NSLog 的數(shù)量有時(shí)會(huì)顯示多個(gè)。線程問(wèn)題極其難以調(diào)試,因?yàn)樗鼈兺y以重現(xiàn)。

要糾正這個(gè)狀況,實(shí)例化代碼應(yīng)該只執(zhí)行一次,并阻塞其它實(shí)例在 if 條件的臨界區(qū)運(yùn)行。這剛好就是 dispatch_once 能做的事。


在單例初始化方法中用 dispatch_once 取代 if 條件判斷,如下所示:

1+ (instancetype)sharedManager? 2{? 3staticPhotoManager *sharedPhotoManager = nil;? 4static dispatch_once_t onceToken;? 5dispatch_once(&onceToken, ^{? 6[NSThread sleepForTimeInterval:2];? 7sharedPhotoManager = [[PhotoManager alloc] init];? 8NSLog(@"Singleton has memory address at: %@", sharedPhotoManager);? 9[NSThread sleepForTimeInterval:2]; 10sharedPhotoManager->_photosArray = [NSMutableArray array]; 11? ? }); 12return sharedPhotoManager; 13}

編譯并運(yùn)行你的應(yīng)用;查看控制臺(tái)輸出,你會(huì)看到有且僅有一個(gè)單例的實(shí)例——這就是你對(duì)單例的期望!:]


現(xiàn)在你已經(jīng)明白了防止競(jìng)態(tài)條件的重要性,從 AppDelegate.m 中移除 dispatch_async 語(yǔ)句,并用下面的實(shí)現(xiàn)替換 PhotoManager 單例的初始化:

1+ (instancetype)sharedManager? 2{? 3staticPhotoManager *sharedPhotoManager = nil;? 4static dispatch_once_t onceToken;? 5dispatch_once(&onceToken, ^{? 6sharedPhotoManager = [[PhotoManager alloc] init];? 7sharedPhotoManager->_photosArray = [NSMutableArray array];? 8? ? });? 9return sharedPhotoManager; 10}

dispatch_once() 以線程安全的方式執(zhí)行且僅執(zhí)行其代碼塊一次。試圖訪問(wèn)臨界區(qū)(即傳遞給 dispatch_once 的代碼)的不同的線程會(huì)在臨界區(qū)已有一個(gè)線程的情況下被阻塞,直到臨界區(qū)完成為止。

需要記住的是,這只是讓訪問(wèn)共享實(shí)例線程安全。它絕對(duì)沒(méi)有讓類本身線程安全。類中可能還有其它競(jìng)態(tài)條件,例如任何操縱內(nèi)部數(shù)據(jù)的情況。這些需要用其它方式來(lái)保證線程安全,例如同步訪問(wèn)數(shù)據(jù),你將在下面幾個(gè)小節(jié)看到。

處理讀者與寫(xiě)者問(wèn)題

線程安全實(shí)例不是處理單例時(shí)的唯一問(wèn)題。如果單例屬性表示一個(gè)可變對(duì)象,那么你就需要考慮是否那個(gè)對(duì)象自身線程安全。

如果問(wèn)題中的這個(gè)對(duì)象是一個(gè) Foundation 容器類,那么答案是——“很可能不安全”!Apple 維護(hù)一個(gè)有用且有些心寒的列表,眾多的 Foundation 類都不是線程安全的。 NSMutableArray,已用于你的單例,正在那個(gè)列表里休息。

雖然許多線程可以同時(shí)讀取 NSMutableArray 的一個(gè)實(shí)例而不會(huì)產(chǎn)生問(wèn)題,但當(dāng)一個(gè)線程正在讀取時(shí)讓另外一個(gè)線程修改數(shù)組就是不安全的。你的單例在目前的狀況下不能預(yù)防這種情況的發(fā)生。


要分析這個(gè)問(wèn)題,看看 PhotoManager.m 中的 addPhoto:,轉(zhuǎn)載如下:

1- (void)addPhoto:(Photo *)photo 2{ 3if (photo) { 4? ? ? ? [_photosArray addObject:photo]; 5dispatch_async(dispatch_get_main_queue(), ^{ 6? ? ? ? ? ? [self postContentAddedNotification]; 7? ? ? ? }); 8? ? } 9}

這是一個(gè)寫(xiě)方法,它修改一個(gè)私有可變數(shù)組對(duì)象。


現(xiàn)在看看 photos ,轉(zhuǎn)載如下:

1?- (NSArray *)photos?2?{?3?return?[NSArray arrayWithArray:_photosArray];?4?}?


這是所謂的讀方法,它讀取可變數(shù)組。它為調(diào)用者生成一個(gè)不可變的拷貝,防止調(diào)用者不當(dāng)?shù)馗淖償?shù)組,但這不能提供任何保護(hù)來(lái)對(duì)抗當(dāng)一個(gè)線程調(diào)用讀方法 photos 的同時(shí)另一個(gè)線程調(diào)用寫(xiě)方法 addPhoto: 。


這就是軟件開(kāi)發(fā)中經(jīng)典的讀者寫(xiě)者問(wèn)題。GCD 通過(guò)用 dispatch barriers 創(chuàng)建一個(gè)讀者寫(xiě)者鎖 提供了一個(gè)優(yōu)雅的解決方案。


Dispatch barriers 是一組函數(shù),在并發(fā)隊(duì)列上工作時(shí)扮演一個(gè)串行式的瓶頸。使用 GCD 的障礙(barrier)API 確保提交的 Block 在那個(gè)特定時(shí)間上是指定隊(duì)列上唯一被執(zhí)行的條目。這就意味著所有的先于調(diào)度障礙提交到隊(duì)列的條目必能在這個(gè) Block 執(zhí)行前完成。


當(dāng)這個(gè) Block 的時(shí)機(jī)到達(dá),調(diào)度障礙執(zhí)行這個(gè) Block 并確保在那個(gè)時(shí)間里隊(duì)列不會(huì)執(zhí)行任何其它 Block 。一旦完成,隊(duì)列就返回到它默認(rèn)的實(shí)現(xiàn)狀態(tài)。 GCD 提供了同步和異步兩種障礙函數(shù)。


下圖顯示了障礙函數(shù)對(duì)多個(gè)異步隊(duì)列的影響:

注意到正常部分的操作就如同一個(gè)正常的并發(fā)隊(duì)列。但當(dāng)障礙執(zhí)行時(shí),它本質(zhì)上就如同一個(gè)串行隊(duì)列。也就是,障礙是唯一在執(zhí)行的事物。在障礙完成后,隊(duì)列回到一個(gè)正常并發(fā)隊(duì)列的樣子。


下面是你何時(shí)會(huì)——和不會(huì)——使用障礙函數(shù)的情況:

1. 自定義串行隊(duì)列:一個(gè)很壞的選擇;障礙不會(huì)有任何幫助,因?yàn)椴还茉鯓?,一個(gè)串行隊(duì)列一次都只執(zhí)行一個(gè)操作。

2. 全局并發(fā)隊(duì)列:要小心;這可能不是最好的主意,因?yàn)槠渌到y(tǒng)可能在使用隊(duì)列而且你不能壟斷它們只為你自己的目的。

3. 自定義并發(fā)隊(duì)列:這對(duì)于原子或臨界區(qū)代碼來(lái)說(shuō)是極佳的選擇。任何你在設(shè)置或?qū)嵗男枰€程安全的事物都是使用障礙的最佳候選。

由于上面唯一像樣的選擇是自定義并發(fā)隊(duì)列,你將創(chuàng)建一個(gè)你自己的隊(duì)列去處理你的障礙函數(shù)并分開(kāi)讀和寫(xiě)函數(shù)。且這個(gè)并發(fā)隊(duì)列將允許多個(gè)多操作同時(shí)進(jìn)行。


打開(kāi) PhotoManager.m,添加如下私有屬性到類擴(kuò)展中:

1@interface PhotoManager () 2@property (nonatomic,strong,readonly) NSMutableArray *photosArray; 3@property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue;///< Add this 4@end

找到 addPhoto: 并用下面的實(shí)現(xiàn)替換它:

1- (void)addPhoto:(Photo *)photo? 2{? 3if(photo) {// 1? 4dispatch_barrier_async(self.concurrentPhotoQueue, ^{// 2? 5[_photosArray addObject:photo];// 3? 6dispatch_async(dispatch_get_main_queue(), ^{// 4? 7? ? ? ? ? ? ? ? [self postContentAddedNotification];? 8? ? ? ? ? ? });? 9? ? ? ? }); 10? ? } 11}

你新寫(xiě)的函數(shù)是這樣工作的:

1. 在執(zhí)行下面所有的工作前檢查是否有合法的相片。

2. 添加寫(xiě)操作到你的自定義隊(duì)列。當(dāng)臨界區(qū)在稍后執(zhí)行時(shí),這將是你隊(duì)列中唯一執(zhí)行的條目。

3. 這是添加對(duì)象到數(shù)組的實(shí)際代碼。由于它是一個(gè)障礙 Block ,這個(gè) Block 永遠(yuǎn)不會(huì)同時(shí)和其它 Block 一起在 concurrentPhotoQueue 中執(zhí)行。

4. 最后你發(fā)送一個(gè)通知說(shuō)明完成了添加圖片。這個(gè)通知將在主線程被發(fā)送因?yàn)樗鼘?huì)做一些 UI 工作,所以在此為了通知,你異步地調(diào)度另一個(gè)任務(wù)到主線程。

這就處理了寫(xiě)操作,但你還需要實(shí)現(xiàn) photos 讀方法并實(shí)例化 concurrentPhotoQueue 。


在寫(xiě)者打擾的情況下,要確保線程安全,你需要在 concurrentPhotoQueue 隊(duì)列上執(zhí)行讀操作。既然你需要從函數(shù)返回,你就不能異步調(diào)度到隊(duì)列,因?yàn)槟菢釉谧x者函數(shù)返回之前不一定運(yùn)行。


在這種情況下,dispatch_sync 就是一個(gè)絕好的候選。


dispatch_sync() 同步地提交工作并在返回前等待它完成。使用 dispatch_sync 跟蹤你的調(diào)度障礙工作,或者當(dāng)你需要等待操作完成后才能使用 Block 處理過(guò)的數(shù)據(jù)。如果你使用第二種情況做事,你將不時(shí)看到一個(gè) __block 變量寫(xiě)在 dispatch_sync 范圍之外,以便返回時(shí)在 dispatch_sync 使用處理過(guò)的對(duì)象。


但你需要很小心。想像如果你調(diào)用 dispatch_sync 并放在你已運(yùn)行著的當(dāng)前隊(duì)列。這會(huì)導(dǎo)致死鎖,因?yàn)檎{(diào)用會(huì)一直等待直到 Block 完成,但 Block 不能完成(它甚至不會(huì)開(kāi)始?。?,直到當(dāng)前已經(jīng)存在的任務(wù)完成,而當(dāng)前任務(wù)無(wú)法完成!這將迫使你自覺(jué)于你正從哪個(gè)隊(duì)列調(diào)用——以及你正在傳遞進(jìn)入哪個(gè)隊(duì)列。


下面是一個(gè)快速總覽,關(guān)于在何時(shí)以及何處使用 dispatch_sync :

1. 自定義串行隊(duì)列:在這個(gè)狀況下要非常小心!如果你正運(yùn)行在一個(gè)隊(duì)列并調(diào)用 dispatch_sync 放在同一個(gè)隊(duì)列,那你就百分百地創(chuàng)建了一個(gè)死鎖。

2. 主隊(duì)列(串行):同上面的理由一樣,必須非常小心!這個(gè)狀況同樣有潛在的導(dǎo)致死鎖的情況。

3. 并發(fā)隊(duì)列:這才是做同步工作的好選擇,不論是通過(guò)調(diào)度障礙,或者需要等待一個(gè)任務(wù)完成才能執(zhí)行進(jìn)一步處理的情況。


繼續(xù)在 PhotoManager.m 上工作,用下面的實(shí)現(xiàn)替換 photos :

1- (NSArray *)photos 2{ 3__block NSArray *array;// 1 4dispatch_sync(self.concurrentPhotoQueue, ^{// 2 5array = [NSArray arrayWithArray:_photosArray];// 3 6? ? }); 7return array; 8}

這就是你的讀函數(shù)。按順序看看編過(guò)號(hào)的注釋,有這些:

1. __block 關(guān)鍵字允許對(duì)象在 Block 內(nèi)可變。沒(méi)有它,array 在 Block 內(nèi)部就只是只讀的,你的代碼甚至不能通過(guò)編譯。

2. 在 concurrentPhotoQueue 上同步調(diào)度來(lái)執(zhí)行讀操作。

3. 將相片數(shù)組存儲(chǔ)在 array 內(nèi)并返回它。


最后,你需要實(shí)例化你的 concurrentPhotoQueue 屬性。修改 sharedManager 以便像下面這樣初始化隊(duì)列:

1+ (instancetype)sharedManager? 2{? 3staticPhotoManager *sharedPhotoManager = nil;? 4static dispatch_once_t onceToken;? 5dispatch_once(&onceToken, ^{? 6sharedPhotoManager = [[PhotoManager alloc] init];? 7sharedPhotoManager->_photosArray = [NSMutableArray array];? 8 9// ADD THIS: 10sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue", 11? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? DISPATCH_QUEUE_CONCURRENT);? 12? ? }); 1314return sharedPhotoManager; 15}

這里使用 dispatch_queue_create 初始化 concurrentPhotoQueue 為一個(gè)并發(fā)隊(duì)列。第一個(gè)參數(shù)是反向DNS樣式命名慣例;確保它是描述性的,將有助于調(diào)試。第二個(gè)參數(shù)指定你的隊(duì)列是串行還是并發(fā)。


注意:當(dāng)你在網(wǎng)上搜索例子時(shí),你會(huì)經(jīng)??慈藗儌鬟f 0 或者 NULL 給 dispatch_queue_create 的第二個(gè)參數(shù)。這是一個(gè)創(chuàng)建串行隊(duì)列的過(guò)時(shí)方式;明確你的參數(shù)總是更好。

恭喜——你的 PhotoManager 單例現(xiàn)在是線程安全的了。不論你在何處或怎樣讀或?qū)懩愕恼掌?,你都有這樣的自信,即它將以安全的方式完成,不會(huì)出現(xiàn)任何驚嚇。


A Visual Review of Queueing 隊(duì)列的虛擬回顧

依然沒(méi)有 100% 地掌握 GCD 的要領(lǐng)?確保你可以使用 GCD 函數(shù)輕松地創(chuàng)建簡(jiǎn)單的例子,使用斷點(diǎn)和 NSLog 語(yǔ)句保證自己明白當(dāng)下發(fā)生的情況。


我在下面提供了兩個(gè) GIF動(dòng)畫(huà)來(lái)幫助你鞏固對(duì) dispatch_async 和 dispatch_sync 的理解。包含在每個(gè) GIF 中的代碼可以提供視覺(jué)輔助;仔細(xì)注意 GIF 左邊顯示代碼斷點(diǎn)的每一步,以及右邊相關(guān)隊(duì)列的狀態(tài)。


dispatch_sync 回顧

1- (void)viewDidLoad? 2{? 3? [super viewDidLoad];? 4 5dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0), ^{? 6 7NSLog(@"First Log");? 8 9? }); 1011NSLog(@"Second Log"); 12}

下面是圖中幾個(gè)步驟的說(shuō)明:

1. 主隊(duì)列一路按順序執(zhí)行任務(wù)——接著是一個(gè)實(shí)例化 UIViewController 的任務(wù),其中包含了 viewDidLoad 。

2. viewDidLoad 在主線程執(zhí)行。

3. 主線程目前在 viewDidLoad 內(nèi),正要到達(dá) dispatch_sync 。

4. dispatch_sync Block 被添加到一個(gè)全局隊(duì)列中,將在稍后執(zhí)行。進(jìn)程將在主線程掛起直到該 Block 完成。同時(shí),全局隊(duì)列并發(fā)處理任務(wù);要記得 Block 在全局隊(duì)列中將按照 FIFO 順序出列,但可以并發(fā)執(zhí)行。

5. 全局隊(duì)列處理 dispatch_sync Block 加入之前已經(jīng)出現(xiàn)在隊(duì)列中的任務(wù)。

6. 終于,輪到 dispatch_sync Block 。

7. 這個(gè) Block 完成,因此主線程上的任務(wù)可以恢復(fù)。

8. viewDidLoad 方法完成,主隊(duì)列繼續(xù)處理其他任務(wù)。


dispatch_sync 添加任務(wù)到一個(gè)隊(duì)列并等待直到任務(wù)完成。dispatch_async 做類似的事情,但不同之處是它不會(huì)等待任務(wù)的完成,而是立即繼續(xù)“調(diào)用線程”的其它任務(wù)。


dispatch_async 回顧

1- (void)viewDidLoad? 2{? 3? [super viewDidLoad];? 4 5dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0), ^{? 6 7NSLog(@"First Log");? 8 9? }); 1011NSLog(@"Second Log"); 12}

1.主隊(duì)列一路按順序執(zhí)行任務(wù)——接著是一個(gè)實(shí)例化 UIViewController 的任務(wù),其中包含了 viewDidLoad 。

2. viewDidLoad 在主線程執(zhí)行。

3.主線程目前在 viewDidLoad 內(nèi),正要到達(dá) dispatch_async 。

4.dispatch_async Block 被添加到一個(gè)全局隊(duì)列中,將在稍后執(zhí)行。

5.viewDidLoad 在添加 dispatch_async 到全局隊(duì)列后繼續(xù)進(jìn)行,主線程把注意力轉(zhuǎn)向剩下的任務(wù)。同時(shí),全局隊(duì)列并發(fā)地處理它未完成地任務(wù)。記住 Block 在全局隊(duì)列中將按照 FIFO 順序出列,但可以并發(fā)執(zhí)行。

6.添加到 dispatch_async 的代碼塊開(kāi)始執(zhí)行。

7.dispatch_async Block 完成,兩個(gè) NSLog 語(yǔ)句將它們的輸出放在控制臺(tái)上。


在這個(gè)特定的實(shí)例中,第二個(gè) NSLog 語(yǔ)句執(zhí)行,跟著是第一個(gè) NSLog 語(yǔ)句。并不總是這樣——著取決于給定時(shí)刻硬件正在做的事情,而且你無(wú)法控制或知曉哪個(gè)語(yǔ)句會(huì)先執(zhí)行?!暗谝粋€(gè)” NSLog 在某些調(diào)用情況下會(huì)第一個(gè)執(zhí)行。


你可以下載GooglyPuff 項(xiàng)目,它包含了目前所有本教程中編寫(xiě)的實(shí)現(xiàn)。在本教程的第二部分,你將繼續(xù)改進(jìn)這個(gè)項(xiàng)目。


糾正過(guò)早彈出的提示

你可能已經(jīng)注意到當(dāng)你嘗試用 Le Internet 選項(xiàng)來(lái)添加圖片時(shí),一個(gè) UIAlertView 會(huì)在圖片下載完成之前就彈出,如下如所示:

問(wèn)題的癥結(jié)在 PhotoManagers 的 downloadPhotoWithCompletionBlock: 里,它目前的實(shí)現(xiàn)如下:

1- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock? 2{? 3__block NSError *error;? 4 5for(NSInteger i =0; i <3; i++) {? 6NSURL *url;? 7switch (i) {? 8case0:? 9url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; 10break; 11case1: 12url = [NSURL URLWithString:kSuccessKidURLString]; 13break; 14case2: 15url = [NSURL URLWithString:kLotsOfFacesURLString]; 16break; 17default: 18break; 19? ? ? ? } 2021Photo *photo = [[Photo alloc] initwithURL:url 22withCompletionBlock:^(UIImage *image, NSError *_error) { 23if (_error) { 24error = _error; 25? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? } 26? ? ? ? ? ? ? ? ? ? ? ? ? ? ? }]; 2728? ? ? ? [[PhotoManager sharedManager] addPhoto:photo]; 29? ? } 3031if (completionBlock) { 32? ? ? ? completionBlock(error); 33? ? } 34}

在方法的最后你調(diào)用了 completionBlock ——因?yàn)榇藭r(shí)你假設(shè)所有的照片都已下載完成。但很不幸,此時(shí)并不能保證所有的下載都已完成。

Photo 類的實(shí)例方法用某個(gè) URL 開(kāi)始下載某個(gè)文件并立即返回,但此時(shí)下載并未完成。換句話說(shuō),當(dāng) downloadPhotoWithCompletionBlock: 在其末尾調(diào)用 completionBlock 時(shí),它就假設(shè)了它自己所使用的方法全都是同步的,而且每個(gè)方法都完成了它們的工作。

然而,-[Photo initWithURL:withCompletionBlock:] 是異步執(zhí)行的,會(huì)立即返回——所以這種方式行不通。

因此,只有在所有的圖像下載任務(wù)都調(diào)用了它們自己的 Completion Block 之后,downloadPhotoWithCompletionBlock: 才能調(diào)用它自己的 completionBlock 。問(wèn)題是:你該如何監(jiān)控并發(fā)的異步事件?你不知道它們何時(shí)完成,而且它們完成的順序完全是不確定的。

或許你可以寫(xiě)一些比較 Hacky 的代碼,用多個(gè)布爾值來(lái)記錄每個(gè)下載的完成情況,但這樣做就缺失了擴(kuò)展性,而且說(shuō)實(shí)話,代碼會(huì)很難看。

幸運(yùn)的是, 解決這種對(duì)多個(gè)異步任務(wù)的完成進(jìn)行監(jiān)控的問(wèn)題,恰好就是設(shè)計(jì) dispatch_group 的目的。

Dispatch Groups(調(diào)度組)

Dispatch Group 會(huì)在整個(gè)組的任務(wù)都完成時(shí)通知你。這些任務(wù)可以是同步的,也可以是異步的,即便在不同的隊(duì)列也行。而且在整個(gè)組的任務(wù)都完成時(shí),Dispatch Group 可以用同步的或者異步的方式通知你。因?yàn)橐O(jiān)控的任務(wù)在不同隊(duì)列,那就用一個(gè) dispatch_group_t 的實(shí)例來(lái)記下這些不同的任務(wù)。

當(dāng)組中所有的事件都完成時(shí),GCD 的 API 提供了兩種通知方式。

第一種是 dispatch_group_wait ,它會(huì)阻塞當(dāng)前線程,直到組里面所有的任務(wù)都完成或者等到某個(gè)超時(shí)發(fā)生。這恰好是你目前所需要的。

打開(kāi) PhotoManager.m,用下列實(shí)現(xiàn)替換 downloadPhotosWithCompletionBlock:

1- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock? 2{? 3dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0), ^{// 1? 4 5__block NSError *error;? 6dispatch_group_t downloadGroup = dispatch_group_create();// 2? 7 8for(NSInteger i =0; i <3; i++) {? 9NSURL *url; 10switch (i) { 11case0: 12url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; 13break; 14case1: 15url = [NSURL URLWithString:kSuccessKidURLString]; 16break; 17case2: 18url = [NSURL URLWithString:kLotsOfFacesURLString]; 19break; 20default: 21break; 22? ? ? ? ? ? } 2324dispatch_group_enter(downloadGroup);// 3 25Photo *photo = [[Photo alloc] initwithURL:url 26withCompletionBlock:^(UIImage *image, NSError *_error) { 27if (_error) { 28error = _error; 29? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? } 30dispatch_group_leave(downloadGroup);// 4 31? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? }]; 3233? ? ? ? ? ? [[PhotoManager sharedManager] addPhoto:photo]; 34? ? ? ? } 35dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER);// 5 36dispatch_async(dispatch_get_main_queue(), ^{// 6 37if(completionBlock) {// 7 38? ? ? ? ? ? ? ? completionBlock(error); 39? ? ? ? ? ? } 40? ? ? ? }); 41? ? }); 42}

按照注釋的順序,你會(huì)看到:

1. 因?yàn)槟阍谑褂玫氖峭降?dispatch_group_wait ,它會(huì)阻塞當(dāng)前線程,所以你要用 dispatch_async 將整個(gè)方法放入后臺(tái)隊(duì)列以避免阻塞主線程。

2. 創(chuàng)建一個(gè)新的 Dispatch Group,它的作用就像一個(gè)用于未完成任務(wù)的計(jì)數(shù)器。

3. dispatch_group_enter 手動(dòng)通知 Dispatch Group 任務(wù)已經(jīng)開(kāi)始。你必須保證 dispatch_group_enter 和 dispatch_group_leave 成對(duì)出現(xiàn),否則你可能會(huì)遇到詭異的崩潰問(wèn)題。

4. 手動(dòng)通知 Group 它的工作已經(jīng)完成。再次說(shuō)明,你必須要確保進(jìn)入 Group 的次數(shù)和離開(kāi) Group 的次數(shù)相等。

5. dispatch_group_wait 會(huì)一直等待,直到任務(wù)全部完成或者超時(shí)。如果在所有任務(wù)完成前超時(shí)了,該函數(shù)會(huì)返回一個(gè)非零值。你可以對(duì)此返回值做條件判斷以確定是否超出等待周期;然而,你在這里用 DISPATCH_TIME_FOREVER 讓它永遠(yuǎn)等待。它的意思,勿庸置疑就是,永-遠(yuǎn)-等-待!這樣很好,因?yàn)閳D片的創(chuàng)建工作總是會(huì)完成的。

6. 此時(shí)此刻,你已經(jīng)確保了,要么所有的圖片任務(wù)都已完成,要么發(fā)生了超時(shí)。然后,你在主線程上運(yùn)行 completionBlock 回調(diào)。這會(huì)將工作放到主線程上,并在稍后執(zhí)行。

7. 最后,檢查 completionBlock 是否為 nil,如果不是,那就運(yùn)行它。

編譯并運(yùn)行你的應(yīng)用,嘗試下載多個(gè)圖片,觀察你的應(yīng)用是在何時(shí)運(yùn)行 completionBlock 的。

注意:如果你是在真機(jī)上運(yùn)行應(yīng)用,而且網(wǎng)絡(luò)活動(dòng)發(fā)生得太快以致難以觀察 completionBlock 被調(diào)用的時(shí)刻,那么你可以在 Settings 應(yīng)用里的開(kāi)發(fā)者相關(guān)部分里打開(kāi)一些網(wǎng)絡(luò)設(shè)置,以確保代碼按照我們所期望的那樣工作。只需去往 Network Link Conditioner 區(qū),開(kāi)啟它,再選擇一個(gè) Profile,“Very Bad Network” 就不錯(cuò)。

如果你是在模擬器里運(yùn)行應(yīng)用,你可以使用 來(lái)自?GitHub 的 Network Link Conditioner來(lái)改變網(wǎng)絡(luò)速度。它會(huì)成為你工具箱中的一個(gè)好工具,因?yàn)樗鼜?qiáng)制你研究你的應(yīng)用在連接速度并非最佳的情況下會(huì)變成什么樣。

目前為止的解決方案還不錯(cuò),但是總體來(lái)說(shuō),如果可能,最好還是要避免阻塞線程。你的下一個(gè)任務(wù)是重寫(xiě)一些方法,以便當(dāng)所有下載任務(wù)完成時(shí)能異步通知你。

在我們轉(zhuǎn)向另外一種使用 Dispatch Group 的方式之前,先看一個(gè)簡(jiǎn)要的概述,關(guān)于何時(shí)以及怎樣使用有著不同的隊(duì)列類型的 Dispatch Group :

1. 自定義串行隊(duì)列:它很適合當(dāng)一組任務(wù)完成時(shí)發(fā)出通知。

2. 主隊(duì)列(串行):它也很適合這樣的情況。但如果你要同步地等待所有工作地完成,那你就不應(yīng)該使用它,因?yàn)槟悴荒茏枞骶€程。然而,異步模型是一個(gè)很有吸引力的能用于在幾個(gè)較長(zhǎng)任務(wù)(例如網(wǎng)絡(luò)調(diào)用)完成后更新 UI 的方式。

3. 并發(fā)隊(duì)列:它也很適合 Dispatch Group 和完成時(shí)通知。

Dispatch Group,第二種方式

上面的一切都很好,但在另一個(gè)隊(duì)列上異步調(diào)度然后使用 dispatch_group_wait 來(lái)阻塞實(shí)在顯得有些笨拙。是的,還有另一種方式……

在 PhotoManager.m 中找到 downloadPhotosWithCompletionBlock: 方法,用下面的實(shí)現(xiàn)替換它:

1- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock? 2{? 3// 1? 4__block NSError *error;? 5dispatch_group_t downloadGroup = dispatch_group_create();? 6 7for(NSInteger i =0; i <3; i++) {? 8NSURL *url;? 9switch (i) { 10case0: 11url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; 12break; 13case1: 14url = [NSURL URLWithString:kSuccessKidURLString]; 15break; 16case2: 17url = [NSURL URLWithString:kLotsOfFacesURLString]; 18break; 19default: 20break; 21? ? ? ? } 2223dispatch_group_enter(downloadGroup);// 2 24Photo *photo = [[Photo alloc] initwithURL:url 25withCompletionBlock:^(UIImage *image, NSError *_error) { 26if (_error) { 27error = _error; 28? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? } 29dispatch_group_leave(downloadGroup);// 3 30? ? ? ? ? ? ? ? ? ? ? ? ? ? ? }]; 3132? ? ? ? [[PhotoManager sharedManager] addPhoto:photo]; 33? ? } 3435dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{// 4 36if (completionBlock) { 37? ? ? ? ? ? completionBlock(error); 38? ? ? ? } 39? ? }); 40}

下面解釋新的異步方法如何工作:

1. 在新的實(shí)現(xiàn)里,因?yàn)槟銢](méi)有阻塞主線程,所以你并不需要將方法包裹在 async 調(diào)用中。

2. 同樣的 enter 方法,沒(méi)做任何修改。

3. 同樣的 leave 方法,也沒(méi)做任何修改。

4. dispatch_group_notify 以異步的方式工作。當(dāng) Dispatch Group 中沒(méi)有任何任務(wù)時(shí),它就會(huì)執(zhí)行其代碼,那么 completionBlock 便會(huì)運(yùn)行。你還指定了運(yùn)行 completionBlock 的隊(duì)列,此處,主隊(duì)列就是你所需要的。

對(duì)于這個(gè)特定的工作,上面的處理明顯更清晰,而且也不會(huì)阻塞任何線程。

太多并發(fā)帶來(lái)的風(fēng)險(xiǎn)

既然你的工具箱里有了這些新工具,你大概做任何事情都想使用它們,對(duì)吧?

看看 PhotoManager 中的 downloadPhotosWithCompletionBlock 方法。你可能已經(jīng)注意到這里的 for 循環(huán),它迭代三次,下載三個(gè)不同的圖片。你的任務(wù)是嘗試讓 for 循環(huán)并發(fā)運(yùn)行,以提高其速度。

dispatch_apply 剛好可用于這個(gè)任務(wù)。

dispatch_apply 表現(xiàn)得就像一個(gè) for 循環(huán),但它能并發(fā)地執(zhí)行不同的迭代。這個(gè)函數(shù)是同步的,所以和普通的 for 循環(huán)一樣,它只會(huì)在所有工作都完成后才會(huì)返回。

當(dāng)在 Block 內(nèi)計(jì)算任何給定數(shù)量的工作的最佳迭代數(shù)量時(shí),必須要小心,因?yàn)檫^(guò)多的迭代和每個(gè)迭代只有少量的工作會(huì)導(dǎo)致大量開(kāi)銷(xiāo)以致它能抵消任何因并發(fā)帶來(lái)的收益。而被稱為跨越式(striding)的技術(shù)可以在此幫到你,即通過(guò)在每個(gè)迭代里多做幾個(gè)不同的工作。

譯者注:大概就能減少并發(fā)數(shù)量吧,作者是提醒大家注意并發(fā)的開(kāi)銷(xiāo),記在心里!

那何時(shí)才適合用 dispatch_apply 呢?

1. 自定義串行隊(duì)列:串行隊(duì)列會(huì)完全抵消 dispatch_apply 的功能;你還不如直接使用普通的 for 循環(huán)。

2. 主隊(duì)列(串行):與上面一樣,在串行隊(duì)列上不適合使用 dispatch_apply 。還是用普通的 for 循環(huán)吧。

3. 并發(fā)隊(duì)列:對(duì)于并發(fā)循環(huán)來(lái)說(shuō)是很好選擇,特別是當(dāng)你需要追蹤任務(wù)的進(jìn)度時(shí)。

回到 downloadPhotosWithCompletionBlock: 并用下列實(shí)現(xiàn)替換它:

1- (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock? 2{? 3__block NSError *error;? 4dispatch_group_t downloadGroup = dispatch_group_create();? 5 6dispatch_apply(3, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0), ^(size_t i) {? 7 8NSURL *url;? 9switch (i) { 10case0: 11url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; 12break; 13case1: 14url = [NSURL URLWithString:kSuccessKidURLString]; 15break; 16case2: 17url = [NSURL URLWithString:kLotsOfFacesURLString]; 18break; 19default: 20break; 21? ? ? ? } 2223? ? ? ? dispatch_group_enter(downloadGroup); 24Photo *photo = [[Photo alloc] initwithURL:url 25withCompletionBlock:^(UIImage *image, NSError *_error) { 26if (_error) { 27error = _error; 28? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? } 29? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? dispatch_group_leave(downloadGroup); 30? ? ? ? ? ? ? ? ? ? ? ? ? ? ? }]; 3132? ? ? ? [[PhotoManager sharedManager] addPhoto:photo]; 33? ? }); 3435dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ 36if (completionBlock) { 37? ? ? ? ? ? completionBlock(error); 38? ? ? ? } 39? ? }); 40}

你的循環(huán)現(xiàn)在是并行運(yùn)行的了;在上面的代碼中,在調(diào)用 dispatch_apply 時(shí),你用第一次參數(shù)指明了迭代的次數(shù),用第二個(gè)參數(shù)指定了任務(wù)運(yùn)行的隊(duì)列,而第三個(gè)參數(shù)是一個(gè) Block。

要知道雖然你有代碼保證添加相片時(shí)線程安全,但圖片的順序卻可能不同,這取決于線程完成的順序。

編譯并運(yùn)行,然后從 “Le Internet” 添加一些照片。注意到區(qū)別了嗎?

在真機(jī)上運(yùn)行新代碼會(huì)稍微更快的得到結(jié)果。但我們所做的這些提速工作真的值得嗎?

實(shí)際上,在這個(gè)例子里并不值得。下面是原因:

1. 你創(chuàng)建并行運(yùn)行線程而付出的開(kāi)銷(xiāo),很可能比直接使用 for 循環(huán)要多。若你要以合適的步長(zhǎng)迭代非常大的集合,那才應(yīng)該考慮使用 dispatch_apply。

2. 你用于創(chuàng)建應(yīng)用的時(shí)間是有限的——除非實(shí)在太糟糕否則不要浪費(fèi)時(shí)間去提前優(yōu)化代碼。如果你要優(yōu)化什么,那去優(yōu)化那些明顯值得你付出時(shí)間的部分。你可以通過(guò)在 Instruments 里分析你的應(yīng)用,找出最長(zhǎng)運(yùn)行時(shí)間的方法??纯?a target="_blank">如何在 Xcode 中使用 Instruments可以學(xué)到更多相關(guān)知識(shí)。

3. 通常情況下,優(yōu)化代碼會(huì)讓你的代碼更加復(fù)雜,不利于你自己和其他開(kāi)發(fā)者閱讀。請(qǐng)確保添加的復(fù)雜性能換來(lái)足夠多的好處。

記住,不要在優(yōu)化上太瘋狂。你只會(huì)讓你自己和后來(lái)者更難以讀懂你的代碼。

原文鏈接:

http://www.cocoachina.com/industry/20140520/8485.html

http://www.cocoachina.com/applenews/devnews/2014/0428/8248.html

http://www.cocoachina.com/applenews/devnews/2014/0515/8433.html

http://blog.csdn.net/q199109106q/article/details/8565923

相關(guān)閱讀:

條碼的開(kāi)發(fā)使用介紹文檔

identity server4

IUrlHelper ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. Parameter name: index

netcore 版本 切換 sdk

深層目錄文件復(fù)制,C# 遞歸,錄音錄像圖片文件過(guò)多,用于測(cè)試程序

C# int uint long ulong byte sbyte float double decimal 范圍,及類型!

sqlserver 數(shù)據(jù)類型 C# clr 數(shù)據(jù)類型 映射

Unable to resolve service for type 'Microsoft.Extensions.Logging.ILogger' while attempting to activate 'xxxxx.Controllers.xxxxController'.

identity 基礎(chǔ)表沒(méi)有創(chuàng)建 aspnetuserclaims aspnetuserlogins

DatabaseGeneratedOption

原文地址:https://www.cnblogs.com/novia/p/4497907.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容