1、線程概述html
有些程序是一條直線,起點到終點——如簡單的hello world,運行打印完,它的生命週期便結束了,像曇花一現。程序員
有些程序是一個圓,直到循環將它切斷——像操做系統,一直運行,直到你關機。數據庫
一個運行着的程序就是一個進程或者叫作一個任務,一個進程至少包含一個線程,線程就是程序的執行流。編程
Mac和IOS中的程序啓動,建立好一個進程的同時,一個線程便開始運做,這個線程叫作主線程。主線程在程序中的位置和其餘線程不一樣,它是其餘線程最終的父線程,且全部的界面的顯示操做即AppKit或UIKit的操做必須在主線程進行。數組
系統中每個進程都有本身獨立的虛擬內存空間,而同一個進程中的多個線程則公用進程的內存空間。xcode
每建立一個新的進程,都須要一些內存(如每一個線程有本身的stack空間)和消耗必定的CPU時間。安全
當多個進程對同一個資源出現爭奪的時候須要注意線程安全問題。網絡
建立線程:建立一個新的線程就是給進程增長一個執行流,因此新建一個線程須要提供一個函數或者方法做爲線程的進口。多線程
概要提示:併發
iPhone中的線程應用並非無節制的,官方給出的資料顯示,iPhone OS下的主線程的堆棧大小是1M,第二個線程開始就是512KB,而且該值不能經過編譯器開關或線程API函數來更改,只有主線程有直接修改UI的能力。
2、簡介
iOS有三種多線程編程的技術,分別是:
(一)NSThread
(二)Cocoa NSOperation
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;
在當前線程中作事情:
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;
取消發送給當前線程的某個消息:
1 cancelPreviousPerformRequestsWithTarget: 2 cancelPreviousPerformRequestsWithTarget:selector:object:
1 - (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 2 + (void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument
第一個是實例方法,第二個是類方法。使用方式以下:
1 1、[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:nil]; 2 3 2、NSThread* myThread = [[NSThread alloc] initWithTarget:self 4 selector:@selector(doSomething:) 5 object:nil]; 6 [myThread start];
1 // 2 // ViewController.m 3 // NSThreadDemo 4 // 5 // Created by rongfzh on 12-9-23. 6 // Copyright (c) 2012年 rongfzh. All rights reserved. 7 // 8 9 #import "ViewController.h" 10 #define kURL @"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg" 11 @interface ViewController () 12 13 @end 14 15 @implementation ViewController 16 17 -(void)downloadImage:(NSString *) url{ 18 NSData *data = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:url]]; 19 UIImage *image = [[UIImage alloc]initWithData:data]; 20 if(image == nil){ 21 22 }else{ 23 [self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES]; 24 } 25 } 26 27 -(void)updateUI:(UIImage*) image{ 28 self.imageView.image = image; 29 } 30 31 - (void)viewDidLoad 32 { 33 [super viewDidLoad]; 34 35 // [NSThread detachNewThreadSelector:@selector(downloadImage:) toTarget:self withObject:kURL]; 36 NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(downloadImage:) object:kURL]; 37 [thread start]; 38 } 39 40 - (void)didReceiveMemoryWarning 41 { 42 [super didReceiveMemoryWarning]; 43 // Dispose of any resources that can be recreated. 44 } 45 46 @end 47
1 #import <UIKit/UIKit.h> 2 3 @class ViewController; 4 5 @interface AppDelegate : UIResponder <UIApplicationDelegate> 6 { 7 int tickets; 8 int count; 9 NSThread* ticketsThreadone; 10 NSThread* ticketsThreadtwo; 11 NSCondition* ticketsCondition; 12 NSLock *theLock; 13 } 14 @property (strong, nonatomic) UIWindow *window; 15 16 @property (strong, nonatomic) ViewController *viewController; 17 18 @end
1 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 2 { 3 4 tickets = 100; 5 count = 0; 6 theLock = [[NSLock alloc] init]; 7 // 鎖對象 8 ticketsCondition = [[NSCondition alloc] init]; 9 ticketsThreadone = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; 10 [ticketsThreadone setName:@"Thread-1"]; 11 [ticketsThreadone start]; 12 13 ticketsThreadtwo = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; 14 [ticketsThreadtwo setName:@"Thread-2"]; 15 [ticketsThreadtwo start]; 16 17 self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 18 // Override point for customization after application launch. 19 self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil]; 20 self.window.rootViewController = self.viewController; 21 [self.window makeKeyAndVisible]; 22 return YES; 23 } 24 25 - (void)run{ 26 while (TRUE) { 27 // 上鎖 28 // [ticketsCondition lock]; 29 [theLock lock]; 30 if(tickets >= 0){ 31 [NSThread sleepForTimeInterval:0.09]; 32 count = 100 - tickets; 33 NSLog(@"當前票數是:%d,售出:%d,線程名:%@",tickets,count,[[NSThread currentThread] name]); 34 tickets--; 35 }else{ 36 break; 37 } 38 [theLock unlock]; 39 // [ticketsCondition unlock]; 40 } 41 }
若是沒有線程同步的lock,賣票數多是-1.加上lock加上lock以後線程同步保證了數據的正確性。
上面例子我使用了兩種鎖,一種NSCondition ,一種是:NSLock。 NSCondition我已經註釋了。
1 #import "AppDelegate.h" 2 3 #import "ViewController.h" 4 5 @implementation AppDelegate 6 7 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 8 { 9 tickets = 100; 10 count = 0; 11 theLock = [[NSLock alloc] init]; 12 // 鎖對象 13 ticketsCondition = [[NSCondition alloc] init]; 14 ticketsThreadone = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; 15 [ticketsThreadone setName:@"Thread-1"]; 16 [ticketsThreadone start]; 17 18 ticketsThreadtwo = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; 19 [ticketsThreadtwo setName:@"Thread-2"]; 20 [ticketsThreadtwo start]; 21 22 NSThread *ticketsThreadthree = [[NSThread alloc] initWithTarget:self selector:@selector(run3) object:nil]; 23 [ticketsThreadthree setName:@"Thread-3"]; 24 [ticketsThreadthree start]; 25 self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 26 // Override point for customization after application launch. 27 self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil]; 28 self.window.rootViewController = self.viewController; 29 [self.window makeKeyAndVisible]; 30 return YES; 31 } 32 33 -(void)run3{ 34 while (YES) { 35 [ticketsCondition lock]; 36 [NSThread sleepForTimeInterval:3]; 37 [ticketsCondition signal]; 38 [ticketsCondition unlock]; 39 } 40 } 41 42 - (void)run{ 43 while (TRUE) { 44 // 上鎖 45 [ticketsCondition lock]; 46 [ticketsCondition wait]; 47 [theLock lock]; 48 if(tickets >= 0){ 49 [NSThread sleepForTimeInterval:0.09]; 50 count = 100 - tickets; 51 NSLog(@"當前票數是:%d,售出:%d,線程名:%@",tickets,count,[[NSThread currentThread] name]); 52 tickets--; 53 }else{ 54 break; 55 } 56 [theLock unlock]; 57 [ticketsCondition unlock]; 58 } 59 }
1 - (void)doSomeThing:(id)anObj 2 { 3 @synchronized(anObj) 4 { 5 // Everything between the braces is protected by the @synchronized directive. 6 } 7 }
還有其餘的一些鎖對象,好比:循環鎖NSRecursiveLock,條件鎖NSConditionLock,分佈式鎖NSDistributedLock等等,能夠本身看官方文檔學習。
NSThread下載圖片的例子代碼:http://download.csdn.net/detail/totogo2010/4591149
(二)Cocoa Operation的使用
NSOperation實例封裝了須要執行的操做和執行操做所需的數據,而且可以以併發或非併發的方式執行這個操做。NSOperation自己是抽象基類,所以必須使用它的子類,使用NSOperation子類的方式有2種:
1> Foundation框架提供了兩個具體子類直接供咱們使用:NSInvocationOperation和NSBlockOperation
2> 自定義子類繼承NSOperation,實現內部相應的方法
執行操做:
NSOperation調用start方法便可開始執行操做,NSOperation對象默認按同步方式執行,也就是在調用start方法的那個線程中直接執行。NSOperation對象的isConcurrent方法會告訴咱們這個操做相對於調用start方法的線程,是同步仍是異步執行。isConcurrent方法默認返回NO,表示操做與調用線程同步執行。
取消操做:
operation開始執行以後, 默認會一直執行操做直到完成,咱們也能夠調用cancel方法中途取消操做。
1 [operation cancel];
監聽操做的執行:
若是咱們想在一個NSOperation執行完畢後作一些事情,就調用NSOperation的setCompletionBlock方法來設置想作的事情。
1 operation.completionBlock = ^() { 2 NSLog(@"執行完畢"); 3 }; 4 5 或者 6 7 [operation setCompletionBlock:^() { 8 NSLog(@"執行完畢"); 9 }];
1)NSInvocationOperation
基於一個對象和selector來建立操做。若是你已經有現有的方法來執行須要的任務,就可使用這個類。
建立並執行操做:
1 // 這個操做是:調用self的run方法 2 NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil]; 3 // 開始執行任務(同步執行) 4 [operation start];
1 #import "ViewController.h" 2 #define kURL @"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg" 3 4 @interface ViewController () 5 6 @end 7 8 @implementation ViewController 9 10 - (void)viewDidLoad 11 { 12 [super viewDidLoad]; 13 NSInvocationOperation *operation = [[NSInvocationOperation alloc]initWithTarget:self 14 selector:@selector(downloadImage:) 15 object:kURL]; 16 17 NSOperationQueue *queue = [[NSOperationQueue alloc]init]; 18 [queue addOperation:operation]; 19 // Do any additional setup after loading the view, typically from a nib. 20 } 21 22 -(void)downloadImage:(NSString *)url{ 23 NSLog(@"url:%@", url); 24 NSURL *nsUrl = [NSURL URLWithString:url]; 25 NSData *data = [[NSData alloc]initWithContentsOfURL:nsUrl]; 26 UIImage * image = [[UIImage alloc]initWithData:data]; 27 [self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES]; 28 } 29 30 -(void)updateUI:(UIImage*) image{ 31 self.imageView.image = image; 32 }
運行能夠看到下載圖片顯示在界面上。
2)NSBlockOperation
可以併發地執行一個或多個block對象,全部相關的block都執行完以後,操做纔算完成。
建立並執行操做:
1 NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^(){ 2 NSLog(@"執行了一個新的操做,線程:%@", [NSThread currentThread]); 3 }]; 4 // 開始執行任務(這裏仍是同步執行) 5 [operation start];
經過addExecutionBlock方法添加block操做:
1 NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^(){ 2 NSLog(@"執行第1次操做,線程:%@", [NSThread currentThread]); 3 }]; 4 5 [operation addExecutionBlock:^() { 6 NSLog(@"又執行了1個新的操做,線程:%@", [NSThread currentThread]); 7 }]; 8 9 [operation addExecutionBlock:^() { 10 NSLog(@"又執行了1個新的操做,線程:%@", [NSThread currentThread]); 11 }]; 12 13 [operation addExecutionBlock:^() { 14 NSLog(@"又執行了1個新的操做,線程:%@", [NSThread currentThread]); 15 }]; 16 17 // 開始執行任務 18 [operation start];
1 2013-02-02 21:38:46.102 thread[4602:c07] 又執行了1個新的操做,線程:<NSThread: 0x7121d50>{name = (null), num = 1} 2 2013-02-02 21:38:46.102 thread[4602:3f03] 又執行了1個新的操做,線程:<NSThread: 0x742e1d0>{name = (null), num = 5} 3 2013-02-02 21:38:46.102 thread[4602:1b03] 執行第1次操做,線程:<NSThread: 0x742de50>{name = (null), num = 3} 4 2013-02-02 21:38:46.102 thread[4602:1303] 又執行了1個新的操做,線程:<NSThread: 0x7157bf0>{name = (null), num = 4}
能夠看出,這4個block是併發執行的,也就是在不一樣線程中執行的,num屬性能夠當作是線程的id。
3)自定義NSOperation
若是NSInvocationOperation和NSBlockOperation對象不能知足需求, 你能夠直接繼承NSOperation, 並添加任何你想要的行爲。繼承所需的工做量主要取決於你要實現非併發仍是併發的NSOperation。定義非併發的NSOperation要簡單許多,只須要重載-(void)main這個方法,在這個方法裏面執行主任務,並正確地響應取消事件; 對於併發NSOperation, 你必須重寫NSOperation的多個基本方法進行實現(這裏暫時先介紹非併發的NSOperation)。
非併發的NSOperation:
好比叫作DownloadOperation,用來下載圖片。
1> 繼承NSOperation,重寫main方法,執行主任務
DownloadOperation.h
1 #import <Foundation/Foundation.h> 2 @protocol DownloadOperationDelegate; 3 4 @interface DownloadOperation : NSOperation 5 // 圖片的url路徑 6 @property (nonatomic, copy) NSString *imageUrl; 7 // 代理 8 @property (nonatomic, retain) id<DownloadOperationDelegate> delegate; 9 10 - (id)initWithUrl:(NSString *)url delegate:(id<DownloadOperationDelegate>)delegate; 11 @end 12 13 // 圖片下載的協議 14 @protocol DownloadOperationDelegate <NSObject> 15 - (void)downloadFinishWithImage:(UIImage *)image; 16 @end
DownloadOperation.m
1 #import "DownloadOperation.h" 2 3 @implementation DownloadOperation 4 @synthesize delegate = _delegate; 5 @synthesize imageUrl = _imageUrl; 6 7 // 初始化 8 - (id)initWithUrl:(NSString *)url delegate:(id<DownloadOperationDelegate>)delegate { 9 if (self = [super init]) { 10 self.imageUrl = url; 11 self.delegate = delegate; 12 } 13 return self; 14 } 15 // 釋放內存 16 - (void)dealloc { 17 [super dealloc]; 18 [_delegate release]; 19 [_imageUrl release]; 20 } 21 22 // 執行主任務 23 - (void)main { 24 // 新建一個自動釋放池,若是是異步執行操做,那麼將沒法訪問到主線程的自動釋放池 25 @autoreleasepool { 26 // .... 27 } 28 } 29 @end
2> 正確響應取消事件
operation開始執行以後,會一直執行任務直到完成,或者顯式地取消操做。取消可能發生在任什麼時候候,甚至在operation執行以前。儘管NSOperation提供了一個方法,讓應用取消一個操做,可是識別出取消事件則是咱們本身的事情。若是operation直接終止, 可能沒法回收全部已分配的內存或資源。所以operation對象須要檢測取消事件,並優雅地退出執行
NSOperation對象須要按期地調用isCancelled方法檢測操做是否已經被取消,若是返回YES(表示已取消),則當即退出執行。無論是自定義NSOperation子類,仍是使用系統提供的兩個具體子類,都須要支持取消。isCancelled方法自己很是輕量,能夠頻繁地調用而不產生大的性能損失。
如下地方可能須要調用isCancelled:
* 在執行任何實際的工做以前
* 在循環的每次迭代過程當中,若是每一個迭代相對較長可能須要調用屢次
* 代碼中相對比較容易停止操做的任何地方
DownloadOperation的main方法實現以下:
1 - (void)main { 2 // 新建一個自動釋放池,若是是異步執行操做,那麼將沒法訪問到主線程的自動釋放池 3 @autoreleasepool { 4 if (self.isCancelled) return; 5 6 // 獲取圖片數據 7 NSURL *url = [NSURL URLWithString:self.imageUrl]; 8 NSData *imageData = [NSData dataWithContentsOfURL:url]; 9 10 if (self.isCancelled) { 11 url = nil; 12 imageData = nil; 13 return; 14 } 15 16 // 初始化圖片 17 UIImage *image = [UIImage imageWithData:imageData]; 18 19 if (self.isCancelled) { 20 image = nil; 21 return; 22 } 23 24 if ([self.delegate respondsToSelector:@selector(downloadFinishWithImage:)]) { 25 // 把圖片數據傳回到主線程 26 [(NSObject *)self.delegate performSelectorOnMainThread:@selector(downloadFinishWithImage:) withObject:image waitUntilDone:NO]; 27 } 28 } 29 }
併發行和並行性的區別能夠用饅頭作比喻。前者至關於一我的同時吃三個饅頭和三我的同時吃一個饅頭。
併發性(Concurrence):指兩個或兩個以上的事件或活動在同一時間間隔內發生。併發的實質是物理CPU(也能夠多個物理CPU) 在若干道程序之間多路複用,併發性是對有限物理資源強制行使多用戶共享以提升效率。
並行性(parallelism)指兩個或兩個以上事件或活動在同一時刻發生。在多道程序環境下,並行性使多個程序同一時刻可在不一樣CPU上同時執行。
區別:一個處理器同時處理多個任務和多個處理器或者是多核的處理器同時處理多個不一樣的任務。
前者是邏輯上的同時發生(simultaneous),然後者是物理上的同時發生。
二者的聯繫:並行的事件或活動必定是併發的,但反之併發的事件或活動未必是並行的。並行性是併發性的特例,而併發性是並行性的擴展。
全部的調度隊列(dispatch queues)自身都是線程安全的,你能從多個線程並行的訪問它們。 GCD 的優勢是顯而易見的,即當你瞭解了調度隊列如何爲你本身代碼的不一樣部分提供線程安全。關於這一點的關鍵是選擇正確類型的調度隊列和正確的調度函數來提交你的工做。
Serial Queues 串行隊列
這些任務的執行時機受到 GCD 的控制;惟一能確保的事情是 GCD 一次只執行一個任務,而且按照咱們添加到隊列的順序來執行。
因爲在串行隊列中不會有兩個任務併發運行,所以不會出現同時訪問臨界區的風險;相對於這些任務來講,這就從競態條件下保護了臨界區。因此若是訪問臨界區的惟一方式是經過提交到調度隊列的任務,那麼你就不須要擔憂臨界區的安全問題了。
Concurrent Queues 併發隊列
注意 Block 1,2 和 3 都立馬開始運行,一個接一個。在 Block 0 開始後,Block 1等待了好一下子纔開始。一樣, Block 3 在 Block 2 以後纔開始,但它先於 Block 2 完成。
在併發隊列中的任務能獲得的保證是它們會按照被添加的順序開始執行,但這就是所有的保證了。任務可能以任意順序完成,你不會知道什麼時候開始運行下一個任務,或者任意時刻有多少 Block 在運行。再說一遍,這徹底取決於 GCD 。
什麼時候開始一個 Block 徹底取決於 GCD 。若是一個 Block 的執行時間與另外一個重疊,也是由 GCD 來決定是否將其運行在另外一個不一樣的核心上,若是那個核心可用,不然就用上下文切換的方式來執行不一樣的 Block 。
接下來咱們來了解GCD的使用:
用GCD實現這個流程的操做比前面介紹的NSThread NSOperation的方法都要簡單。代碼框架結構以下:
1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 2 // 耗時的操做 3 dispatch_async(dispatch_get_main_queue(), ^{ 4 // 更新界面 5 }); 6 });
若是這樣還不清晰的話,那咱們仍是用上兩篇博客中的下載圖片爲例子,代碼以下:
1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 2 NSURL * url = [NSURL URLWithString:@"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"]; 3 NSData * data = [[NSData alloc]initWithContentsOfURL:url]; 4 UIImage *image = [[UIImage alloc]initWithData:data]; 5 if (data != nil) { 6 dispatch_async(dispatch_get_main_queue(), ^{ 7 self.imageView.image = image; 8 }); 9 } 10 });
運行會顯示下載的圖片。
是否是代碼比NSThread 、NSOperation簡潔不少,並且GCD會自動根據任務在多核處理器上分配資源,優化程序。
系統給每個應用程序提供了三個concurrent dispatch queues。這三個併發調度隊列是全局的,它們只有優先級的不一樣。由於是全局的,咱們不須要去建立。咱們只須要經過使用函數dispath_get_global_queue去獲得隊列,以下:
1 dispatch_queue_t globalQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
這裏也用到了系統默認就有一個串行隊列main_queue:
1 dispatch_queue_t mainQ = dispatch_get_main_queue();
1 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 2 dispatch_group_t group = dispatch_group_create(); 3 dispatch_group_async(group, queue, ^{ 4 [NSThread sleepForTimeInterval:1]; 5 NSLog(@"group1"); 6 }); 7 dispatch_group_async(group, queue, ^{ 8 [NSThread sleepForTimeInterval:2]; 9 NSLog(@"group2"); 10 }); 11 dispatch_group_async(group, queue, ^{ 12 [NSThread sleepForTimeInterval:3]; 13 NSLog(@"group3"); 14 }); 15 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ 16 NSLog(@"updateUi"); 17 }); 18 dispatch_release(group);
dispatch_group_async是異步的方法,運行後能夠看到打印結果:
1 2012-09-25 16:04:16.737 gcdTest[43328:11303] group1 2 2012-09-25 16:04:17.738 gcdTest[43328:12a1b] group2 3 2012-09-25 16:04:18.738 gcdTest[43328:13003] group3 4 2012-09-25 16:04:18.739 gcdTest[43328:f803] updateUi
每隔一秒打印一個,當第三個任務執行後,upadteUi被打印。
1 dispatch_queue_t queue = dispatch_queue_create("gcdtest.rongfzh.yc", DISPATCH_QUEUE_CONCURRENT); 2 dispatch_async(queue, ^{ 3 [NSThread sleepForTimeInterval:2]; 4 NSLog(@"dispatch_async1"); 5 }); 6 dispatch_async(queue, ^{ 7 [NSThread sleepForTimeInterval:4]; 8 NSLog(@"dispatch_async2"); 9 }); 10 dispatch_barrier_async(queue, ^{ 11 NSLog(@"dispatch_barrier_async"); 12 [NSThread sleepForTimeInterval:4]; 13 14 }); 15 dispatch_async(queue, ^{ 16 [NSThread sleepForTimeInterval:1]; 17 NSLog(@"dispatch_async3"); 18 });
打印結果:
1 2012-09-25 16:20:33.967 gcdTest[45547:11203] dispatch_async1 2 2012-09-25 16:20:35.967 gcdTest[45547:11303] dispatch_async2 3 2012-09-25 16:20:35.967 gcdTest[45547:11303] dispatch_barrier_async 4 2012-09-25 16:20:40.970 gcdTest[45547:11303] dispatch_async3
請注意執行的時間,能夠看到執行的順序如上所述。
1 - (void)showOrHideNavPrompt 2 { 3 NSUInteger count = [[PhotoManager sharedManager] photos].count; 4 double delayInSeconds = 1.0; 5 dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); // 1 6 dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ // 2 7 if (!count) { 8 [self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"]; 9 } else { 10 [self.navigationItem setPrompt:nil]; 11 } 12 }); 13 }
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 NSAssert(_image, @"Image not set; required to use view controller"); 5 self.photoImageView.image = _image; 6 7 //Resize if neccessary to ensure it's not pixelated 8 if (_image.size.height <= self.photoImageView.bounds.size.height && 9 _image.size.width <= self.photoImageView.bounds.size.width) { 10 [self.photoImageView setContentMode:UIViewContentModeCenter]; 11 } 12 13 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1 14 UIImage *overlayImage = [self faceOverlayImageFromImage:_image]; 15 dispatch_async(dispatch_get_main_queue(), ^{ // 2 16 [self fadeInNewImage:overlayImage]; // 3 17 }); 18 }); 19 }
下面來講明上面的新代碼所作的事:
1 - (void)showOrHideNavPrompt 2 { 3 NSUInteger count = [[PhotoManager sharedManager] photos].count; 4 double delayInSeconds = 1.0; 5 dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); // 1 6 dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ // 2 7 if (!count) { 8 [self.navigationItem setPrompt:@"Add photos with faces to Googlyify them!"]; 9 } else { 10 [self.navigationItem setPrompt:nil]; 11 } 12 }); 13 }
1 + (instancetype)sharedManager 2 { 3 static PhotoManager *sharedPhotoManager = nil; 4 if (!sharedPhotoManager) { 5 sharedPhotoManager = [[PhotoManager alloc] init]; 6 sharedPhotoManager->_photosArray = [NSMutableArray array]; 7 } 8 return sharedPhotoManager; 9 }
1 + (instancetype)sharedManager 2 { 3 static PhotoManager *sharedPhotoManager = nil; 4 if (!sharedPhotoManager) { 5 [NSThread sleepForTimeInterval:2]; 6 sharedPhotoManager = [[PhotoManager alloc] init]; 7 NSLog(@"Singleton has memory address at: %@", sharedPhotoManager); 8 [NSThread sleepForTimeInterval:2]; 9 sharedPhotoManager->_photosArray = [NSMutableArray array]; 10 } 11 return sharedPhotoManager; 12 }
1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 2 [PhotoManager sharedManager]; 3 }); 4 5 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 6 [PhotoManager sharedManager]; 7 });
1 + (instancetype)sharedManager 2 { 3 static PhotoManager *sharedPhotoManager = nil; 4 static dispatch_once_t onceToken; 5 dispatch_once(&onceToken, ^{ 6 [NSThread sleepForTimeInterval:2]; 7 sharedPhotoManager = [[PhotoManager alloc] init]; 8 NSLog(@"Singleton has memory address at: %@", sharedPhotoManager); 9 [NSThread sleepForTimeInterval:2]; 10 sharedPhotoManager->_photosArray = [NSMutableArray array]; 11 }); 12 return sharedPhotoManager; 13 }
1 + (instancetype)sharedManager 2 { 3 static PhotoManager *sharedPhotoManager = nil; 4 static dispatch_once_t onceToken; 5 dispatch_once(&onceToken, ^{ 6 sharedPhotoManager = [[PhotoManager alloc] init]; 7 sharedPhotoManager->_photosArray = [NSMutableArray array]; 8 }); 9 return sharedPhotoManager; 10 }
dispatch_once() 以線程安全的方式執行且僅執行其代碼塊一次。試圖訪問臨界區(即傳遞給 dispatch_once 的代碼)的不一樣的線程會在臨界區已有一個線程的狀況下被阻塞,直到臨界區完成爲止。
須要記住的是,這只是讓訪問共享實例線程安全。它絕對沒有讓類自己線程安全。類中可能還有其它競態條件,例如任何操縱內部數據的狀況。這些須要用其它方式來保證線程安全,例如同步訪問數據,你將在下面幾個小節看到。
1 - (void)addPhoto:(Photo *)photo 2 { 3 if (photo) { 4 [_photosArray addObject:photo]; 5 dispatch_async(dispatch_get_main_queue(), ^{ 6 [self postContentAddedNotification]; 7 }); 8 } 9 }
1 @interface PhotoManager () 2 @property (nonatomic,strong,readonly) NSMutableArray *photosArray; 3 @property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue; ///< Add this 4 @end
找到 addPhoto: 並用下面的實現替換它:
1 - (void)addPhoto:(Photo *)photo 2 { 3 if (photo) { // 1 4 dispatch_barrier_async(self.concurrentPhotoQueue, ^{ // 2 5 [_photosArray addObject:photo]; // 3 6 dispatch_async(dispatch_get_main_queue(), ^{ // 4 7 [self postContentAddedNotification]; 8 }); 9 }); 10 } 11 }
1 - (NSArray *)photos 2 { 3 __block NSArray *array; // 1 4 dispatch_sync(self.concurrentPhotoQueue, ^{ // 2 5 array = [NSArray arrayWithArray:_photosArray]; // 3 6 }); 7 return array; 8 }
1 + (instancetype)sharedManager 2 { 3 static PhotoManager *sharedPhotoManager = nil; 4 static dispatch_once_t onceToken; 5 dispatch_once(&onceToken, ^{ 6 sharedPhotoManager = [[PhotoManager alloc] init]; 7 sharedPhotoManager->_photosArray = [NSMutableArray array]; 8 9 // ADD THIS: 10 sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue", 11 DISPATCH_QUEUE_CONCURRENT); 12 }); 13 14 return sharedPhotoManager; 15 }
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 5 dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 6 7 NSLog(@"First Log"); 8 9 }); 10 11 NSLog(@"Second Log"); 12 }
1 - (void)viewDidLoad 2 { 3 [super viewDidLoad]; 4 5 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 6 7 NSLog(@"First Log"); 8 9 }); 10 11 NSLog(@"Second Log"); 12 }
糾正過早彈出的提示
你可能已經注意到當你嘗試用 Le Internet 選項來添加圖片時,一個 UIAlertView 會在圖片下載完成以前就彈出,以下如所示:
問題的癥結在 PhotoManagers 的 downloadPhotoWithCompletionBlock: 裏,它目前的實現以下:
1 - (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock 2 { 3 __block NSError *error; 4 5 for (NSInteger i = 0; i < 3; i++) { 6 NSURL *url; 7 switch (i) { 8 case 0: 9 url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; 10 break; 11 case 1: 12 url = [NSURL URLWithString:kSuccessKidURLString]; 13 break; 14 case 2: 15 url = [NSURL URLWithString:kLotsOfFacesURLString]; 16 break; 17 default: 18 break; 19 } 20 21 Photo *photo = [[Photo alloc] initwithURL:url 22 withCompletionBlock:^(UIImage *image, NSError *_error) { 23 if (_error) { 24 error = _error; 25 } 26 }]; 27 28 [[PhotoManager sharedManager] addPhoto:photo]; 29 } 30 31 if (completionBlock) { 32 completionBlock(error); 33 } 34 }
在方法的最後你調用了 completionBlock ——由於此時你假設全部的照片都已下載完成。但很不幸,此時並不能保證全部的下載都已完成。
Photo 類的實例方法用某個 URL 開始下載某個文件並當即返回,但此時下載並未完成。換句話說,當 downloadPhotoWithCompletionBlock: 在其末尾調用 completionBlock 時,它就假設了它本身所使用的方法全都是同步的,並且每一個方法都完成了它們的工做。
然而,-[Photo initWithURL:withCompletionBlock:] 是異步執行的,會當即返回——因此這種方式行不通。
所以,只有在全部的圖像下載任務都調用了它們本身的 Completion Block 以後,downloadPhotoWithCompletionBlock: 才能調用它本身的 completionBlock 。問題是:你該如何監控併發的異步事件?你不知道它們什麼時候完成,並且它們完成的順序徹底是不肯定的。
或許你能夠寫一些比較 Hacky 的代碼,用多個布爾值來記錄每一個下載的完成狀況,但這樣作就缺失了擴展性,並且說實話,代碼會很難看。
幸運的是, 解決這種對多個異步任務的完成進行監控的問題,剛好就是設計 dispatch_group 的目的。
Dispatch Groups(調度組)
Dispatch Group 會在整個組的任務都完成時通知你。這些任務能夠是同步的,也能夠是異步的,即使在不一樣的隊列也行。並且在整個組的任務都完成時,Dispatch Group 能夠用同步的或者異步的方式通知你。由於要監控的任務在不一樣隊列,那就用一個 dispatch_group_t 的實例來記下這些不一樣的任務。
當組中全部的事件都完成時,GCD 的 API 提供了兩種通知方式。
第一種是 dispatch_group_wait ,它會阻塞當前線程,直到組裏面全部的任務都完成或者等到某個超時發生。這剛好是你目前所須要的。
打開 PhotoManager.m,用下列實現替換 downloadPhotosWithCompletionBlock:
1 - (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock 2 { 3 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // 1 4 5 __block NSError *error; 6 dispatch_group_t downloadGroup = dispatch_group_create(); // 2 7 8 for (NSInteger i = 0; i < 3; i++) { 9 NSURL *url; 10 switch (i) { 11 case 0: 12 url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; 13 break; 14 case 1: 15 url = [NSURL URLWithString:kSuccessKidURLString]; 16 break; 17 case 2: 18 url = [NSURL URLWithString:kLotsOfFacesURLString]; 19 break; 20 default: 21 break; 22 } 23 24 dispatch_group_enter(downloadGroup); // 3 25 Photo *photo = [[Photo alloc] initwithURL:url 26 withCompletionBlock:^(UIImage *image, NSError *_error) { 27 if (_error) { 28 error = _error; 29 } 30 dispatch_group_leave(downloadGroup); // 4 31 }]; 32 33 [[PhotoManager sharedManager] addPhoto:photo]; 34 } 35 dispatch_group_wait(downloadGroup, DISPATCH_TIME_FOREVER); // 5 36 dispatch_async(dispatch_get_main_queue(), ^{ // 6 37 if (completionBlock) { // 7 38 completionBlock(error); 39 } 40 }); 41 }); 42 }
按照註釋的順序,你會看到:
1. 由於你在使用的是同步的 dispatch_group_wait ,它會阻塞當前線程,因此你要用 dispatch_async 將整個方法放入後臺隊列以免阻塞主線程。
2. 建立一個新的 Dispatch Group,它的做用就像一個用於未完成任務的計數器。
3. dispatch_group_enter 手動通知 Dispatch Group 任務已經開始。你必須保證 dispatch_group_enter 和 dispatch_group_leave 成對出現,不然你可能會遇到詭異的崩潰問題。
4. 手動通知 Group 它的工做已經完成。再次說明,你必需要確保進入 Group 的次數和離開 Group 的次數相等。
5. dispatch_group_wait 會一直等待,直到任務所有完成或者超時。若是在全部任務完成前超時了,該函數會返回一個非零值。你能夠對此返回值作條件判斷以肯定是否超出等待週期;然而,你在這裏用 DISPATCH_TIME_FOREVER 讓它永遠等待。它的意思,勿庸置疑就是,永-遠-等-待!這樣很好,由於圖片的建立工做老是會完成的。
6. 此時此刻,你已經確保了,要麼全部的圖片任務都已完成,要麼發生了超時。而後,你在主線程上運行 completionBlock 回調。這會將工做放到主線程上,並在稍後執行。
7. 最後,檢查 completionBlock 是否爲 nil,若是不是,那就運行它。
編譯並運行你的應用,嘗試下載多個圖片,觀察你的應用是在什麼時候運行 completionBlock 的。
注意:若是你是在真機上運行應用,並且網絡活動發生得太快以至難以觀察 completionBlock 被調用的時刻,那麼你能夠在 Settings 應用裏的開發者相關部分裏打開一些網絡設置,以確保代碼按照咱們所指望的那樣工做。只需去往 Network Link Conditioner 區,開啓它,再選擇一個 Profile,「Very Bad Network」 就不錯。
若是你是在模擬器裏運行應用,你可使用 來自 GitHub 的 Network Link Conditioner 來改變網絡速度。它會成爲你工具箱中的一個好工具,由於它強制你研究你的應用在鏈接速度並不是最佳的狀況下會變成什麼樣。
目前爲止的解決方案還不錯,可是整體來講,若是可能,最好仍是要避免阻塞線程。你的下一個任務是重寫一些方法,以便當全部下載任務完成時能異步通知你。
在咱們轉向另一種使用 Dispatch Group 的方式以前,先看一個簡要的概述,關於什麼時候以及怎樣使用有着不一樣的隊列類型的 Dispatch Group :
1. 自定義串行隊列:它很適合當一組任務完成時發出通知。
2. 主隊列(串行):它也很適合這樣的狀況。但若是你要同步地等待全部工做地完成,那你就不該該使用它,由於你不能阻塞主線程。然而,異步模型是一個頗有吸引力的能用於在幾個較長任務(例如網絡調用)完成後更新 UI 的方式。
3. 併發隊列:它也很適合 Dispatch Group 和完成時通知。
Dispatch Group,第二種方式
上面的一切都很好,但在另外一個隊列上異步調度而後使用 dispatch_group_wait 來阻塞實在顯得有些笨拙。是的,還有另外一種方式……
在 PhotoManager.m 中找到 downloadPhotosWithCompletionBlock: 方法,用下面的實現替換它:
1 - (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock 2 { 3 // 1 4 __block NSError *error; 5 dispatch_group_t downloadGroup = dispatch_group_create(); 6 7 for (NSInteger i = 0; i < 3; i++) { 8 NSURL *url; 9 switch (i) { 10 case 0: 11 url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; 12 break; 13 case 1: 14 url = [NSURL URLWithString:kSuccessKidURLString]; 15 break; 16 case 2: 17 url = [NSURL URLWithString:kLotsOfFacesURLString]; 18 break; 19 default: 20 break; 21 } 22 23 dispatch_group_enter(downloadGroup); // 2 24 Photo *photo = [[Photo alloc] initwithURL:url 25 withCompletionBlock:^(UIImage *image, NSError *_error) { 26 if (_error) { 27 error = _error; 28 } 29 dispatch_group_leave(downloadGroup); // 3 30 }]; 31 32 [[PhotoManager sharedManager] addPhoto:photo]; 33 } 34 35 dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ // 4 36 if (completionBlock) { 37 completionBlock(error); 38 } 39 }); 40 }
下面解釋新的異步方法如何工做:
1. 在新的實現裏,由於你沒有阻塞主線程,因此你並不須要將方法包裹在 async 調用中。
2. 一樣的 enter 方法,沒作任何修改。
3. 一樣的 leave 方法,也沒作任何修改。
4. dispatch_group_notify 以異步的方式工做。當 Dispatch Group 中沒有任何任務時,它就會執行其代碼,那麼 completionBlock 便會運行。你還指定了運行 completionBlock 的隊列,此處,主隊列就是你所須要的。
對於這個特定的工做,上面的處理明顯更清晰,並且也不會阻塞任何線程。
太多併發帶來的風險
既然你的工具箱裏有了這些新工具,你大概作任何事情都想使用它們,對吧?
看看 PhotoManager 中的 downloadPhotosWithCompletionBlock 方法。你可能已經注意到這裏的 for 循環,它迭代三次,下載三個不一樣的圖片。你的任務是嘗試讓 for 循環併發運行,以提升其速度。
dispatch_apply 恰好可用於這個任務。
dispatch_apply 表現得就像一個 for 循環,但它能併發地執行不一樣的迭代。這個函數是同步的,因此和普通的 for 循環同樣,它只會在全部工做都完成後纔會返回。
當在 Block 內計算任何給定數量的工做的最佳迭代數量時,必需要當心,由於過多的迭代和每一個迭代只有少許的工做會致使大量開銷以至它能抵消任何因併發帶來的收益。而被稱爲跨越式(striding)的技術能夠在此幫到你,即經過在每一個迭代裏多作幾個不一樣的工做。
譯者注:大概就能減小併發數量吧,做者是提醒你們注意併發的開銷,記在內心!
那什麼時候才適合用 dispatch_apply 呢?
1. 自定義串行隊列:串行隊列會徹底抵消 dispatch_apply 的功能;你還不如直接使用普通的 for 循環。
2. 主隊列(串行):與上面同樣,在串行隊列上不適合使用 dispatch_apply 。仍是用普通的 for 循環吧。
3. 併發隊列:對於併發循環來講是很好選擇,特別是當你須要追蹤任務的進度時。
回到 downloadPhotosWithCompletionBlock: 並用下列實現替換它:
1 - (void)downloadPhotosWithCompletionBlock:(BatchPhotoDownloadingCompletionBlock)completionBlock 2 { 3 __block NSError *error; 4 dispatch_group_t downloadGroup = dispatch_group_create(); 5 6 dispatch_apply(3, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t i) { 7 8 NSURL *url; 9 switch (i) { 10 case 0: 11 url = [NSURL URLWithString:kOverlyAttachedGirlfriendURLString]; 12 break; 13 case 1: 14 url = [NSURL URLWithString:kSuccessKidURLString]; 15 break; 16 case 2: 17 url = [NSURL URLWithString:kLotsOfFacesURLString]; 18 break; 19 default: 20 break; 21 } 22 23 dispatch_group_enter(downloadGroup); 24 Photo *photo = [[Photo alloc] initwithURL:url 25 withCompletionBlock:^(UIImage *image, NSError *_error) { 26 if (_error) { 27 error = _error; 28 } 29 dispatch_group_leave(downloadGroup); 30 }]; 31 32 [[PhotoManager sharedManager] addPhoto:photo]; 33 }); 34 35 dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{ 36 if (completionBlock) { 37 completionBlock(error); 38 } 39 }); 40 }
你的循環如今是並行運行的了;在上面的代碼中,在調用 dispatch_apply 時,你用第一次參數指明瞭迭代的次數,用第二個參數指定了任務運行的隊列,而第三個參數是一個 Block。
要知道雖然你有代碼保證添加相片時線程安全,但圖片的順序卻可能不一樣,這取決於線程完成的順序。
編譯並運行,而後從 「Le Internet」 添加一些照片。注意到區別了嗎?
在真機上運行新代碼會稍微更快的獲得結果。但咱們所作的這些提速工做真的值得嗎?
實際上,在這個例子裏並不值得。下面是緣由:
1. 你建立並行運行線程而付出的開銷,極可能比直接使用 for 循環要多。若你要以合適的步長迭代很是大的集合,那才應該考慮使用 dispatch_apply。
2. 你用於建立應用的時間是有限的——除非實在太糟糕不然不要浪費時間去提早優化代碼。若是你要優化什麼,那去優化那些明顯值得你付出時間的部分。你能夠經過在 Instruments 裏分析你的應用,找出最長運行時間的方法。看看 如何在 Xcode 中使用 Instruments 能夠學到更多相關知識。
3. 一般狀況下,優化代碼會讓你的代碼更加複雜,不利於你本身和其餘開發者閱讀。請確保添加的複雜性能換來足夠多的好處。
記住,不要在優化上太瘋狂。你只會讓你本身和後來者更難以讀懂你的代碼。
原文連接:
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