天天看點

ios開發-多線程GCD之詳解 概覽 多線程 NSThread NSOperation GCD 線程同步 總結

概覽

大家都知道,在開發過程中應該盡可能減少使用者等待時間,讓程式盡可能快的完成運算。可是無論是哪種語言開發的程式最終往往轉換成彙編語言進而解釋成機器碼來執行。但是機器碼是按順序執行的,一個複雜的多步操作隻能一步步按順序逐個執行。改變這種狀況可以從兩個角度出發:對于單核處理器,可以将多個步驟放到不同的線程,這樣一來使用者完成UI操作後其他後續任務在其他線程中,當CPU空閑時會繼續執行,而此時對于使用者而言可以繼續進行其他操作;對于多核處理器,如果使用者在UI線程中完成某個操作之後,其他後續操作在别的線程中繼續執行,使用者同樣可以繼續進行其他UI操作,與此同時前一個操作的後續任務可以分散到多個空閑CPU中繼續執行(當然具體排程順序要根據程式設計而定),及解決了線程阻塞又提高了運作效率。蘋果從iPad2 開始使用雙核A5處理器(iPhone中從iPhone 4S開始使用),A7中還加入了協處理器,如何充分發揮這些處理器的性能确實值得思考。今天将重點分析iOS多線程開發:

  1. 多線程
    1. 簡介
    2. iOS多線程
  2. NSThread
    1. 解決線程阻塞問題
    2. 多線程并發
    3. 線程狀态
    4. 擴充-NSObject分類擴充
  3. NSOperation
    1. NSInvocationOperation
    2. NSBlockOperation
    3. 線程執行順序
  4. GCD
    1. 串行隊列
    2. 并發隊列
    3. 其他任務執行方法
  5. 線程同步
    1. NSLock同步鎖
    2. @synchronized代碼塊
    3. 擴充--使用GCD解決資源搶占問題
    4. 擴充--控制線程通信
  6. 總結
  7. 目 錄

多線程

簡介

當使用者播放音頻、下載下傳資源、進行圖像處理時往往希望做這些事情的時候其他操作不會被中斷或者希望這些操作過程中更加順暢。在單線程中一個線程隻能做一件事情,一件事情處理不完另一件事就不能開始,這樣勢必影響使用者體驗。早在單核處理器時期就有多線程,這個時候多線程更多的用于解決線程阻塞造成的使用者等待(通常是操作完UI後使用者不再幹涉,其他線程在等待隊列中,CPU一旦空閑就繼續執行,不影響使用者其他UI操作),其處理能力并沒有明顯的變化。如今無論是移動作業系統還是PC、伺服器都是多核處理器,于是“并行運算”就更多的被提及。一件事情我們可以分成多個步驟,在沒有順序要求的情況下使用多線程既能解決線程阻塞又能充分利用多核處理器運作能力。

下圖反映了一個包含8個操作的任務在一個有兩核心的CPU中建立四個線程運作的情況。假設每個核心有兩個線程,那麼每個CPU中兩個線程會交替執行,兩個CPU之間的操作會并行運算。單就一個CPU而言兩個線程可以解決線程阻塞造成的不流暢問題,其本身運作效率并沒有提高,多CPU的并行運算才真正解決了運作效率問題,這也正是并發和并行的差別。當然,不管是多核還是單核開發人員不用過多的擔心,因為任務具體配置設定給幾個CPU運算是由系統排程的,開發人員不用過多關心系統有幾個CPU。開發人員需要關心的是線程之間的依賴關系,因為有些操作必須在某個操作完成完才能執行,如果不能保證這個順序勢必會造成程式問題。

ios開發-多線程GCD之詳解 概覽 多線程 NSThread NSOperation GCD 線程同步 總結

iOS多線程

在iOS中每個程序啟動後都會建立一個主線程(UI線程),這個線程是其他線程的父線程。由于在iOS中除了主線程,其他子線程是獨立于Cocoa Touch的,是以隻有主線程可以更新UI界面(新版iOS中,使用其他線程更新UI可能也能成功,但是不推薦)。iOS中多線程使用并不複雜,關鍵是如何控制好各個線程的執行順序、處理好資源競争問題。常用的多線程開發有三種方式:

1.NSThread

2.NSOperation

3.GCD

三種方式是随着iOS的發展逐漸引入的,是以相比而言後者比前者更加簡單易用,并且GCD也是目前蘋果官方比較推薦的方式(它充分利用了多核處理器的運算性能)。做過.Net開發的朋友不難發現其實這三種開發方式 剛好對應.Net中的多線程、線程池和異步調用,是以在文章中也會對比講解。

NSThread

NSThread是輕量級的多線程開發,使用起來也并不複雜,但是使用NSThread需要自己管理線程生命周期。可以使用對象方法+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument直接将操作添加到線程中并啟動,也可以使用對象方法- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 建立一個線程對象,然後調用start方法啟動線程。

解決線程阻塞問題

在資源下載下傳過程中,由于網絡原因有時候很難保證下載下傳時間,如果不使用多線程可能使用者完成一個下載下傳操作需要長時間的等待,這個過程中無法進行其他操作。下面示範一個采用多線程下載下傳圖檔的過程,在這個示例中點選按鈕會啟動一個線程去下載下傳圖檔,下載下傳完成後使用UIImageView将圖檔顯示到界面中。可以看到使用者點選完下載下傳按鈕後,不管圖檔是否下載下傳完成都可以繼續操作界面,不會造成阻塞。

//
//  NSThread實作多線程
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"

@interface KCMainViewController (){
    UIImageView *_imageView;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self layoutUI];
}

