原文出處:容芳志的博客
簡(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)開銷
NSThread實(shí)現(xiàn)的技術(shù)有下面三種:

一般使用cocoa thread 技術(shù)。
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í)行。
GCD
Grand Central Dispatch (GCD)是Apple開發(fā)的一個(gè)多核編程的解決方法。在iOS4.0開始之后才能使用。GCD是一個(gè)替代諸如NSThread, NSOperationQueue, NSInvocationOperation等技術(shù)的很高效和強(qiáng)大的技術(shù)。現(xiàn)在的iOS系統(tǒng)都升級(jí)到9了,所以不用擔(dān)心該技術(shù)不能使用。
介紹完這三種多線程編程方式,本文將依次介紹這三種技術(shù)的使用。
(一)NSThread的使用
NSThread 有兩種直接創(chuàng)建方式:
-(id)initWithTarget:(id)targetselector:(SEL)selector object:(id)argument
+(void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument
第一個(gè)是實(shí)例方法,第二個(gè)是類方法
1、[NSThread detachNewThreadSelector:@selector(doSomething:)toTarget:self withObject:nil];
2、NSThread*myThread=[[NSThread alloc]initWithTarget:self?selector:@selector(doSomething:)?object:nil];
[myThread start];
參數(shù)的意義:
selector :線程執(zhí)行的方法,這個(gè)selector只能有一個(gè)參數(shù),而且不能有返回值。
target ?:selector消息發(fā)送的對(duì)象
argument:傳輸給target的唯一參數(shù),也可以是nil
第一種方式會(huì)直接創(chuàng)建線程并且開始運(yùn)行線程,第二種方式是先創(chuàng)建線程對(duì)象,然后再運(yùn)行線程操作,在運(yùn)行線程操作前可以設(shè)置線程的優(yōu)先級(jí)等線程信息
不顯式創(chuàng)建線程的方法:
用NSObject的類方法 ?performSelectorInBackground:withObject: 創(chuàng)建一個(gè)線程:
[Obj performSelectorInBackground:@selector(doSomething)withObject:nil];
下載圖片的例子:
新建singeView app
新建項(xiàng)目,并在xib文件上放置一個(gè)imageView控件。按住control鍵拖到viewController.h文件中創(chuàng)建imageView IBOutlet?ViewController.m中實(shí)現(xiàn):
#import "ViewController.h"
#define kURL @"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"
@interfaceViewController()
@end
@implementationViewController
-(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
{
[superviewDidLoad];
//????[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
線程間通訊
線程下載完圖片后怎么通知主線程更新界面呢?
[self performSelectorOnMainThread:@selector(updateUI:)withObject:image waitUntilDone:YES];
performSelectorOnMainThread是NSObject的方法,除了可以更新主線程的數(shù)據(jù)外,還可以更新其他線程的比如:
performSelector:onThread:withObject:waitUntilDone:
運(yùn)行下載圖片:

線程同步
我們演示一個(gè)經(jīng)典的賣票的例子來(lái)講NSThread的線程同步:
#import
@classViewController;
@interfaceAppDelegate:UIResponder
{
inttickets;
intcount;
NSThread*ticketsThreadone;
NSThread*ticketsThreadtwo;
NSCondition*ticketsCondition;
NSLock *theLock;
}
@property(strong,nonatomic)UIWindow *window;
@property(strong,nonatomic)ViewController *viewController;
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
tickets=100;
count=0;
theLock=[[NSLock alloc]init];
// 鎖對(duì)象
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];
returnYES;
}
-(void)run{
while(TRUE){
// 上鎖
//????????[ticketsCondition lock];
[theLock lock];
if(tickets>=0){
[NSThread sleepForTimeInterval:0.09];
count=100-tickets;
NSLog(@"當(dāng)前票數(shù)是:%d,售出:%d,線程名:%@",tickets,count,[[NSThread currentThread]name]);
tickets--;
}else{
break;
}
[theLockunlock];
//????????[ticketsCondition unlock];
}
}
如果沒有線程同步的lock,賣票數(shù)可能是-1.加上lock之后線程同步保證了數(shù)據(jù)的正確性。
上面例子我使用了兩種鎖,一種NSCondition ,一種是:NSLock。 NSCondition我已經(jīng)注釋了。
線程的順序執(zhí)行
他們都可以通過(guò)[ticketsCondition signal]; 發(fā)送信號(hào)的方式,在一個(gè)線程喚醒另外一個(gè)線程的等待。
比如:
#import "AppDelegate.h"
#import "ViewController.h"
@implementationAppDelegate
-(BOOL)application:(UIApplication *)applicationdidFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
tickets=100;
count=0;
theLock=[[NSLock alloc]init];
// 鎖對(duì)象
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];
returnYES;
}
-(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āng)前票數(shù)是:%d,售出:%d,線程名:%@",tickets,count,[[NSThread currentThread]name]);
tickets--;
}else{
break;
}
[theLock unlock];
[ticketsCondition unlock];
}
}
wait是等待,我加了一個(gè) 線程3 去喚醒其他兩個(gè)線程鎖中的wait。
其他同步
我們可以使用指令 @synchronized 來(lái)簡(jiǎn)化 NSLock的使用,這樣我們就不必顯示編寫創(chuàng)建NSLock,加鎖并解鎖相關(guān)代碼。
-(void)doSomeThing:(id)anObj
{
@synchronized(anObj)
{
// Everything between the braces is protected by the @synchronized directive.
}
?}
還有其他的一些鎖對(duì)象,比如:循環(huán)鎖NSRecursiveLock,條件鎖NSConditionLock,分布式鎖NSDistributedLock等等,可以自己看官方文檔學(xué)習(xí)
NSThread下載圖片的例子代碼:http://download.csdn.net/detail/totogo2010/4591149
(二)Cocoa NSOperation的使用
使用 NSOperation的方式有兩種,
一種是用定義好的兩個(gè)子類:NSInvocationOperation 和 NSBlockOperation。
另一種是繼承NSOperation
如果你也熟悉Java,NSOperation就和java.lang.Runnable接口很相似。和Java的Runnable一樣,NSOperation也是設(shè)計(jì)用來(lái)擴(kuò)展的,只需繼承重寫NSOperation的一個(gè)方法main。相當(dāng)與java 中Runnalbe的Run方法。然后把NSOperation子類的對(duì)象放入NSOperationQueue隊(duì)列中,該隊(duì)列就會(huì)啟動(dòng)并開始處理它。
NSInvocationOperation例子:
這里同樣,我們實(shí)現(xiàn)一個(gè)下載圖片的例子。新建一個(gè)Single View app,拖放一個(gè)ImageView控件到xib界面。
實(shí)現(xiàn)代碼如下:
#import "ViewController.h"
#define kURL @"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"
@interfaceViewController()
@end
@implementationViewController
-(void)viewDidLoad
{
[superviewDidLoad];
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建了一個(gè)后臺(tái)線程,并且放到
2.NSOperationQueue中。后臺(tái)線程執(zhí)行downloadImage方法。
3.downloadImage 方法處理下載圖片的邏輯。下載完成后用performSelectorOnMainThread執(zhí)行主線程updateUI方法。
updateUI 并把下載的圖片顯示到圖片控件中。
運(yùn)行可以看到下載圖片顯示在界面上。

第二種方式繼承NSOperation
在.m文件中實(shí)現(xiàn)main方法,main方法編寫要執(zhí)行的代碼即可。
如何控制線程池中的線程數(shù)?
隊(duì)列里可以加入很多個(gè)NSOperation, 可以把NSOperationQueue看作一個(gè)線程池,可往線程池中添加操作(NSOperation)到隊(duì)列中。線程池中的線程可看作消費(fèi)者,從隊(duì)列中取走操作,并執(zhí)行它。
通過(guò)下面的代碼設(shè)置:
[queue setMaxConcurrentOperationCount:5];
線程池中的線程數(shù),也就是并發(fā)操作數(shù)。默認(rèn)情況下是-1,-1表示沒有限制,這樣會(huì)同時(shí)運(yùn)行隊(duì)列中的全部的操作。
(三)GCD的介紹和使用
介紹:
Grand Central Dispatch 簡(jiǎn)稱(GCD)是蘋果公司開發(fā)的技術(shù),以優(yōu)化的應(yīng)用程序支持多核心處理器和其他的對(duì)稱多處理系統(tǒng)的系統(tǒng)。這建立在任務(wù)并行執(zhí)行的線程池模式的基礎(chǔ)上的。它首次發(fā)布在Mac OS X 10.6 ,iOS 4及以上也可用。
設(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ù)的。
我們看看dispatch queue如何使用?
1、常用的方法dispatch_async
為了避免界面在處理耗時(shí)的操作時(shí)卡死,比如讀取網(wǎng)絡(luò)數(shù)據(jù),IO,數(shù)據(jù)庫(kù)讀寫等,我們會(huì)在另外一個(gè)線程中處理這些操作,然后通知主線程更新界面。
用GCD實(shí)現(xiàn)這個(gè)流程的操作比前面介紹的NSThread ?NSOperation的方法都要簡(jiǎn)單。代碼框架結(jié)構(gòu)如下:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^{
// 耗時(shí)的操作
dispatch_async(dispatch_get_main_queue(),^{
// 更新界面
});
});
如果這樣還不清晰的話,那我們還是用上兩篇博客中的下載圖片為例子,代碼如下:
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;
});
}
});
運(yùn)行顯示:

