【轉】iOS多線程編程技術之NSThread、Cocoa NSOperation、GCD

 
簡介
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. - (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 
  2. + (void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument 
 
第一個是實例方法,第二個是類方法
  1. 一、[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:nil];   
  2.   
  3. 二、NSThread* myThread = [[NSThread alloc] initWithTarget:self   
  4.                                         selector:@selector(doSomething:)   
  5.                                         object:nil];   
  6. [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. //  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.    
  32. - (void)viewDidLoad   
  33. {   
  34.     [super viewDidLoad];   
  35.        
  36. //    [NSThread detachNewThreadSelector:@selector(downloadImage:) toTarget:self withObject:kURL];   
  37.     NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(downloadImage:) object:kURL];   
  38.     [thread start];   
  39. }   
  40.    
  41. - (void)didReceiveMemoryWarning   
  42. {   
  43.     [super didReceiveMemoryWarning];   
  44.     // Dispose of any resources that can be recreated.   
  45. }   
  46.    
  47. @end   
 
線程間通信
線程下載完圖片後怎麼通知主線程更新界面呢?
  1. [self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES]; 
 
performSelectorOnMainThread是NSObject的方法,除了能夠更新主線程的數據外,還能夠更新其餘線程的好比:
  1. performSelector:onThread:withObject:waitUntilDone:  
 
運行下載圖片:
 
線程同步
咱們演示一個經典的賣票的例子來說NSThread的線程同步: 
  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.        
  14.     ticketsThreadtwo = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];   
  15.     [ticketsThreadtwo setName:@"Thread-2"];   
  16.     [ticketsThreadtwo start];   
  17.        
  18.     self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];   
  19.     // Override point for customization after application launch.   
  20.     self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];   
  21.     self.window.rootViewController = self.viewController;   
  22.     [self.window makeKeyAndVisible];   
  23.     return YES;   
  24. }   
  25.    
  26. - (void)run{   
  27.     while (TRUE) {   
  28.         // 上鎖   
  29. //        [ticketsCondition lock];   
  30.         [theLock lock];   
  31.         if(tickets >= 0){   
  32.             [NSThread sleepForTimeInterval:0.09];   
  33.             count = 100 - tickets;   
  34.             NSLog(@"當前票數是:%d,售出:%d,線程名:%@",tickets,count,[[NSThread currentThread] name]);   
  35.             tickets--;   
  36.         }else{   
  37.             break;   
  38.         }   
  39.         [theLock unlock];   
  40. //        [ticketsCondition unlock];   
  41.     }   
  42. }   
 若是沒有線程同步的lock,賣票數多是-1.加上lock以後線程同步保證了數據的正確性。
 
上面例子我使用了兩種鎖,一種NSCondition ,一種是:NSLock。 NSCondition我已經註釋了。
 
線程的順序執行
他們均可以經過[ticketsCondition signal]; 發送信號的方式,在一個線程喚醒另一個線程的等待。
 
好比: 
  1. #import "AppDelegate.h"   
  2.    
  3. #import "ViewController.h"   
  4.    
  5. @implementation AppDelegate   
  6.    
  7. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions   
  8. {   
  9.        
  10.     tickets = 100;   
  11.     count = 0;   
  12.     theLock = [[NSLock alloc] init];   
  13.     // 鎖對象   
  14.     ticketsCondition = [[NSCondition alloc] init];   
  15.     ticketsThreadone = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];   
  16.     [ticketsThreadone setName:@"Thread-1"];   
  17.     [ticketsThreadone start];   
  18.        
  19.     ticketsThreadtwo = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];   
  20.     [ticketsThreadtwo setName:@"Thread-2"];   
  21.     [ticketsThreadtwo start];   
  22.        
  23.     NSThread *ticketsThreadthree = [[NSThread alloc] initWithTarget:self selector:@selector(run3) object:nil];   
  24.     [ticketsThreadthree setName:@"Thread-3"];   
  25.     [ticketsThreadthree start];       
  26.     self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];   
  27.     // Override point for customization after application launch.   
  28.     self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];   
  29.     self.window.rootViewController = self.viewController;   
  30.     [self.window makeKeyAndVisible];   
  31.     return YES;   
  32. }   
  33.    
  34. -(void)run3{   
  35.     while (YES) {   
  36.         [ticketsCondition lock];   
  37.         [NSThread sleepForTimeInterval:3];   
  38.         [ticketsCondition signal];   
  39.         [ticketsCondition unlock];   
  40.     }   
  41. }   
  42.    
  43. - (void)run{   
  44.     while (TRUE) {   
  45.         // 上鎖   
  46.         [ticketsCondition lock];   
  47.         [ticketsCondition wait];   
  48.         [theLock lock];   
  49.         if(tickets >= 0){   
  50.             [NSThread sleepForTimeInterval:0.09];   
  51.             count = 100 - tickets;   
  52.             NSLog(@"當前票數是:%d,售出:%d,線程名:%@",tickets,count,[[NSThread currentThread] name]);   
  53.             tickets--;   
  54.         }else{   
  55.             break;   
  56.         }   
  57.         [theLock unlock];   
  58.         [ticketsCondition unlock];   
  59.     }   
  60. }   
 wait是等待,我加了一個 線程3 去喚醒其餘兩個線程鎖中的wait
 
其餘同步
咱們可使用指令 @synchronized 來簡化 NSLock的使用,這樣咱們就沒必要顯示編寫建立NSLock,加鎖並解鎖相關代碼。
  1. - (void)doSomeThing:(id)anObj 
  2.     @synchronized(anObj) 
  3.     { 
  4.         // Everything between the braces is protected by the @synchronized directive. 
  5.     } 
 
還有其餘的一些鎖對象,好比:循環鎖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. #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. -(void)updateUI:(UIImage*) image{   
  30.     self.imageView.image = image;   
  31. }   
代碼註釋:
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如何使用?
 
一、經常使用的方法dispatch_async
爲了不界面在處理耗時的操做時卡死,好比讀取網絡數據,IO,數據庫讀寫等,咱們會在另一個線程中處理這些操做,而後通知主線程更新界面。
 
用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();     
 
雖然dispatch queue是引用計數的對象,可是以上兩個都是全局的隊列,不用retain或release。
 
二、dispatch_group_async的使用
dispatch_group_async能夠實現監聽一組任務是否完成,完成後獲得通知執行其餘的操做。這個方法頗有用,好比你執行三個下載任務,當三個任務都下載完成後你才通知界面說完成的了。下面是一段例子代碼:
  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被打印。
 
三、dispatch_barrier_async的使用
dispatch_barrier_async是在前面的任務執行結束後它才執行,並且它後面的任務等它執行完成以後纔會執行
例子代碼以下:
  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 
 
請注意執行的時間,能夠看到執行的順序如上所述。
 
四、dispatch_apply 
執行某個代碼片斷N次。
  1. dispatch_apply(5, globalQ, ^(size_t index) { 
  2.     // 執行5次 
  3. }); 
 
本篇使用的到的例子代碼: http://download.csdn.net/detail/totogo2010/4596471
 
GCD還有不少其餘用法,能夠參考 官方文檔http://en.wikipedia.org/wiki/Grand_Central_Dispatch
 
擴展閱讀:
 
相關文章
相關標籤/搜索