#pragma mark 界面布局
-(void)layoutUI{
    _imageView =[[UIImageView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame];
    _imageView.contentMode=UIViewContentModeScaleAspectFit;
    [self.view addSubview:_imageView];
    
    UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame=CGRectMake(50, 500, 220, 25);
    [button setTitle:@"加載圖檔" forState:UIControlStateNormal];
    //添加方法
    [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

#pragma mark 将圖檔顯示到界面
-(void)updateImage:(NSData *)imageData{
    UIImage *image=[UIImage imageWithData:imageData];
    _imageView.image=image;
}

#pragma mark 請求圖檔資料
-(NSData *)requestData{
    //對于多線程操作建議把線程操作放到@autoreleasepool中
    @autoreleasepool {
        NSURL *url=[NSURL URLWithString:@"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"];
        NSData *data=[NSData dataWithContentsOfURL:url];
        return data;
    }
}

#pragma mark 加載圖檔
-(void)loadImage{
    //請求資料
    NSData *data= [self requestData];
    /*将資料顯示到UI控件,注意隻能在主線程中更新UI,
     另外performSelectorOnMainThread方法是NSObject的分類方法,每個NSObject對象都有此方法,
     它調用的selector方法是目前調用控件的方法,例如使用UIImageView調用的時候selector就是UIImageView的方法
     Object:代表調用方法的參數,不過隻能傳遞一個參數(如果有多個參數請使用對象進行封裝)
     waitUntilDone:是否線程任務完成執行
    */
    [self performSelectorOnMainThread:@selector(updateImage:) withObject:data waitUntilDone:YES];
}

#pragma mark 多線程下載下傳圖檔
-(void)loadImageWithMultiThread{
    //方法1:使用對象方法
    //建立一個線程,第一個參數是請求的操作,第二個參數是操作方法的參數
//    NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
//    //啟動一個線程,注意啟動一個線程并非就一定立即執行,而是處于就緒狀态,當系統排程時才真正執行
//    [thread start];
    
    //方法2:使用類方法
    [NSThread detachNewThreadSelector:@selector(loadImage) toTarget:self withObject:nil];
}
@end
      

運作效果:

ios開發-多線程GCD之詳解 概覽 多線程 NSThread NSOperation GCD 線程同步 總結

程式比較簡單,但是需要注意執行步驟:當點選了“加載圖檔”按鈕後啟動一個新的線程,這個線程在示範中大概用了5s左右,在這5s内UI線程是不會阻塞的,使用者可以進行其他操作,大約5s之後圖檔下載下傳完成,此時調用UI線程将圖檔顯示到界面中(這個過程瞬間完成)。另外前面也提到過,更新UI的時候使用UI線程,這裡調用了NSObject的分類擴充方法,調用UI線程完成更新。

多個線程并發

上面這個示範并沒有示範多個子線程操作之間的關系,現在不妨在界面中多加載幾張圖檔,每個圖檔都來自遠端請求。

大家應該注意到不管是使用+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument、- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 方法還是使用- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait方法都隻能傳一個參數,由于更新圖檔需要傳遞UIImageView的索引和圖檔資料,是以這裡不妨定義一個類儲存圖檔索引和圖檔資料以供後面使用。

KCImageData.h

//
//  KCImageData.h
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface KCImageData : NSObject

#pragma mark 索引
@property (nonatomic,assign) int index;

#pragma mark 圖檔資料
@property (nonatomic,strong) NSData *data;

@end      

接下來将建立多個UIImageView并建立多個線程用于往UIImageView中填充圖檔。

KCMainViewController.m

//
//  NSThread實作多線程
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"
#import "KCImageData.h"
#define ROW_COUNT 5
#define COLUMN_COUNT 3
#define ROW_HEIGHT 100
#define ROW_WIDTH ROW_HEIGHT
#define CELL_SPACING 10

@interface KCMainViewController (){
    NSMutableArray *_imageViews;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self layoutUI];
}

#pragma mark 界面布局
-(void)layoutUI{
    //建立多個圖檔控件用于顯示圖檔
    _imageViews=[NSMutableArray array];
    for (int r=0; r<ROW_COUNT; r++) {
        for (int c=0; c<COLUMN_COUNT; c++) {
            UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING                           ), ROW_WIDTH, ROW_HEIGHT)];
            imageView.contentMode=UIViewContentModeScaleAspectFit;
//            imageView.backgroundColor=[UIColor redColor];
            [self.view addSubview:imageView];
            [_imageViews addObject:imageView];

        }
    }
    
    UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame=CGRectMake(50, 500, 220, 25);
    [button setTitle:@"加載圖檔" forState:UIControlStateNormal];
    //添加方法
    [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

#pragma mark 将圖檔顯示到界面
-(void)updateImage:(KCImageData *)imageData{
    UIImage *image=[UIImage imageWithData:imageData.data];
    UIImageView *imageView= _imageViews[imageData.index];
    imageView.image=image;
}

#pragma mark 請求圖檔資料
-(NSData *)requestData:(int )index{
    //對于多線程操作建議把線程操作放到@autoreleasepool中
    @autoreleasepool {
        NSURL *url=[NSURL URLWithString:@"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"];
        NSData *data=[NSData dataWithContentsOfURL:url];
        return data;
    }
}

#pragma mark 加載圖檔
-(void)loadImage:(NSNumber *)index{
    //    NSLog(@"%i",i);
    //currentThread方法可以取得目前操作線程
    NSLog(@"current thread:%@",[NSThread currentThread]);
    
    int i=[index integerValue];
    
//    NSLog(@"%i",i);//未必按順序輸出
    
    NSData *data= [self requestData:i];
    
    KCImageData *imageData=[[KCImageData alloc]init];
    imageData.index=i;
    imageData.data=data;
    [self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES];
}

#pragma mark 多線程下載下傳圖檔
-(void)loadImageWithMultiThread{
    //建立多個線程用于填充圖檔
    for (int i=0; i<ROW_COUNT*COLUMN_COUNT; ++i) {
//        [NSThread detachNewThreadSelector:@selector(loadImage:) toTarget:self withObject:[NSNumber numberWithInt:i]];
        NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]];
        thread.name=[NSString stringWithFormat:@"myThread%i",i];//設定線程名稱
        [thread start];
    }
}
@end      
ios開發-多線程GCD之詳解 概覽 多線程 NSThread NSOperation GCD 線程同步 總結

通過NSThread的currentThread可以取得目前操作的線程,其中會記錄線程名稱name和編号number,需要注意主線程編号永遠為1。多個線程雖然按順序啟動,但是實際執行未必按照順序加載照片(loadImage:方法未必依次建立,可以通過在loadImage:中列印索引檢視),因為線程啟動後僅僅處于就緒狀态,實際是否執行要由CPU根據目前狀态排程。

從上面的運作效果大家不難發現,圖檔并未按順序加載,原因有兩個:第一,每個線程的實際執行順序并不一定按順序執行(雖然是按順序啟動);第二,每個線程執行時實際網絡狀況很可能不一緻。當然網絡問題無法改變,隻能盡可能讓網速更快,但是可以改變線程的優先級,讓15個線程優先執行某個線程。線程優先級範圍為0~1,值越大優先級越高,每個線程的優先級預設為0.5。修改圖檔下載下傳方法如下,改變最後一張圖檔加載的優先級,這樣可以提高它被優先加載的幾率,但是它也未必就第一個加載。因為首先其他線程是先啟動的,其次網絡狀況我們沒辦法修改:

-(void)loadImageWithMultiThread{
    NSMutableArray *threads=[NSMutableArray array];
    int count=ROW_COUNT*COLUMN_COUNT;
    //建立多個線程用于填充圖檔
    for (int i=0; i<count; ++i) {
//        [NSThread detachNewThreadSelector:@selector(loadImage:) toTarget:self withObject:[NSNumber numberWithInt:i]];
        NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]];
        thread.name=[NSString stringWithFormat:@"myThread%i",i];//設定線程名稱
        if(i==(count-1)){
            thread.threadPriority=1.0;
        }else{
            thread.threadPriority=0.0;
        }
        [threads addObject:thread];
    }
    
    for (int i=0; i<count; i++) {
        NSThread *thread=threads[i];
        [thread start];
    }
}      

線程狀态

線上程操作過程中可以讓某個線程休眠等待,優先執行其他線程操作,而且在這個過程中還可以修改某個線程的狀态或者終止某個指定線程。為了解決上面優先加載最後一張圖檔的問題,不妨讓其他線程先休眠一會等待最後一個線程執行。修改圖檔加載方法如下即可:

-(NSData *)requestData:(int )index{
    //對于多線程操作建議把線程操作放到@autoreleasepool中
    @autoreleasepool {
        //對非最後一張圖檔加載線程休眠2秒
        if (index!=(ROW_COUNT*COLUMN_COUNT-1)) {
            [NSThread sleepForTimeInterval:2.0];
        }
        NSURL *url=[NSURL URLWithString:_imageNames[index]];
        NSData *data=[NSData dataWithContentsOfURL:url];

        return data;
    }
}      

在這裡讓其他線程休眠2秒,此時你就會看到最後一張圖檔總是第一個加載(除非網速特别差)。

線程狀态分為isExecuting(正在執行)、isFinished(已經完成)、isCancellled(已經取消)三種。其中取消狀态程式可以幹預設定,隻要調用線程的cancel方法即可。但是需要注意在主線程中僅僅能設定線程狀态,并不能真正停止目前線程,如果要終止線程必須線上程中調用exist方法,這是一個靜态方法,調用該方法可以退出目前線程。

假設在圖檔加載過程中點選停止按鈕讓沒有完成的線程停止加載,可以改造程式如下:

//
//  NSThread實作多線程
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"
#import "KCImageData.h"
#define ROW_COUNT 5
#define COLUMN_COUNT 3
#define ROW_HEIGHT 100
#define ROW_WIDTH ROW_HEIGHT
#define CELL_SPACING 10

@interface KCMainViewController (){
    NSMutableArray *_imageViews;
    NSMutableArray *_imageNames;
    NSMutableArray *_threads;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self layoutUI];
}