是不是代碼比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ì)列,如下:
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();
雖然dispatch queue是引用計(jì)數(shù)的對(duì)象,但是以上兩個(gè)都是全局的隊(duì)列,不用retain或release。
2、dispatch_group_async的使用
dispatch_group_async可以實(shí)現(xiàn)監(jiān)聽一組任務(wù)是否完成,完成后得到通知執(zhí)行其他的操作。這個(gè)方法很有用,比如你執(zhí)行三個(gè)下載任務(wù),當(dāng)三個(gè)任務(wù)都下載完成后你才通知界面說(shuō)完成的了。下面是一段例子代碼:
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是異步的方法,運(yùn)行后可以看到打印結(jié)果:
2012-09-2516:04:16.737gcdTest[43328:11303]group1
2012-09-2516:04:17.738gcdTest[43328:12a1b]group2
2012-09-2516:04:18.738gcdTest[43328:13003]group3
2012-09-2516:04:18.739gcdTest[43328:f803]updateUi
每個(gè)一秒打印一個(gè),當(dāng)?shù)谌齻€(gè)任務(wù)執(zhí)行后,upadteUi被打印。
3、dispatch_barrier_async的使用
dispatch_barrier_async是在前面的任務(wù)執(zhí)行結(jié)束后它才執(zhí)行,而且它后面的任務(wù)等它執(zhí)行完成之后才會(huì)執(zhí)行
例子代碼如下:
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");
});
打印結(jié)果:
2012-09-2516:20:33.967gcdTest[45547:11203]dispatch_async1
2012-09-2516:20:35.967gcdTest[45547:11303]dispatch_async2
2012-09-2516:20:35.967gcdTest[45547:11303]dispatch_barrier_async
2012-09-2516:20:40.970gcdTest[45547:11303]dispatch_async3
請(qǐng)注意執(zhí)行的時(shí)間,可以看到執(zhí)行的順序如上所述。
4、dispatch_apply
執(zhí)行某個(gè)代碼片段N次。
dispatch_apply(5,globalQ,^(size_t index){
// 執(zhí)行5次
});
本篇使用的到的例子代碼:http://download.csdn.net/detail/totogo2010/4596471
GCD還有很多其他用法,可以參考官方文檔、http://en.wikipedia.org/wiki/Grand_Central_Dispatch