1.NSThread
2.NSOperationQueue
3.GCD Thread
是這三種范式里面相對輕量級的,但也是使用起來最負責的,你需要自己管理thread的生命周期,線程之間的同步。線程共享同一應用程序的部分內存空間, 它們擁有對數據相同的訪問權限。你得協調多個線程對同一數據的訪問,一般做法是在訪問之前加鎖,這會導致一定的性能開銷。在 iOS 中我們可以使用多種形式的 thread: Cocoa threads: 使用NSThread 或直接從 NSObject 的類方法 performSelectorInBackground:withObject: 來創建一個線程。如果你選擇thread來實現多線程,那么 NSThread 就是官方推薦優先選用的方式。 Cocoa operations是基於 Obective-C實現的,類 NSOperation 以面向對象的方式封裝了用戶需要執行的操作,我們只要聚焦於我們需要做的事情,而不必太操心線程的管理,同步等事情,因為NSOperation已經為我 們封裝了這些事情。 NSOperation 是一個抽象基類,我們必須使用它的子類。iOS 提供了兩種默認實現:NSInvocationOperation 和 NSBlockOperation。 Grand Central Dispatch (GCD): iOS4 才開始支持,它提供了一些新的特性,以及運行庫來支持多核並行編程,它的關注點更高:如何在多個 cpu 上提升效率。
具體實施
這三種編程方式從上到下,抽象度層次是從低到高的,抽象度越高的使用越簡單,也是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