#pragma mark 界面布局
-(void)layoutUI{
    //建立多個圖檔空間用于顯示圖檔
    _imageViews=[NSMutableArray array];
    for (int r=0; r<ROW_COUNT; r++) {
        for (int c=0; c<COLUMN_COUNT; c++) {
            UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING                           ), ROW_WIDTH, ROW_HEIGHT)];
            imageView.contentMode=UIViewContentModeScaleAspectFit;
//            imageView.backgroundColor=[UIColor redColor];
            [self.view addSubview:imageView];
            [_imageViews addObject:imageView];

        }
    }

    //加載按鈕
    UIButton *buttonStart=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    buttonStart.frame=CGRectMake(50, 500, 100, 25);
    [buttonStart setTitle:@"加載圖檔" forState:UIControlStateNormal];
    [buttonStart addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:buttonStart];
    
    //停止按鈕
    UIButton *buttonStop=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    buttonStop.frame=CGRectMake(160, 500, 100, 25);
    [buttonStop setTitle:@"停止加載" forState:UIControlStateNormal];
    [buttonStop addTarget:self action:@selector(stopLoadImage) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:buttonStop];
    
    //建立圖檔連結
    _imageNames=[NSMutableArray array];
    [_imageNames addObject:@    for (int i=0; i<IMAGE_COUNT; i++) {
        [_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];
    }    
}

#pragma mark 将圖檔顯示到界面
-(void)updateImage:(KCImageData *)imageData{
    UIImage *image=[UIImage imageWithData:imageData.data];
    UIImageView *imageView= _imageViews[imageData.index];
    imageView.image=image;
}

#pragma mark 請求圖檔資料
-(NSData *)requestData:(int )index{
    //對于多線程操作建議把線程操作放到@autoreleasepool中
    @autoreleasepool {
        NSURL *url=[NSURL URLWithString:_imageNames[index]];
        NSData *data=[NSData dataWithContentsOfURL:url];

        return data;
    }
}

#pragma mark 加載圖檔
-(void)loadImage:(NSNumber *)index{
    int i=[index integerValue];

    NSData *data= [self requestData:i];

    
    NSThread *currentThread=[NSThread currentThread];
    
//    如果目前線程處于取消狀态,則退出目前線程
    if (currentThread.isCancelled) {
        NSLog(@"thread(%@) will be cancelled!",currentThread);
        [NSThread exit];//取消目前線程
    }
    
    KCImageData *imageData=[[KCImageData alloc]init];
    imageData.index=i;
    imageData.data=data;
    [self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES];
}

#pragma mark 多線程下載下傳圖檔
-(void)loadImageWithMultiThread{
    int count=ROW_COUNT*COLUMN_COUNT;
    _threads=[NSMutableArray arrayWithCapacity:count];
    
    //建立多個線程用于填充圖檔
    for (int i=0; i<count; ++i) {
        NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]];
        thread.name=[NSString stringWithFormat:@"myThread%i",i];//設定線程名稱
        [_threads addObject:thread];
    }
    //循環啟動線程
    for (int i=0; i<count; ++i) {
        NSThread *thread= _threads[i];
        [thread start];
    }
}

#pragma mark 停止加載圖檔
-(void)stopLoadImage{
    for (int i=0; i<ROW_COUNT*COLUMN_COUNT; i++) {
        NSThread *thread= _threads[i];
        //判斷線程是否完成,如果沒有完成則設定為取消狀态
        //注意設定為取消狀态僅僅是改變了線程狀态而言,并不能終止線程
        if (!thread.isFinished) {
            [thread cancel];
            
        }
    }
}
@end      

運作效果(點選加載大概1秒後點選停止加載):

ios開發-多線程GCD之詳解 概覽 多線程 NSThread NSOperation GCD 線程同步 總結

使用NSThread在進行多線程開發過程中操作比較簡單,但是要控制線程執行順序并不容易(前面萬不得已采用了休眠的方法),另外在這個過程中如果列印線程會發現循環幾次就建立了幾個線程,這在實際開發過程中是不得不考慮的問題,因為每個線程的建立也是相當占用系統開銷的。

擴充--NSObject分類擴充方法

為了簡化多線程開發過程,蘋果官方對NSObject進行分類擴充(本質還是建立NSThread),對于簡單的多線程操作可以直接使用這些擴充方法。

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg:在背景執行一個操作,本質就是重新建立一個線程執行目前方法。

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait:在指定的線程上執行一個方法,需要使用者建立一個線程對象。

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait:在主線程上執行一個方法(前面已經使用過)。

例如前面加載圖多個圖檔的方法,可以改為背景線程執行:

-(void)loadImageWithMultiThread{
    int count=ROW_COUNT*COLUMN_COUNT;
    
    for (int i=0; i<count; ++i) {
        [self performSelectorInBackground:@selector(loadImage:) withObject:[NSNumber numberWithInt:i]];
    }
}      

NSOperation

使用NSOperation和NSOperationQueue進行多線程開發類似于C#中的線程池,隻要将一個NSOperation(實際開中需要使用其子類NSInvocationOperation、NSBlockOperation)放到NSOperationQueue這個隊列中線程就會依次啟動。NSOperationQueue負責管理、執行所有的NSOperation,在這個過程中可以更加容易的管理線程總數和控制線程之間的依賴關系。

NSOperation有兩個常用子類用于建立線程操作:NSInvocationOperation和NSBlockOperation,兩種方式本質沒有差別,但是是後者使用Block形式進行代碼組織,使用相對友善。

NSInvocationOperation

首先使用NSInvocationOperation進行一張圖檔的加載示範,整個過程就是:建立一個操作,在這個操作中指定調用方法和參數,然後加入到操作隊列。其他代碼基本不用修改,直接修加載圖檔方法如下:

-(void)loadImageWithMultiThread{
    /*建立一個調用操作
     object:調用方法參數
    */
    NSInvocationOperation *invocationOperation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
    //建立完NSInvocationOperation對象并不會調用,它由一個start方法啟動操作,但是注意如果直接調用start方法,則此操作會在主線程中調用,一般不會這麼操作,而是添加到NSOperationQueue中
//    [invocationOperation start];
    
    //建立操作隊列
    NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
    //注意添加到操作隊後,隊列會開啟一個線程執行此操作
    [operationQueue addOperation:invocationOperation];
}      

NSBlockOperation

下面采用NSBlockOperation建立多個線程加載圖檔。

//
//  NSOperation實作多線程
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"
#import "KCImageData.h"
#define ROW_COUNT 5
#define COLUMN_COUNT 3
#define ROW_HEIGHT 100
#define ROW_WIDTH ROW_HEIGHT
#define CELL_SPACING 10

@interface KCMainViewController (){
    NSMutableArray *_imageViews;
    NSMutableArray *_imageNames;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self layoutUI];
}

#pragma mark 界面布局
-(void)layoutUI{
    //建立多個圖檔控件用于顯示圖檔
    _imageViews=[NSMutableArray array];
    for (int r=0; r<ROW_COUNT; r++) {
        for (int c=0; c<COLUMN_COUNT; c++) {
            UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING                           ), ROW_WIDTH, ROW_HEIGHT)];
            imageView.contentMode=UIViewContentModeScaleAspectFit;
//            imageView.backgroundColor=[UIColor redColor];
            [self.view addSubview:imageView];
            [_imageViews addObject:imageView];

        }
    }

    UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame=CGRectMake(50, 500, 220, 25);
    [button setTitle:@"加載圖檔" forState:UIControlStateNormal];
    //添加方法
    [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
    
    //建立圖檔連結
    _imageNames=[NSMutableArray array];
    for (int i=0; i<IMAGE_COUNT; i++) {
        [_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];
    }    
}

#pragma mark 将圖檔顯示到界面
-(void)updateImageWithData:(NSData *)data andIndex:(int )index{
    UIImage *image=[UIImage imageWithData:data];
    UIImageView *imageView= _imageViews[index];
    imageView.image=image;
}

