1. 程式人生 > >iOS開發系列--並行開發(處理多個網路請求併發的情況)

iOS開發系列--並行開發(處理多個網路請求併發的情況)

概覽

大家都知道,在開發過程中應該儘可能減少使用者等待時間,讓程式儘可能快的完成運算。可是無論是哪種語言開發的程式最終往往轉換成組合語言進而解釋成機器碼來執行。但是機器碼是按順序執行的,一個複雜的多步操作只能一步步按順序逐個執行。改變這種狀況可以從兩個角度出發:對於單核處理器,可以將多個步驟放到不同的執行緒,這樣一來使用者完成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. 總結

多執行緒

簡介

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

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

MultiThread

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

執行效果:

NSThreadEffect

程式比較簡單,但是需要注意執行步驟:當點選了“載入圖片”按鈕後啟動一個新的執行緒,這個執行緒在演示中大概用了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{
    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

NSThreadEffect2

通過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{
    //對非最後一張圖片載入執行緒休眠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];
    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{
    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秒後點擊停止載入):

 NSThreadEffect3

使用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{
    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];
}

執行效果:

NSOperationEffect

可以看到雖然載入最後一張圖片的操作最後被加入到操作佇列,但是它卻是被第一個執行的。操作依賴關係可以設定多個,例如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

執行效果:

GCDEffect1

在上面的程式碼中更新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]];
        });
    }
}

執行效果:

GCDEffect2

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

GCDEffect3

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

  • 在GDC中一個操作是多執行緒執行還是單執行緒執行取決於當前佇列型別和執行方法,只有佇列型別為並行佇列並且使用非同步方法執行時才能在多個執行緒中執行。
  • 序列佇列可以按順序執行,並行佇列的非同步方法無法確定執行順序。
  • UI介面的更新最好採用同步方法,其他操作採用非同步方法。

其他任務執行方法

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的個數,如果大於一就讀取一個連結載入圖片,隨即把用過的連結刪除,一切貌似都沒有問題。此時執行程式:

LockEffect1

上面這個結果不一定每次都出現,關鍵要看從_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

執行效果:

LockEffect2

前面也說過使用同步鎖時如果一個執行緒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];
    
    UIBut