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 有兩種直接創建方式:

- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 
+ (void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument

第一個是實例方法,第二個是類方法

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: 創建一個線程:

[Obj performSelectorInBackground:@selector(doSomething) withObject:nil];

下載圖片的例子:
新建singeView app
新建項目,並在xib文件上放置一個imageView控件。按住control鍵拖到viewController.h文件中創建imageView IBOutlet ViewController.m中實現:


//   
//  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

線程間通訊
線程下載完圖片後怎麼通知主線程更新界面呢?

[self performSelectorOnMainThread:@selector(updateUI:) withObject:image waitUntilDone:YES];

performSelectorOnMainThread是NSObject的方法,除了可以更新主線程的數據外,還可以更新其他線程的比如:

performSelector:onThread:withObject:waitUntilDone:

運行下載圖片:


我們演示一個經典的賣票的例子來講NSThread的線程同步:

#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
- (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]; 發送信號的方式,在一個線程喚醒另外一個線程的等待。

比如:

#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,加鎖並解鎖相關代碼。

- (void)doSomeThing:(id)anObj 
{ 
    @synchronized(anObj) 
    { 
        // Everything between the braces is protected by the @synchronized directive. 
    } 
}

(二)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界面。
實現代碼如下:

#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)到隊列中。線程池中的線程可看作消費者,從隊列中取走操作,並執行它。

通過下面的代碼設置:

[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的方法都要簡單。代碼框架結構如下:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{   
    // 耗時的操作   
    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;   
         });   
    }   
});

是不是代碼比NSThread  NSOperation簡潔很多,而且GCD會自動根據任務在多核處理器上分配資源,優化程序。

系統給每一個應用程序提供了三個concurrent dispatch queues。這三個併發調度隊列是全局的,它們只有優先級的不同。因爲是全局的,我們不需要去創建。我們只需要通過使用函數dispath_get_global_queue去得到隊列,如下:

dispatch_queue_t globalQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
這裏也用到了系統默認就有一個串行隊列main_queue:

dispatch_queue_t mainQ = dispatch_get_main_queue();

雖然dispatch queue是引用計數的對象,但是以上兩個都是全局的隊列,不用retain或release。

2、dispatch_group_async的使用
dispatch_group_async可以實現監聽一組任務是否完成,完成後得到通知執行其他的操作。這個方法很有用,比如你執行三個下載任務,當三個任務都下載完成後你才通知界面說完成的了。下面是一段例子代碼:

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是異步的方法,運行後可以看到打印結果:

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是在前面的任務執行結束後它才執行,而且它後面的任務等它執行完成之後纔會執行
例子代碼如下:

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");   
});

打印結果:

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

請注意執行的時間,可以看到執行的順序如上所述。

dispatch_apply(5, globalQ, ^(size_t index) { 
    // 執行5次 
});

GCD還有很多其他用法,可以參考官方文檔http://en.wikipedia.org/wiki/Grand_Central_Dispatch


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章