#pragma mark 請求圖檔資料
-(NSData *)requestData:(int )index{
    //對于多線程操作建議把線程操作放到@autoreleasepool中
    @autoreleasepool {
        NSURL *url=[NSURL URLWithString:_imageNames[index]];
        NSData *data=[NSData dataWithContentsOfURL:url];

        return data;
    }
}

#pragma mark 加載圖檔
-(void)loadImage:(NSNumber *)index{
    int i=[index integerValue];

    //請求資料
    NSData *data= [self requestData:i];
    NSLog(@"%@",[NSThread currentThread]);
    //更新UI界面,此處調用了主線程隊列的方法(mainQueue是UI主線程)
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        [self updateImageWithData:data andIndex:i];
    }];
}

#pragma mark 多線程下載下傳圖檔
-(void)loadImageWithMultiThread{
    int count=ROW_COUNT*COLUMN_COUNT;
    //建立操作隊列
    NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
    operationQueue.maxConcurrentOperationCount=5;//設定最大并發線程數
    //建立多個線程用于填充圖檔
    for (int i=0; i<count; ++i) {
        //方法1:建立操作塊添加到隊列
//        //建立多線程操作
//        NSBlockOperation *blockOperation=[NSBlockOperation blockOperationWithBlock:^{
//            [self loadImage:[NSNumber numberWithInt:i]];
//        }];
//        //建立操作隊列
//
//        [operationQueue addOperation:blockOperation];
        
        //方法2:直接使用操隊列添加操作
        [operationQueue addOperationWithBlock:^{
            [self loadImage:[NSNumber numberWithInt:i]];
        }];
        
    }
}
@end      

對比之前NSThread加載張圖檔很發現核心代碼簡化了不少,這裡着重強調兩點:

  1. 使用NSBlockOperation方法,所有的操作不必單獨定義方法,同時解決了隻能傳遞一個參數的問題。
  2. 調用主線程隊列的addOperationWithBlock:方法進行UI更新,不用再定義一個參數實體(之前必須定義一個KCImageData解決隻能傳遞一個參數的問題)。
  3. 使用NSOperation進行多線程開發可以設定最大并發線程,有效的對線程進行了控制(上面的代碼運作起來你會發現列印目前程序時隻有有限的線程被建立,如上面的代碼設定最大線程數為5,則圖檔基本上是五個一次加載的)。

線程執行順序

前面使用NSThread很難控制線程的執行順序,但是使用NSOperation就容易多了,每個NSOperation可以設定依賴線程。假設操作A依賴于操作B,線程操作隊列在啟動線程時就會首先執行B操作,然後執行A。對于前面優先加載最後一張圖的需求,隻要設定前面的線程操作的依賴線程為最後一個操作即可。修改圖檔加載方法如下:

-(void)loadImageWithMultiThread{
    int count=ROW_COUNT*COLUMN_COUNT;
    //建立操作隊列
    NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
    operationQueue.maxConcurrentOperationCount=5;//設定最大并發線程數
    
    NSBlockOperation *lastBlockOperation=[NSBlockOperation blockOperationWithBlock:^{
        [self loadImage:[NSNumber numberWithInt:(count-1)]];
    }];
    //建立多個線程用于填充圖檔
    for (int i=0; i<count-1; ++i) {
        //方法1:建立操作塊添加到隊列
        //建立多線程操作
        NSBlockOperation *blockOperation=[NSBlockOperation blockOperationWithBlock:^{
            [self loadImage:[NSNumber numberWithInt:i]];
        }];
        //設定依賴操作為最後一張圖檔加載操作
        [blockOperation addDependency:lastBlockOperation];
        
        [operationQueue addOperation:blockOperation];
        
    }
    //将最後一個圖檔的加載操作加入線程隊列
    [operationQueue addOperation:lastBlockOperation];
}      

運作效果:

ios開發-多線程GCD之詳解 概覽 多線程 NSThread NSOperation GCD 線程同步 總結

可以看到雖然加載最後一張圖檔的操作最後被加入到操作隊列,但是它卻是被第一個執行的。操作依賴關系可以設定多個,例如A依賴于B、B依賴于C…但是千萬不要設定為循環依賴關系(例如A依賴于B,B依賴于C,C又依賴于A),否則是不會被執行的。

GCD

GCD(Grand Central Dispatch)是基于C語言開發的一套多線程開發機制,也是目前蘋果官方推薦的多線程開發方法。前面也說過三種開發中GCD抽象層次最高,當然是用起來也最簡單,隻是它基于C語言開發,并不像NSOperation是面向對象的開發,而是完全面向過程的。對于熟悉C#異步調用的朋友對于GCD學習起來應該很快,因為它與C#中的異步調用基本是一樣的。這種機制相比較于前面兩種多線程開發方式最顯著的優點就是它對于多核運算更加有效。

GCD中也有一個類似于NSOperationQueue的隊列,GCD統一管理整個隊列中的任務。但是GCD中的隊列分為并行隊列和串行隊列兩類:

  • 串行隊列:隻有一個線程,加入到隊列中的操作按添加順序依次執行。
  • 并發隊列:有多個線程,操作進來之後它會将這些隊列安排在可用的處理器上,同時保證先進來的任務優先處理。

其實在GCD中還有一個特殊隊列就是主隊列,用來執行主線程上的操作任務(從前面的示範中可以看到其實在NSOperation中也有一個主隊列)。

串行隊列

使用串行隊列時首先要建立一個串行隊列,然後調用異步調用方法,在此方法中傳入串行隊列和線程操作即可自動執行。下面使用線程隊列示範圖檔的加載過程,你會發現多張圖檔會按順序加載,因為目前隊列中隻有一個線程。

//
//  GCD實作多線程
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"
#import "KCImageData.h"
#define ROW_COUNT 5
#define COLUMN_COUNT 3
#define ROW_HEIGHT 100
#define ROW_WIDTH ROW_HEIGHT
#define CELL_SPACING 10

@interface KCMainViewController (){
    NSMutableArray *_imageViews;
    NSMutableArray *_imageNames;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self layoutUI];
}

#pragma mark 界面布局
-(void)layoutUI{
    //建立多個圖檔控件用于顯示圖檔
    _imageViews=[NSMutableArray array];
    for (int r=0; r<ROW_COUNT; r++) {
        for (int c=0; c<COLUMN_COUNT; c++) {
            UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING                           ), ROW_WIDTH, ROW_HEIGHT)];
            imageView.contentMode=UIViewContentModeScaleAspectFit;
//            imageView.backgroundColor=[UIColor redColor];
            [self.view addSubview:imageView];
            [_imageViews addObject:imageView];

        }
    }

    UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame=CGRectMake(50, 500, 220, 25);
    [button setTitle:@"加載圖檔" forState:UIControlStateNormal];
    //添加方法
    [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
    
    //建立圖檔連結
    _imageNames=[NSMutableArray array];
    for (int i=0; i<ROW_COUNT*COLUMN_COUNT; i++) {
        [_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];
    }

}

#pragma mark 将圖檔顯示到界面
-(void)updateImageWithData:(NSData *)data andIndex:(int )index{
    UIImage *image=[UIImage imageWithData:data];
    UIImageView *imageView= _imageViews[index];
    imageView.image=image;
}

#pragma mark 請求圖檔資料
-(NSData *)requestData:(int )index{
    NSURL *url=[NSURL URLWithString:_imageNames[index]];
    NSData *data=[NSData dataWithContentsOfURL:url];

    return data;
}

#pragma mark 加載圖檔
-(void)loadImage:(NSNumber *)index{
    
    //如果在串行隊列中會發現目前線程列印變化完全一樣,因為他們在一個線程中
    NSLog(@"thread is :%@",[NSThread currentThread]);
    
    int i=[index integerValue];
    //請求資料
    NSData *data= [self requestData:i];
    //更新UI界面,此處調用了GCD主線程隊列的方法
    dispatch_queue_t mainQueue= dispatch_get_main_queue();
    dispatch_sync(mainQueue, ^{
        [self updateImageWithData:data andIndex:i];
    });
}

