簡介
iOS有三種多執行緒程式設計的技術,分別是:
(一)NSThread
(二)Cocoa NSOperation
(三)GCD(全稱:Grand Central Dispatch)
這三種程式設計方式從上到下,抽象度層次是從低到高的,抽象度越高的使用越簡單,也是Apple最推薦使用的。
三種方式的優缺點介紹:
1)NSThread:
優點:NSThread 比其他兩個輕量級
缺點:需要自己管理執行緒的生命週期,執行緒同步。執行緒同步對資料的加鎖會有一定的系統開銷
NSThread實現的技術有下面三種:
一般使用cocoa thread 技術。
Cocoa NSOperation
優點:不需要關心執行緒管理,資料同步的事情,可以把精力放在自己需要執行的操作上。
Cocoa operation 相關的類是 NSOperation ,NSOperationQueue。
NSOperation是個抽象類,使用它必須用它的子類,可以實現它或者使用它定義好的兩個子類:NSInvocationOperation 和 NSBlockOperation。
建立NSOperation子類的物件,把物件新增到NSOperationQueue佇列裡執行。
GCD
Grand Central Dispatch (GCD)是Apple開發的一個多核程式設計的解決方法。在iOS4.0開始之後才能使用。GCD是一個替代諸如NSThread, NSOperationQueue, NSInvocationOperation等技術的很高效和強大的技術。現在的iOS系統都升級到7了,所以不用擔心該技術不能使用。
介紹完這三種多執行緒程式設計方式,本文將依次介紹這三種技術的使用。
(一)NSThread的使用
NSThread 有兩種直接建立方式:
1 2 |
- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument + (void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument |
第一個是例項方法,第二個是類方法
1 2 3 4 5 6 |
1、[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:nil]; 2、NSThread* myThread = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething:) object:nil]; [myThread start]; |
引數的意義:
selector :執行緒執行的方法,這個selector只能有一個引數,而且不能有返回值。
target :selector訊息傳送的物件
argument:傳輸給target的唯一引數,也可以是nil
第一種方式會直接建立執行緒並且開始執行執行緒,第二種方式是先建立執行緒物件,然後再執行執行緒操作,在執行執行緒操作前可以設定執行緒的優先順序等執行緒資訊
不顯式建立執行緒的方法:
用NSObject的類方法 performSelectorInBackground:withObject: 建立一個執行緒:
1 |
[Obj performSelectorInBackground:@selector(doSomething) withObject:nil]; |
下載圖片的例子:
新建singeView app
新建專案,並在xib檔案上放置一個imageView控制元件。按住control鍵拖到viewController.h檔案中建立imageView IBOutlet ViewController.m中實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// // ViewController.m // NSThreadDemo // // Created by rongfzh on 12-9-23. // Copyright (c) 2012年 rongfzh. All rights reserved. // #import "ViewController.h" #define kURL @"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg" @interface ViewController () @end @implementation ViewController -(void)downloadImage:(NSString *) url{ NSData *data = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:url]]; UIImage *image = [[UIImage alloc]initWithData:data]; if(image == nil){ }else{ [self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES]; } } -(void)updateUI:(UIImage*) image{ self.imageView.image = image; } - (void)viewDidLoad { [super viewDidLoad]; // [NSThread detachNewThreadSelector:@selector(downloadImage:) toTarget:self withObject:kURL]; NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(downloadImage:) object:kURL]; [thread start]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } @end |
執行緒間通訊
執行緒下載完圖片後怎麼通知主執行緒更新介面呢?
1 |
[self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES]; |
performSelectorOnMainThread是NSObject的方法,除了可以更新主執行緒的資料外,還可以更新其他執行緒的比如:
1 |
performSelector:onThread:withObject:waitUntilDone: |
執行緒同步
我們演示一個經典的賣票的例子來講NSThread的執行緒同步:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#import <UIKit/UIKit.h> @class ViewController; @interface AppDelegate : UIResponder <UIApplicationDelegate> { int tickets; int count; NSThread* ticketsThreadone; NSThread* ticketsThreadtwo; NSCondition* ticketsCondition; NSLock *theLock; } @property (strong, nonatomic) UIWindow *window; @property (strong, nonatomic) ViewController *viewController; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { tickets = 100; count = 0; theLock = [[NSLock alloc] init]; // 鎖物件 ticketsCondition = [[NSCondition alloc] init]; ticketsThreadone = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; [ticketsThreadone setName:@"Thread-1"]; [ticketsThreadone start]; ticketsThreadtwo = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; [ticketsThreadtwo setName:@"Thread-2"]; [ticketsThreadtwo start]; self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil]; self.window.rootViewController = self.viewController; [self.window makeKeyAndVisible]; return YES; } - (void)run{ while (TRUE) { // 上鎖 // [ticketsCondition lock]; [theLock lock]; if(tickets >= 0){ [NSThread sleepForTimeInterval:0.09]; count = 100 - tickets; NSLog(@"當前票數是:%d,售出:%d,執行緒名:%@",tickets,count,[[NSThread currentThread] name]); tickets--; }else{ break; } [theLock unlock]; // [ticketsCondition unlock]; } } |
如果沒有執行緒同步的lock,賣票數可能是-1.加上lock之後執行緒同步保證了資料的正確性。
上面例子我使用了兩種鎖,一種NSCondition ,一種是:NSLock。 NSCondition我已經註釋了。
執行緒的順序執行
他們都可以通過[ticketsCondition signal]; 傳送訊號的方式,在一個執行緒喚醒另外一個執行緒的等待。
比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
#import "AppDelegate.h" #import "ViewController.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { tickets = 100; count = 0; theLock = [[NSLock alloc] init]; // 鎖物件 ticketsCondition = [[NSCondition alloc] init]; ticketsThreadone = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; [ticketsThreadone setName:@"Thread-1"]; [ticketsThreadone start]; ticketsThreadtwo = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; [ticketsThreadtwo setName:@"Thread-2"]; [ticketsThreadtwo start]; NSThread *ticketsThreadthree = [[NSThread alloc] initWithTarget:self selector:@selector(run3) object:nil]; [ticketsThreadthree setName:@"Thread-3"]; [ticketsThreadthree start]; self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil]; self.window.rootViewController = self.viewController; [self.window makeKeyAndVisible]; return YES; } -(void)run3{ while (YES) { [ticketsCondition lock]; [NSThread sleepForTimeInterval:3]; [ticketsCondition signal]; [ticketsCondition unlock]; } } - (void)run{ while (TRUE) { // 上鎖 [ticketsCondition lock]; [ticketsCondition wait]; [theLock lock]; if(tickets >= 0){ [NSThread sleepForTimeInterval:0.09]; count = 100 - tickets; NSLog(@"當前票數是:%d,售出:%d,執行緒名:%@",tickets,count,[[NSThread currentThread] name]); tickets--; }else{ break; } [theLock unlock]; [ticketsCondition unlock]; } } |
wait是等待,我加了一個 執行緒3 去喚醒其他兩個執行緒鎖中的wait。
其他同步
我們可以使用指令 @synchronized 來簡化 NSLock的使用,這樣我們就不必顯示編寫建立NSLock,加鎖並解鎖相關程式碼。
1 2 3 4 5 6 7 |
- (void)doSomeThing:(id)anObj { @synchronized(anObj) { // Everything between the braces is protected by the @synchronized directive. } } |
還有其他的一些鎖物件,比如:迴圈鎖NSRecursiveLock,條件鎖NSConditionLock,分散式鎖NSDistributedLock等等,可以自己看官方文件學習
NSThread下載圖片的例子程式碼:http://download.csdn.net/detail/totogo2010/4591149
(二)Cocoa NSOperation的使用
使用 NSOperation的方式有兩種,
一種是用定義好的兩個子類:NSInvocationOperation 和 NSBlockOperation。
另一種是繼承NSOperation
如果你也熟悉Java,NSOperation就和java.lang.Runnable介面很相似。和Java的Runnable一樣,NSOperation也是設計用來擴充套件的,只需繼承重寫NSOperation的一個方法main。相當與java 中Runnalbe的Run方法。然後把NSOperation子類的物件放入NSOperationQueue佇列中,該佇列就會啟動並開始處理它。
NSInvocationOperation例子:
這裡同樣,我們實現一個下載圖片的例子。新建一個Single View app,拖放一個ImageView控制元件到xib介面。
實現程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#import "ViewController.h" #define kURL @"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; NSInvocationOperation *operation = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(downloadImage:) object:kURL]; NSOperationQueue *queue = [[NSOperationQueue alloc]init]; [queue addOperation:operation]; // Do any additional setup after loading the view, typically from a nib. } -(void)downloadImage:(NSString *)url{ NSLog(@"url:%@", url); NSURL *nsUrl = [NSURL URLWithString:url]; NSData *data = [[NSData alloc]initWithContentsOfURL:nsUrl]; UIImage * image = [[UIImage alloc]initWithData:data]; [self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES]; } -(void)updateUI:(UIImage*) image{ self.imageView.image = image; } |
程式碼註釋:
1.viewDidLoad方法裡可以看到我們用NSInvocationOperation建了一個後臺執行緒,並且放到2.NSOperationQueue中。後臺執行緒執行downloadImage方法。
3.downloadImage 方法處理下載圖片的邏輯。下載完成後用performSelectorOnMainThread執行主執行緒updateUI方法。
updateUI 並把下載的圖片顯示到圖片控制元件中。
第二種方式繼承NSOperation
在.m檔案中實現main方法,main方法編寫要執行的程式碼即可。
如何控制執行緒池中的執行緒數?
佇列裡可以加入很多個NSOperation, 可以把NSOperationQueue看作一個執行緒池,可往執行緒池中新增操作(NSOperation)到佇列中。執行緒池中的執行緒可看作消費者,從佇列中取走操作,並執行它。
通過下面的程式碼設定:
1 |
[queue setMaxConcurrentOperationCount:5]; |
執行緒池中的執行緒數,也就是併發運算元。預設情況下是-1,-1表示沒有限制,這樣會同時執行佇列中的全部的操作。
(三)GCD的介紹和使用
介紹:
Grand Central Dispatch 簡稱(GCD)是蘋果公司開發的技術,以優化的應用程式支援多核心處理器和其他的對稱多處理系統的系統。這建立在任務並行執行的執行緒池模式的基礎上的。它首次釋出在Mac OS X 10.6 ,iOS 4及以上也可用。
設計:
GCD的工作原理是:讓程式平行排隊的特定任務,根據可用的處理資源,安排他們在任何可用的處理器核心上執行任務。
一個任務可以是一個函式(function)或者是一個block。 GCD的底層依然是用執行緒實現,不過這樣可以讓程式設計師不用關注實現的細節。
GCD中的FIFO佇列稱為dispatch queue,它可以保證先進來的任務先得到執行。
dispatch queue分為下面三種:
Serial
又稱為private dispatch queues,同時只執行一個任務。Serial queue通常用於同步訪問特定的資源或資料。當你建立多個Serial queue時,雖然它們各自是同步執行的,但Serial queue與Serial queue之間是併發執行的。
Concurrent
又稱為global dispatch queue,可以併發地執行多個任務,但是執行完成的順序是隨機的。
Main dispatch queue
它是全域性可用的serial queue,它是在應用程式主執行緒上執行任務的。
我們看看dispatch queue如何使用?
1、常用的方法dispatch_async
為了避免介面在處理耗時的操作時卡死,比如讀取網路資料,IO,資料庫讀寫等,我們會在另外一個執行緒中處理這些操作,然後通知主執行緒更新介面。
用GCD實現這個流程的操作比前面介紹的NSThread NSOperation的方法都要簡單。程式碼框架結構如下:
1 2 3 4 5 6 |
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 耗時的操作 dispatch_async(dispatch_get_main_queue(), ^{ // 更新介面 }); }); |
如果這樣還不清晰的話,那我們還是用上兩篇部落格中的下載圖片為例子,程式碼如下:
1 2 3 4 5 6 7 8 9 10 |
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSURL * url = [NSURL URLWithString:@"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"]; NSData * data = [[NSData alloc]initWithContentsOfURL:url]; UIImage *image = [[UIImage alloc]initWithData:data]; if (data != nil) { dispatch_async(dispatch_get_main_queue(), ^{ self.imageView.image = image; }); } }); |
是不是程式碼比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(); |
雖然dispatch queue是引用計數的物件,但是以上兩個都是全域性的佇列,不用retain或release。
2、dispatch_group_async的使用
dispatch_group_async可以實現監聽一組任務是否完成,完成後得到通知執行其他的操作。這個方法很有用,比如你執行三個下載任務,當三個任務都下載完成後你才通知介面說完成的了。下面是一段例子程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, queue, ^{ [NSThread sleepForTimeInterval:1]; NSLog(@"group1"); }); dispatch_group_async(group, queue, ^{ [NSThread sleepForTimeInterval:2]; NSLog(@"group2"); }); dispatch_group_async(group, queue, ^{ [NSThread sleepForTimeInterval:3]; NSLog(@"group3"); }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"updateUi"); }); dispatch_release(group); |
dispatch_group_async是非同步的方法,執行後可以看到列印結果:
1 2 3 4 |
2012-09-25 16:04:16.737 gcdTest[43328:11303] group1 2012-09-25 16:04:17.738 gcdTest[43328:12a1b] group2 2012-09-25 16:04:18.738 gcdTest[43328:13003] group3 2012-09-25 16:04:18.739 gcdTest[43328:f803] updateUi |
每個一秒列印一個,當第三個任務執行後,upadteUi被列印。
3、dispatch_barrier_async的使用
dispatch_barrier_async是在前面的任務執行結束後它才執行,而且它後面的任務等它執行完成之後才會執行
例子程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
dispatch_queue_t queue = dispatch_queue_create("gcdtest.rongfzh.yc", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ [NSThread sleepForTimeInterval:2]; NSLog(@"dispatch_async1"); }); dispatch_async(queue, ^{ [NSThread sleepForTimeInterval:4]; NSLog(@"dispatch_async2"); }); dispatch_barrier_async(queue, ^{ NSLog(@"dispatch_barrier_async"); [NSThread sleepForTimeInterval:4]; }); dispatch_async(queue, ^{ [NSThread sleepForTimeInterval:1]; NSLog(@"dispatch_async3"); }); |
列印結果:
1 2 3 4 |
2012-09-25 16:20:33.967 gcdTest[45547:11203] dispatch_async1 2012-09-25 16:20:35.967 gcdTest[45547:11303] dispatch_async2 2012-09-25 16:20:35.967 gcdTest[45547:11303] dispatch_barrier_async 2012-09-25 16:20:40.970 gcdTest[45547:11303] dispatch_async3 |
請注意執行的時間,可以看到執行的順序如上所述。
4、dispatch_apply
執行某個程式碼片段N次。
1 2 3 |
dispatch_apply(5, globalQ, ^(size_t index) { // 執行5次 }); |
本篇使用的到的例子程式碼:http://download.csdn.net/detail/totogo2010/4596471
GCD還有很多其他用法,可以參考官方文件、http://en.wikipedia.org/wiki/Grand_Central_Dispatch