#pragma mark 多線程下載下傳圖檔
-(void)loadImageWithMultiThread{
    int count=ROW_COUNT*COLUMN_COUNT;
    
    /*建立一個串行隊列
     第一個參數:隊列名稱
     第二個參數:隊列類型
    */
    dispatch_queue_t serialQueue=dispatch_queue_create("myThreadQueue1", DISPATCH_QUEUE_SERIAL);//注意queue對象不是指針類型
    //建立多個線程用于填充圖檔
    for (int i=0; i<count; ++i) {
        //異步執行隊列任務
        dispatch_async(serialQueue, ^{
            [self loadImage:[NSNumber numberWithInt:i]];
        });
        
    }
    //非ARC環境請釋放
//    dispatch_release(seriQueue);
}
@end      

運作效果:

ios開發-多線程GCD之詳解 概覽 多線程 NSThread NSOperation GCD 線程同步 總結

在上面的代碼中更新UI還使用了GCD方法的主線程隊列dispatch_get_main_queue(),其實這與前面兩種主線程更新UI沒有本質的差別。

并發隊列

并發隊列同樣是使用dispatch_queue_create()方法建立,隻是最後一個參數指定為DISPATCH_QUEUE_CONCURRENT進行建立,但是在實際開發中我們通常不會重新建立一個并發隊列而是使用dispatch_get_global_queue()方法取得一個全局的并發隊列(當然如果有多個并發隊列可以使用前者建立)。下面通過并行隊列示範一下多個圖檔的加載。代碼與上面串行隊列加載類似,隻需要修改照片加載方法如下:

-(void)loadImageWithMultiThread{
    int count=ROW_COUNT*COLUMN_COUNT;
    
    /*取得全局隊列
     第一個參數:線程優先級
     第二個參數:标記參數,目前沒有用,一般傳入0
    */
    dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //建立多個線程用于填充圖檔
    for (int i=0; i<count; ++i) {
        //異步執行隊列任務
        dispatch_async(globalQueue, ^{
            [self loadImage:[NSNumber numberWithInt:i]];
        });
    }
}      

運作效果:

ios開發-多線程GCD之詳解 概覽 多線程 NSThread NSOperation GCD 線程同步 總結

細心的朋友肯定會思考,既然可以使用dispatch_async()異步調用方法,是不是還有同步方法,确實如此,在GCD中還有一個dispatch_sync()方法。假設将上面的代碼修改為同步調用,可以看到如下效果:

ios開發-多線程GCD之詳解 概覽 多線程 NSThread NSOperation GCD 線程同步 總結

可以看點選按鈕後按鈕無法再次點選,因為所有圖檔的加載全部在主線程中(可以列印線程檢視),主線程被阻塞,造成圖檔最終是一次性顯示。可以得出結論:

  • 在GDC中一個操作是多線程執行還是單線程執行取決于目前隊列類型和執行方法,隻有隊列類型為并行隊列并且使用異步方法執行時才能在多個線程中執行。
  • 串行隊列可以按順序執行,并行隊列的異步方法無法确定執行順序。
  • UI界面的更新最好采用同步方法,其他操作采用異步方法。
  • GCD中多線程操作方法不需要使用@autoreleasepool,GCD會管理記憶體。

其他任務執行方法

GCD執行任務的方法并非隻有簡單的同步調用方法和異步調用方法,還有其他一些常用方法:

  1. dispatch_apply():重複執行某個任務,但是注意這個方法沒有辦法異步執行(為了不阻塞線程可以使用dispatch_async()包裝一下再執行)。
  2. dispatch_once():單次執行一個任務,此方法中的任務隻會執行一次,重複調用也沒辦法重複執行(單例模式中常用此方法)。
  3. dispatch_time():延遲一定的時間後執行。
  4. dispatch_barrier_async():使用此方法建立的任務首先會檢視隊列中有沒有别的任務要執行,如果有,則會等待已有任務執行完畢再執行;同時在此方法後添加的任務必須等待此方法中任務執行後才能執行。(利用這個方法可以控制執行順序,例如前面先加載最後一張圖檔的需求就可以先使用這個方法将最後一張圖檔加載的操作添加到隊列,然後調用dispatch_async()添加其他圖檔加載任務)
  5. dispatch_group_async():實作對任務分組管理,如果一組任務全部完成可以通過dispatch_group_notify()方法獲得完成通知(需要定義dispatch_group_t作為分組辨別)。

線程同步

說到多線程就不得不提多線程中的鎖機制,多線程操作過程中往往多個線程是并發執行的,同一個資源可能被多個線程同時通路,造成資源搶奪,這個過程中如果沒有鎖機制往往會造成重大問題。舉例來說,每年春節都是一票難求,在12306買票的過程中,成百上千的票瞬間就消失了。不妨假設某輛車有1千張票,同時有幾萬人在搶這列車的車票,順利的話前面的人都能買到票。但是如果現在隻剩下一張票了,而同時還有幾千人在購買這張票,雖然在進入購票環節的時候會判斷目前票數,但是目前已經有100個線程進入購票的環節,每個線程處理完票數都會減1,100個線程執行完目前票數為-99,遇到這種情況很明顯是不允許的。

要解決資源搶奪問題在iOS中有常用的有兩種方法:一種是使用NSLock同步鎖,另一種是使用@synchronized代碼塊。兩種方法實作原理是類似的,隻是在處理上代碼塊使用起來更加簡單(C#中也有類似的處理機制synchronized和lock)。

這裡不妨還拿圖檔加載來舉例,假設現在有9張圖檔,但是有15個線程都準備加載這9張圖檔,約定不能重複加載同一張圖檔,這樣就形成了一個資源搶奪的情況。在下面的程式中将建立9張圖檔,每次讀取照片連結時首先判斷目前連結數是否大于1,用完一個則立即移除,最多隻有9個。在使用同步方法之前先來看一下錯誤的寫法:

//
//  線程同步
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"
#import "KCImageData.h"
#define ROW_COUNT 5
#define COLUMN_COUNT 3
#define ROW_HEIGHT 100
#define ROW_WIDTH ROW_HEIGHT
#define CELL_SPACING 10
#define IMAGE_COUNT 9

@interface KCMainViewController (){
    NSMutableArray *_imageViews;
    NSMutableArray *_imageNames;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self layoutUI];
}

#pragma mark 界面布局
-(void)layoutUI{
    //建立多個圖檔控件用于顯示圖檔
    _imageViews=[NSMutableArray array];
    for (int r=0; r<ROW_COUNT; r++) {
        for (int c=0; c<COLUMN_COUNT; c++) {
            UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING                           ), ROW_WIDTH, ROW_HEIGHT)];
            imageView.contentMode=UIViewContentModeScaleAspectFit;
//            imageView.backgroundColor=[UIColor redColor];
            [self.view addSubview:imageView];
            [_imageViews addObject:imageView];

        }
    }

    UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame=CGRectMake(50, 500, 220, 25);
    [button setTitle:@"加載圖檔" forState:UIControlStateNormal];
    //添加方法
    [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
    
    //建立圖檔連結
    _imageNames=[NSMutableArray array];
    for (int i=0; i<IMAGE_COUNT; i++) {
        [_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];
    }

}

#pragma mark 将圖檔顯示到界面
-(void)updateImageWithData:(NSData *)data andIndex:(int )index{
    UIImage *image=[UIImage imageWithData:data];
    UIImageView *imageView= _imageViews[index];
    imageView.image=image;
}

#pragma mark 請求圖檔資料
-(NSData *)requestData:(int )index{
    NSData *data;
    NSString *name;
    if (_imageNames.count>0) {
        name=[_imageNames lastObject];
        [_imageNames removeObject:name];
    }
    if(name){
        NSURL *url=[NSURL URLWithString:name];
        data=[NSData dataWithContentsOfURL:url];
    }
    return data;
}

#pragma mark 加載圖檔
-(void)loadImage:(NSNumber *)index{
    int i=[index integerValue];
    //請求資料
    NSData *data= [self requestData:i];
    //更新UI界面,此處調用了GCD主線程隊列的方法
    dispatch_queue_t mainQueue= dispatch_get_main_queue();
    dispatch_sync(mainQueue, ^{
        [self updateImageWithData:data andIndex:i];
    });
}

#pragma mark 多線程下載下傳圖檔
-(void)loadImageWithMultiThread{
    int count=ROW_COUNT*COLUMN_COUNT;

    dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //建立多個線程用于填充圖檔
    for (int i=0; i<count; ++i) {
        //異步執行隊列任務
        dispatch_async(globalQueue, ^{
            [self loadImage:[NSNumber numberWithInt:i]];
        });
    }
    
}
@end      

首先在_imageNames中存儲了9個連結用于下載下傳圖檔,然後在requestData:方法中每次隻需先判斷_imageNames的個數,如果大于一就讀取一個連結加載圖檔,随即把用過的連結删除,一切貌似都沒有問題。此時運作程式:

ios開發-多線程GCD之詳解 概覽 多線程 NSThread NSOperation GCD 線程同步 總結

上面這個結果不一定每次都出現,關鍵要看從_imageNames讀取連結、删除連結的速度,如果足夠快可能不會有任何問題,但是如果速度稍慢就會出現上面的情況,很明顯上面情況并不滿足前面的需求。

分析這個問題造成的原因主:當一個線程A已經開始擷取圖檔連結,擷取完之後還沒有來得及從_imageNames中删除,另一個線程B已經進入相應代碼中,由于每次讀取的都是_imageNames的最後一個元素,是以後面的線程其實和前面線程取得的是同一個圖檔連結這樣就造成圖中看到的情況。要解決這個問題,隻要保證線程A進入相應代碼之後B無法進入,隻有等待A完成相關操作之後B才能進入即可。下面分别使用NSLock和@synchronized對代碼進行修改。

NSLock

iOS中對于資源搶占的問題可以使用同步鎖NSLock來解決,使用時把需要加鎖的代碼(以後暫時稱這段代碼為”加鎖代碼“)放到NSLock的lock和unlock之間,一個線程A進入加鎖代碼之後由于已經加鎖,另一個線程B就無法通路,隻有等待前一個線程A執行完加鎖代碼後解鎖,B線程才能通路加鎖代碼。需要注意的是lock和unlock之間的”加鎖代碼“應該是搶占資源的讀取和修改代碼,不要将過多的其他操作代碼放到裡面,否則一個線程執行的時候另一個線程就一直在等待,就無法發揮多線程的作用了。

另外,在上面的代碼中”搶占資源“_imageNames定義成了成員變量,這麼做是不明智的,應該定義為“原子屬性”。對于被搶占資源來說将其定義為原子屬性是一個很好的習慣,因為有時候很難保證同一個資源不在别處讀取和修改。nonatomic屬性讀取的是記憶體資料(寄存器計算好的結果),而atomic就保證直接讀取寄存器的資料,這樣一來就不會出現一個線程正在修改資料,而另一個線程讀取了修改之前(存儲在記憶體中)的資料,永遠保證同時隻有一個線程在通路一個屬性。

下面的代碼示範了如何使用NSLock進行線程同步:

KCMainViewController.h

//
//  KCMainViewController.h
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface KCMainViewController : UIViewController

@property (atomic,strong) NSMutableArray *imageNames;
@end      

KCMainViewController.m

//
//  線程同步
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"
#import "KCImageData.h"
#define ROW_COUNT 5
#define COLUMN_COUNT 3
#define ROW_HEIGHT 100
#define ROW_WIDTH ROW_HEIGHT
#define CELL_SPACING 10
#define IMAGE_COUNT 9

@interface KCMainViewController (){
    NSMutableArray *_imageViews;
    NSLock *_lock;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self layoutUI];
}

#pragma mark 界面布局
-(void)layoutUI{
    //建立多個圖檔控件用于顯示圖檔
    _imageViews=[NSMutableArray array];
    for (int r=0; r<ROW_COUNT; r++) {
        for (int c=0; c<COLUMN_COUNT; c++) {
            UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING                           ), ROW_WIDTH, ROW_HEIGHT)];
            imageView.contentMode=UIViewContentModeScaleAspectFit;
            [self.view addSubview:imageView];
            [_imageViews addObject:imageView];

        }
    }

    UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame=CGRectMake(50, 500, 220, 25);
    [button setTitle:@"加載圖檔" forState:UIControlStateNormal];
    //添加方法
    [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
    
    //建立圖檔連結
    _imageNames=[NSMutableArray array];
    for (int i=0; i<IMAGE_COUNT; i++) {
        [_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];
    }
    
    //初始化鎖對象
    _lock=[[NSLock alloc]init];

}

#pragma mark 将圖檔顯示到界面
-(void)updateImageWithData:(NSData *)data andIndex:(int )index{
    UIImage *image=[UIImage imageWithData:data];
    UIImageView *imageView= _imageViews[index];
    imageView.image=image;
}

#pragma mark 請求圖檔資料
-(NSData *)requestData:(int )index{
    NSData *data;
    NSString *name;
    //加鎖
    [_lock lock];
    if (_imageNames.count>0) {
        name=[_imageNames lastObject];
        [_imageNames removeObject:name];
    }
    //使用完解鎖
    [_lock unlock];
    if(name){
        NSURL *url=[NSURL URLWithString:name];
        data=[NSData dataWithContentsOfURL:url];
    }
    return data;
}

#pragma mark 加載圖檔
-(void)loadImage:(NSNumber *)index{
    int i=[index integerValue];
    //請求資料
    NSData *data= [self requestData:i];
    //更新UI界面,此處調用了GCD主線程隊列的方法
    dispatch_queue_t mainQueue= dispatch_get_main_queue();
    dispatch_sync(mainQueue, ^{
        [self updateImageWithData:data andIndex:i];
    });
}

#pragma mark 多線程下載下傳圖檔
-(void)loadImageWithMultiThread{
    int count=ROW_COUNT*COLUMN_COUNT;

    dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //建立多個線程用于填充圖檔
    for (int i=0; i<count; ++i) {
        //異步執行隊列任務
        dispatch_async(globalQueue, ^{
            [self loadImage:[NSNumber numberWithInt:i]];
        });
    }
    
}
@end      

運作效果:

ios開發-多線程GCD之詳解 概覽 多線程 NSThread NSOperation GCD 線程同步 總結

前面也說過使用同步鎖時如果一個線程A已經加鎖,線程B就無法進入。那麼B怎麼知道是否資源已經被其他線程鎖住呢?可以通過tryLock方法,此方法會傳回一個BOOL型的值,如果為YES說明擷取鎖成功,否則失敗。另外還有一個lockBeforeData:方法指定在某個時間内擷取鎖,同樣傳回一個BOOL值,如果在這個時間内加鎖成功則傳回YES,失敗則傳回NO。

@synchronized代碼塊

使用@synchronized解決線程同步問題相比較NSLock要簡單一些,日常開發中也更推薦使用此方法。首先選擇一個對象作為同步對象(一般使用self),然後将”加鎖代碼”(争奪資源的讀取、修改代碼)放到代碼塊中。@synchronized中的代碼執行時先檢查同步對象是否被另一個線程占用,如果占用該線程就會處于等待狀态,直到同步對象被釋放。下面的代碼示範了如何使用@synchronized進行線程同步:

-(NSData *)requestData:(int )index{
    NSData *data;
    NSString *name;
    //線程同步
    @synchronized(self){
        if (_imageNames.count>0) {
            name=[_imageNames lastObject];
            [NSThread sleepForTimeInterval:0.001f];
            [_imageNames removeObject:name];
        }
    }
    if(name){
        NSURL *url=[NSURL URLWithString:name];
        data=[NSData dataWithContentsOfURL:url];
    }
    return data;
}      

擴充--使用GCD解決資源搶占問題

在GCD中提供了一種信号機制,也可以解決資源搶占問題(和同步鎖的機制并不一樣)。GCD中信号量是dispatch_semaphore_t類型,支援信号通知和信号等待。每當發送一個信号通知,則信号量+1;每當發送一個等待信号時信号量-1,;如果信号量為0則信号會處于等待狀态,直到信号量大于0開始執行。根據這個原理我們可以初始化一個信号量變量,預設信号量設定為1,每當有線程進入“加鎖代碼”之後就調用信号等待指令(此時信号量為0)開始等待,此時其他線程無法進入,執行完後發送信号通知(此時信号量為1),其他線程開始進入執行,如此一來就達到了線程同步目的。

//
//  GCD實作多線程--消息信号
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"
#import "KCImageData.h"
#define ROW_COUNT 5
#define COLUMN_COUNT 3
#define ROW_HEIGHT 100
#define ROW_WIDTH ROW_HEIGHT
#define CELL_SPACING 10
#define IMAGE_COUNT 9

@interface KCMainViewController (){
    NSMutableArray *_imageViews;
    NSLock *_lock;
    dispatch_semaphore_t _semaphore;//定義一個信号量
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self layoutUI];
}

#pragma mark 界面布局
-(void)layoutUI{
    //建立多個圖檔控件用于顯示圖檔
    _imageViews=[NSMutableArray array];
    for (int r=0; r<ROW_COUNT; r++) {
        for (int c=0; c<COLUMN_COUNT; c++) {
            UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING                           ), ROW_WIDTH, ROW_HEIGHT)];
            imageView.contentMode=UIViewContentModeScaleAspectFit;
            [self.view addSubview:imageView];
            [_imageViews addObject:imageView];
            
        }
    }
    
    UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame=CGRectMake(50, 500, 220, 25);
    [button setTitle:@"加載圖檔" forState:UIControlStateNormal];
    //添加方法
    [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
    
    //建立圖檔連結
    _imageNames=[NSMutableArray array];
    for (int i=0; i<IMAGE_COUNT; i++) {
        [_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];
    }
    
    /*初始化信号量
     參數是信号量初始值
     */
    _semaphore=dispatch_semaphore_create(1);
    
}

#pragma mark 将圖檔顯示到界面
-(void)updateImageWithData:(NSData *)data andIndex:(int )index{
    UIImage *image=[UIImage imageWithData:data];
    UIImageView *imageView= _imageViews[index];
    imageView.image=image;
}

#pragma mark 請求圖檔資料
-(NSData *)requestData:(int )index{
    NSData *data;
    NSString *name;
    
    /*信号等待
     第二個參數:等待時間
     */
    dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
    if (_imageNames.count>0) {
        name=[_imageNames lastObject];
        [_imageNames removeObject:name];
    }
    //信号通知
    dispatch_semaphore_signal(_semaphore);

    
    if(name){
        NSURL *url=[NSURL URLWithString:name];
        data=[NSData dataWithContentsOfURL:url];
    }
    
    return data;
}

#pragma mark 加載圖檔
-(void)loadImage:(NSNumber *)index{
    int i=[index integerValue];
    //請求資料
    NSData *data= [self requestData:i];
    //更新UI界面,此處調用了GCD主線程隊列的方法
    dispatch_queue_t mainQueue= dispatch_get_main_queue();
    dispatch_sync(mainQueue, ^{
        [self updateImageWithData:data andIndex:i];
    });
}

#pragma mark 多線程下載下傳圖檔
-(void)loadImageWithMultiThread{
    int count=ROW_COUNT*COLUMN_COUNT;
//    dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    //這裡建立一個并發隊列(使用全局并發隊列也可以)
    dispatch_queue_t queue=dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i=0; i<count; i++) {
        dispatch_async(queue, ^{
            [self loadImage:[NSNumber numberWithInt:i]];
        });
    }
}

@end      

運作效果與前面使用同步鎖是一樣的。

擴充--控制線程通信

由于線程的排程是透明的,程式有時候很難對它進行有效的控制,為了解決這個問題iOS提供了NSCondition來控制線程通信(同前面GCD的信号機制類似)。NSCondition實作了NSLocking協定,是以它本身也有lock和unlock方法,是以也可以将它作為NSLock解決線程同步問題,此時使用方法跟NSLock沒有差別,隻要線上程開始時加鎖,取得資源後釋放鎖即可,這部分内容比較簡單在此不再示範。當然,單純解決線程同步問題不是NSCondition設計的主要目的,NSCondition更重要的是解決線程之間的排程關系(當然,這個過程中也必須先加鎖、解鎖)。NSCondition可以調用wati方法控制某個線程處于等待狀态,直到其他線程調用signal(此方法喚醒一個線程,如果有多個線程在等待則任意喚醒一個)或者broadcast(此方法會喚醒所有等待線程)方法喚醒該線程才能繼續。

假設目前imageNames沒有任何圖檔,而整個界面能夠加載15張圖檔(每張都不能重複),現在建立15個線程分别從imageNames中取圖檔加載到界面中。由于imageNames中沒有任何圖檔,那麼15個線程都處于等待狀态,隻有當調用圖檔建立方法往imageNames中添加圖檔後(每次建立一個)并且喚醒其他線程(這裡隻喚醒一個線程)才能繼續執行加載圖檔。如此,每次建立一個圖檔就會喚醒一個線程去加載,這個過程其實就是一個典型的生産者-消費者模式。下面通過NSCondition實作這個流程的控制:

KCMainViewController.h

//
//  KCMainViewController.h
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface KCMainViewController : UIViewController

#pragma mark 圖檔資源存儲容器
@property (atomic,strong) NSMutableArray *imageNames;

#pragma mark 目前加載的圖檔索引(圖檔連結位址連續)
@property (atomic,assign) int currentIndex;

@end      

KCMainViewController.m

//
//  線程控制
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"
#import "KCImageData.h"
#define ROW_COUNT 5
#define COLUMN_COUNT 3
#define ROW_HEIGHT 100
#define ROW_WIDTH ROW_HEIGHT
#define CELL_SPACING 10
#define IMAGE_COUNT 9

@interface KCMainViewController (){
    NSMutableArray *_imageViews;
    NSCondition *_condition;
}

@end

@implementation KCMainViewController

#pragma mark - 事件
- (void)viewDidLoad {
    [super viewDidLoad];

    [self layoutUI];
}

#pragma mark - 内部私有方法
#pragma mark 界面布局
-(void)layoutUI{
    //建立多個圖檔控件用于顯示圖檔
    _imageViews=[NSMutableArray array];
    for (int r=0; r<ROW_COUNT; r++) {
        for (int c=0; c<COLUMN_COUNT; c++) {
            UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING                           ), ROW_WIDTH, ROW_HEIGHT)];
            imageView.contentMode=UIViewContentModeScaleAspectFit;
            [self.view addSubview:imageView];
            [_imageViews addObject:imageView];
            
        }
    }
    
    UIButton *btnLoad=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    btnLoad.frame=CGRectMake(50, 500, 100, 25);
    [btnLoad setTitle:@"加載圖檔" forState:UIControlStateNormal];
    [btnLoad addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btnLoad];
    
    UIButton *btnCreate=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    btnCreate.frame=CGRectMake(160, 500, 100, 25);
    [btnCreate setTitle:@"建立圖檔" forState:UIControlStateNormal];
    [btnCreate addTarget:self action:@selector(createImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btnCreate];
    
    //建立圖檔連結
    _imageNames=[NSMutableArray array];
    
    //初始化鎖對象
    _condition=[[NSCondition alloc]init];
    
    _currentIndex=0;
    
}


#pragma mark 建立圖檔
-(void)createImageName{
    [_condition lock];
    //如果目前已經有圖檔了則不再建立,線程處于等待狀态
    if (_imageNames.count>0) {
        NSLog(@"createImageName wait, current:%i",_currentIndex);
        [_condition wait];
    }else{
        NSLog(@"createImageName work, current:%i",_currentIndex);
        //生産者,每次生産1張圖檔
        [_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",_currentIndex++]];
        
        //建立完圖檔則發出信号喚醒其他等待線程
        [_condition signal];
    }
    [_condition unlock];
}

#pragma mark 加載圖檔并将圖檔顯示到界面
-(void)loadAnUpdateImageWithIndex:(int )index{
    //請求資料
    NSData *data= [self requestData:index];
    //更新UI界面,此處調用了GCD主線程隊列的方法
    dispatch_queue_t mainQueue= dispatch_get_main_queue();
    dispatch_sync(mainQueue, ^{
        UIImage *image=[UIImage imageWithData:data];
        UIImageView *imageView= _imageViews[index];
        imageView.image=image;
    });
}

#pragma mark 請求圖檔資料
-(NSData *)requestData:(int )index{
    NSData *data;
    NSString *name;
    name=[_imageNames lastObject];
    [_imageNames removeObject:name];
    if(name){
        NSURL *url=[NSURL URLWithString:name];
        data=[NSData dataWithContentsOfURL:url];
    }
    return data;
}

#pragma mark 加載圖檔
-(void)loadImage:(NSNumber *)index{
    int i=(int)[index integerValue];
    //加鎖
    [_condition lock];
    //如果目前有圖檔資源則加載,否則等待
    if (_imageNames.count>0) {
        NSLog(@"loadImage work,index is %i",i);
        [self loadAnUpdateImageWithIndex:i];
        [_condition broadcast];
    }else{
        NSLog(@"loadImage wait,index is %i",i);
        NSLog(@"%@",[NSThread currentThread]);
        //線程等待
        [_condition wait];
        NSLog(@"loadImage resore,index is %i",i);
        //一旦建立完圖檔立即加載
        [self loadAnUpdateImageWithIndex:i];
    }
    //解鎖
    [_condition unlock];
}


#pragma mark - UI調用方法
#pragma mark 異步建立一張圖檔連結
-(void)createImageWithMultiThread{
    dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //建立圖檔連結
    dispatch_async(globalQueue, ^{
        [self createImageName];
    });
}

#pragma mark 多線程下載下傳圖檔
-(void)loadImageWithMultiThread{
    int count=ROW_COUNT*COLUMN_COUNT;
    dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    for (int i=0; i<count; ++i) {
        //加載圖檔
        dispatch_async(globalQueue, ^{
            [self loadImage:[NSNumber numberWithInt:i]];
        });
    }
}
@end      

運作效果:

ios開發-多線程GCD之詳解 概覽 多線程 NSThread NSOperation GCD 線程同步 總結

在上面的代碼中loadImage:方法是消費者,當在界面中點選“加載圖檔”後就建立了15個消費者線程。在這個過程中每個線程進入圖檔加載方法之後都會先加鎖,加鎖之後其他程序是無法進入“加鎖代碼”的。但是第一個線程進入“加鎖代碼”後去加載圖檔卻發現目前并沒有任何圖檔,是以它隻能等待。一旦調用了NSCondition的wait方法後其他線程就可以繼續進入“加鎖代碼”(注意,這一點和前面說的NSLock、@synchronized等是不同的,使用NSLock、@synchronized等進行加鎖後無論什麼情況下,隻要沒有解鎖其他線程就無法進入“加鎖代碼”),同時第一個線程處于等待隊列中(此時并未解鎖)。第二個線程進來之後同第一線程一樣,發現沒有圖檔就進入等待狀态,然後第三個線程進入。。。如此反複,直到第十五個線程也處于等待。此時點選“建立圖檔”後會執行createImageName方法,這是一個生産者,它會建立一個圖檔連結放到imageNames中,然後通過調用NSCondition的signal方法就會在條件等待隊列中選擇一個線程(該線程會任意選取,假設為線程A)開啟,那麼此時這個線程就會繼續執行。在上面代碼中,wati方法之後會繼續執行圖檔加載方法,那麼此時線程A啟動之後繼續執行圖檔加載方法,當然此時可以成功加載圖檔。加載完圖檔之後線程A就會釋放鎖,整個線程任務完成。此時再次點選”建立圖檔“按鈕重複前面的步驟加載其他圖檔。

為了說明上面的過程,這裡以一個流程圖的進行說明,流程圖藍色部分代表15個加載圖檔的線程,綠色部分表示建立圖檔資源線程。

ios開發-多線程GCD之詳解 概覽 多線程 NSThread NSOperation GCD 線程同步 總結

iOS中的其他鎖

在iOS開發中,除了同步鎖有時候還會用到一些其他鎖類型,在此簡單介紹一下:

NSRecursiveLock :遞歸鎖,有時候“加鎖代碼”中存在遞歸調用,遞歸開始前加鎖,遞歸調用開始後會重複執行此方法以至于反複執行加鎖代碼最終造成死鎖,這個時候可以使用遞歸鎖來解決。使用遞歸鎖可以在一個線程中反複擷取鎖而不造成死鎖,這個過程中會記錄擷取鎖和釋放鎖的次數,隻有最後兩者平衡鎖才被最終釋放。

NSDistributedLock:分布鎖,它本身是一個互斥鎖,基于檔案方式實作鎖機制,可以跨程序通路。

pthread_mutex_t:同步鎖,基于C語言的同步鎖機制,使用方法與其他同步鎖機制類似。

提示:在開發過程中除非必須用鎖,否則應該盡可能不使用鎖,因為多線程開發本身就是為了提高程式執行順序,而同步鎖本身就隻能一個程序執行,這樣不免降低執行效率。

總結

1>無論使用哪種方法進行多線程開發,每個線程啟動後并不一定立即執行相應的操作,具體什麼時候由系統排程(CPU空閑時就會執行)。

2>更新UI應該在主線程(UI線程)中進行,并且推薦使用同步調用,常用的方法如下:

  • - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait (或者-(void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL) wait;方法傳遞主線程[NSThread mainThread])
  • [NSOperationQueue mainQueue] addOperationWithBlock:
  • dispatch_sync(dispatch_get_main_queue(), ^{})

3>NSThread适合輕量級多線程開發,控制線程順序比較難,同時線程總數無法控制(每次建立并不能重用之前的線程,隻能建立一個新的線程)。

4>對于簡單的多線程開發建議使用NSObject的擴充方法完成,而不必使用NSThread。

5>可以使用NSThread的currentThread方法取得目前線程,使用 sleepForTimeInterval:方法讓目前線程休眠。

6>NSOperation進行多線程開發可以控制線程總數及線程依賴關系。

7>建立一個NSOperation不應該直接調用start方法(如果直接start則會在主線程中調用)而是應該放到NSOperationQueue中啟動。

8>相比NSInvocationOperation推薦使用NSBlockOperation,代碼簡單,同時由于閉包性使它沒有傳參問題。

9>NSOperation是對GCD面向對象的ObjC封裝,但是相比GCD基于C語言開發,效率卻更高,建議如果任務之間有依賴關系或者想要監聽任務完成狀态的情況下優先選擇NSOperation否則使用GCD。

10>在GCD中串行隊列中的任務被安排到一個單一線程執行(不是主線程),可以友善地控制執行順序;并發隊列在多個線程中執行(前提是使用異步方法),順序控制相對複雜,但是更高效。

11>在GDC中一個操作是多線程執行還是單線程執行取決于目前隊列類型和執行方法,隻有隊列類型為并行隊列并且使用異步方法執行時才能在多個線程中執行(如果是并行隊列使用同步方法調用則會在主線程中執行)。

12>相比使用NSLock,@synchronized更加簡單,推薦使用後者。

繼續閱讀