在開發過程當中應該儘量減小用戶等待時間,讓程序儘量快的完成運算。但是不管是哪一種語言開發的程序最終每每轉換成彙編語言進而解釋成機器碼來執行。可是機器碼是按順序執行的,一個複雜的多步操做只能一步步按順序逐個執行。改變這種情況能夠從兩個角度出發:對於單核處理器,能夠將多個步驟放到不一樣的線程,這樣一來用戶完成UI操做後其餘後續任務在其餘線程中,當CPU空閒時會繼續執行,而此時對於用戶而言能夠繼續進行其餘操做;對於多核處理器,若是用戶在UI線程中完成某個操做以後,其餘後續操做在別的線程中繼續執行,用戶一樣能夠繼續進行其餘UI操做,與此同時前一個操做的後續任務能夠分散到多個空閒CPU中繼續執行(固然具體調度順序要根據程序設計而定),及解決了線程阻塞又提升了運行效率。ios
當用戶播放音頻、下載資源、進行圖像處理時每每但願作這些事情的時候其餘操做不會被中斷或者但願這些操做過程當中更加順暢。在單線程中一個線程只能作一件事情,一件事情處理不完另外一件事就不能開始,這樣勢必影響用戶體驗。早在單核處理器時期就有多線程,這個時候多線程更多的用於解決線程阻塞形成的用戶等待(一般是操做完UI後用戶再也不干涉,其餘線程在等待隊列中,CPU一旦空閒就繼續執行,不影響用戶其餘UI操做),其處理能力並無明顯的變化。現在不管是移動操做系統仍是PC、服務器都是多核處理器,因而「並行運算」就更多的被說起。一件事情咱們能夠分紅多個步驟,在沒有順序要求的狀況下使用多線程既能解決線程阻塞又能充分利用多核處理器運行能力。c++
下圖反映了一個包含8個操做的任務在一個有兩核心的CPU中建立四個線程運行的狀況。假設每一個核心有兩個線程,那麼每一個CPU中兩個線程會交替執行,兩個CPU之間的操做會並行運算。單就一個CPU而言兩個線程能夠解決線程阻塞形成的不流暢問題,其自己運行效率並無提升,多CPU的並行運算才真正解決了運行效率問題,這也正是併發和並行的區別。固然,無論是多核仍是單核開發人員不用過多的擔憂,由於任務具體分配給幾個CPU運算是由系統調度的,開發人員不用過多關心繫統有幾個CPU。開發人員須要關心的是線程之間的依賴關係,由於有些操做必須在某個操做完成完才能執行,若是不能保證這個順序勢必會形成程序問題。服務器
在iOS中每一個進程啓動後都會創建一個主線程(UI線程),這個線程是其餘線程的父線程。因爲在iOS中除了主線程,其餘子線程是獨立於Cocoa Touch的,因此只有主線程能夠更新UI界面(新版iOS中,使用其餘線程更新UI可能也能成功,可是不推薦)。iOS中多線程使用並不複雜,關鍵是如何控制好各個線程的執行順序、處理好資源競爭問題。經常使用的多線程開發有三種方式:網絡
1.NSThread 多線程
2.NSOperation 閉包
3.GCD併發
三種方式是隨着iOS的發展逐漸引入的,因此相比而言後者比前者更加簡單易用,而且GCD也是目前蘋果官方比較推薦的方式(它充分利用了多核處理器的運算性能)。作過.Net開發的朋友不難發現其實這三種開發方式 恰好對應.Net中的多線程、線程池和異步調用,所以在文章中也會對比講解。app
NSThread是輕量級的多線程開發,使用起來也並不複雜,可是使用NSThread須要本身管理線程生命週期。可使用對象方法+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument直接將操做添加到線程中並啓動,也可使用對象方法- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 建立一個線程對象,而後調用start方法啓動線程。iphone
在資源下載過程當中,因爲網絡緣由有時候很難保證下載時間,若是不使用多線程可能用戶完成一個下載操做須要長時間的等待,這個過程當中沒法進行其餘操做。下面演示一個採用多線程下載圖片的過程,在這個示例中點擊按鈕會啓動一個線程去下載圖片,下載完成後使用UIImageView將圖片顯示到界面中。能夠看到用戶點擊完下載按鈕後,無論圖片是否下載完成均可以繼續操做界面,不會形成阻塞。異步
// // NSThread實現多線程 // MultiThread // // Created by mxi on 16-3-22. // Copyright (c) 2014年 mxi 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
運行效果:
程序比較簡單,可是須要注意執行步驟:當點擊了「加載圖片」按鈕後啓動一個新的線程,這個線程在演示中大概用了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 mxi on 16-3-22. // Copyright (c) 2016年 mxj. 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 mxi on 16-6-22. // Copyright (c) 2016年 mxj. 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
經過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 mxi on 16-3-22. // Copyright (c) 2016年 mxj. 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{ 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秒後點擊中止加載):
使用NSThread在進行多線程開發過程當中操做比較簡單,可是要控制線程執行順序並不容易(前面萬不得已採用了休眠的方法),另外在這個過程當中若是打印線程會發現循環幾回就建立了幾個線程,這在實際開發過程當中是不得不考慮的問題,由於每一個線程的建立也是至關佔用系統開銷的。
爲了簡化多線程開發過程,蘋果官方對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和NSOperationQueue進行多線程開發相似於C#中的線程池,只要將一個NSOperation(實際開中須要使用其子類NSInvocationOperation、NSBlockOperation)放到NSOperationQueue這個隊列中線程就會依次啓動。NSOperationQueue負責管理、執行全部的NSOperation,在這個過程當中能夠更加容易的管理線程總數和控制線程之間的依賴關係。
NSOperation有兩個經常使用子類用於建立線程操做:NSInvocationOperation和NSBlockOperation,兩種方式本質沒有區別,可是是後者使用Block形式進行代碼組織,使用相對方便。
首先使用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建立多個線程加載圖片。
// // NSOperation實現多線程 // MultiThread // // Created by mxi on 16-3-22. // Copyright (c) 2016年 mxj. 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加載張圖片很發現核心代碼簡化了很多,這裏着重強調兩點:
前面使用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]; }
運行效果:
能夠看到雖然加載最後一張圖片的操做最後被加入到操做隊列,可是它倒是被第一個執行的。操做依賴關係能夠設置多個,例如A依賴於B、B依賴於C…可是千萬不要設置爲循環依賴關係(例如A依賴於B,B依賴於C,C又依賴於A),不然是不會被執行的。
GCD(Grand Central Dispatch)是基於C語言開發的一套多線程開發機制,也是目前蘋果官方推薦的多線程開發方法。前面也說過三種開發中GCD抽象層次最高,固然是用起來也最簡單,只是它基於C語言開發,並不像NSOperation是面向對象的開發,而是徹底面向過程的。對於熟悉C#異步調用的朋友對於GCD學習起來應該很快,由於它與C#中的異步調用基本是同樣的。這種機制相比較於前面兩種多線程開發方式最顯著的優勢就是它對於多核運算更加有效。
GCD中也有一個相似於NSOperationQueue的隊列,GCD統一管理整個隊列中的任務。可是GCD中的隊列分爲並行隊列和串行隊列兩類:
其實在GCD中還有一個特殊隊列就是主隊列,用來執行主線程上的操做任務(從前面的演示中能夠看到其實在NSOperation中也有一個主隊列)。
使用串行隊列時首先要建立一個串行隊列,而後調用異步調用方法,在此方法中傳入串行隊列和線程操做便可自動執行。下面使用線程隊列演示圖片的加載過程,你會發現多張圖片會按順序加載,由於當前隊列中只有一個線程。
// // GCD實現多線程 // MultiThread // // Created by mxi on 16-3-22. // Copyright (c) 2016年 mxj. 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
運行效果:
在上面的代碼中更新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]]; }); } }
運行效果:
細心的朋友確定會思考,既然可使用dispatch_async()異步調用方法,是否是還有同步方法,確實如此,在GCD中還有一個dispatch_sync()方法。假設將上面的代碼修改成同步調用,能夠看到以下效果:
能夠看點擊按鈕後按鈕沒法再次點擊,由於全部圖片的加載所有在主線程中(能夠打印線程查看),主線程被阻塞,形成圖片最終是一次性顯示。能夠得出結論:
GCD執行任務的方法並不是只有簡單的同步調用方法和異步調用方法,還有其餘一些經常使用方法:
說到多線程就不得不提多線程中的鎖機制,多線程操做過程當中每每多個線程是併發執行的,同一個資源可能被多個線程同時訪問,形成資源搶奪,這個過程當中若是沒有鎖機制每每會形成重大問題。舉例來講,每一年春節都是一票難求,在12306買票的過程當中,成百上千的票瞬間就消失了。不妨假設某輛車有1千張票,同時有幾萬人在搶這列車的車票,順利的話前面的人都能買到票。可是若是如今只剩下一張票了,而同時還有幾千人在購買這張票,雖然在進入購票環節的時候會判斷當前票數,可是當前已經有100個線程進入購票的環節,每一個線程處理完票數都會減1,100個線程執行完當前票數爲-99,遇到這種狀況很明顯是不容許的。
要解決資源搶奪問題在iOS中有經常使用的有兩種方法:一種是使用NSLock同步鎖,另外一種是使用@synchronized代碼塊。兩種方法實現原理是相似的,只是在處理上代碼塊使用起來更加簡單(C#中也有相似的處理機制synchronized和lock)。
這裏不妨還拿圖片加載來舉例,假設如今有9張圖片,可是有15個線程都準備加載這9張圖片,約定不能重複加載同一張圖片,這樣就造成了一個資源搶奪的狀況。在下面的程序中將建立9張圖片,每次讀取照片連接時首先判斷當前連接數是否大於1,用完一個則當即移除,最多隻有9個。在使用同步方法以前先來看一下錯誤的寫法:
// // 線程同步 // MultiThread // // Created by mxi on 16-3-22. // Copyright (c) 2016年 mxj. 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的個數,若是大於一就讀取一個連接加載圖片,隨即把用過的連接刪除,一切貌似都沒有問題。此時運行程序:
上面這個結果不必定每次都出現,關鍵要看從_imageNames讀取連接、刪除連接的速度,若是足夠快可能不會有任何問題,可是若是速度稍慢就會出現上面的狀況,很明顯上面狀況並不知足前面的需求。
分析這個問題形成的緣由主:當一個線程A已經開始獲取圖片連接,獲取完以後尚未來得及從_imageNames中刪除,另外一個線程B已經進入相應代碼中,因爲每次讀取的都是_imageNames的最後一個元素,所以後面的線程其實和前面線程取得的是同一個圖片連接這樣就形成圖中看到的狀況。要解決這個問題,只要保證線程A進入相應代碼以後B沒法進入,只有等待A完成相關操做以後B才能進入便可。下面分別使用NSLock和@synchronized對代碼進行修改。
iOS中對於資源搶佔的問題可使用同步鎖NSLock來解決,使用時把須要加鎖的代碼(之後暫時稱這段代碼爲」加鎖代碼「)放到NSLock的lock和unlock之間,一個線程A進入加鎖代碼以後因爲已經加鎖,另外一個線程B就沒法訪問,只有等待前一個線程A執行完加鎖代碼後解鎖,B線程才能訪問加鎖代碼。須要注意的是lock和unlock之間的」加鎖代碼「應該是搶佔資源的讀取和修改代碼,不要將過多的其餘操做代碼放到裏面,不然一個線程執行的時候另外一個線程就一直在等待,就沒法發揮多線程的做用了。
另外,在上面的代碼中」搶佔資源「_imageNames定義成了成員變量,這麼作是不明智的,應該定義爲「原子屬性」。對於被搶佔資源來講將其定義爲原子屬性是一個很好的習慣,由於有時候很難保證同一個資源不在別處讀取和修改。nonatomic屬性讀取的是內存數據(寄存器計算好的結果),而atomic就保證直接讀取寄存器的數據,這樣一來就不會出現一個線程正在修改數據,而另外一個線程讀取了修改以前(存儲在內存中)的數據,永遠保證同時只有一個線程在訪問一個屬性。
下面的代碼演示瞭如何使用NSLock進行線程同步:
KCMainViewController.h
// // KCMainViewController.h // MultiThread // // Created by mxi on 16-3-22. // Copyright (c) 2016年 mxj. All rights reserved. // #import <UIKit/UIKit.h> @interface KCMainViewController : UIViewController @property (atomic,strong) NSMutableArray *imageNames; @end
KCMainViewController.m
// // 線程同步 // MultiThread // // Created by mxj on 16-3-22. // Copyright (c) 2016年 mxj. 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
運行效果:
前面也說過使用同步鎖時若是一個線程A已經加鎖,線程B就沒法進入。那麼B怎麼知道是否資源已經被其餘線程鎖住呢?能夠經過tryLock方法,此方法會返回一個BOOL型的值,若是爲YES說明獲取鎖成功,不然失敗。另外還有一個lockBeforeData:方法指定在某個時間內獲取鎖,一樣返回一個BOOL值,若是在這個時間內加鎖成功則返回YES,失敗則返回NO。
使用@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中信號量是dispatch_semaphore_t類型,支持信號通知和信號等待。每當發送一個信號通知,則信號量+1;每當發送一個等待信號時信號量-1,;若是信號量爲0則信號會處於等待狀態,直到信號量大於0開始執行。根據這個原理咱們能夠初始化一個信號量變量,默認信號量設置爲1,每當有線程進入「加鎖代碼」以後就調用信號等待命令(此時信號量爲0)開始等待,此時其餘線程沒法進入,執行完後發送信號通知(此時信號量爲1),其餘線程開始進入執行,如此一來就達到了線程同步目的。
// // GCD實現多線程--消息信號 // MultiThread // // Created by mxi on 16-3-22. // Copyright (c) 2016年 mxj. 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 mxi on 16-3-22. // Copyright (c) 2016年 mxj. 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 mxi on 16-3-22. // Copyright (c) 2016年 mxj. 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
運行效果:
在上面的代碼中loadImage:方法是消費者,當在界面中點擊「加載圖片」後就建立了15個消費者線程。在這個過程當中每一個線程進入圖片加載方法以後都會先加鎖,加鎖以後其餘進程是沒法進入「加鎖代碼」的。可是第一個線程進入「加鎖代碼」後去加載圖片卻發現當前並無任何圖片,所以它只能等待。一旦調用了NSCondition的wait方法後其餘線程就能夠繼續進入「加鎖代碼」(注意,這一點和前面說的NSLock、@synchronized等是不一樣的,使用NSLock、@synchronized等進行加鎖後不管什麼狀況下,只要沒有解鎖其餘線程就沒法進入「加鎖代碼」),同時第一個線程處於等待隊列中(此時並未解鎖)。第二個線程進來以後同第一線程同樣,發現沒有圖片就進入等待狀態,而後第三個線程進入。。。如此反覆,直到第十五個線程也處於等待。此時點擊「建立圖片」後會執行createImageName方法,這是一個生產者,它會建立一個圖片連接放到imageNames中,而後經過調用NSCondition的signal方法就會在條件等待隊列中選擇一個線程(該線程會任意選取,假設爲線程A)開啓,那麼此時這個線程就會繼續執行。在上面代碼中,wati方法以後會繼續執行圖片加載方法,那麼此時線程A啓動以後繼續執行圖片加載方法,固然此時能夠成功加載圖片。加載完圖片以後線程A就會釋放鎖,整個線程任務完成。此時再次點擊」建立圖片「按鈕重複前面的步驟加載其餘圖片。
爲了說明上面的過程,這裏以一個流程圖的進行說明,流程圖藍色部分表明15個加載圖片的線程,綠色部分表示建立圖片資源線程。
在iOS開發中,除了同步鎖有時候還會用到一些其餘鎖類型,在此簡單介紹一下:
NSRecursiveLock :遞歸鎖,有時候「加鎖代碼」中存在遞歸調用,遞歸開始前加鎖,遞歸調用開始後會重複執行此方法以致於反覆執行加鎖代碼最終形成死鎖,這個時候可使用遞歸鎖來解決。使用遞歸鎖能夠在一個線程中反覆獲取鎖而不形成死鎖,這個過程當中會記錄獲取鎖和釋放鎖的次數,只有最後二者平衡鎖才被最終釋放。
NSDistributedLock:分佈鎖,它自己是一個互斥鎖,基於文件方式實現鎖機制,能夠跨進程訪問。
pthread_mutex_t:同步鎖,基於C語言的同步鎖機制,使用方法與其餘同步鎖機制相似。
提示:在開發過程當中除非必須用鎖,不然應該儘量不使用鎖,由於多線程開發自己就是爲了提升程序執行順序,而同步鎖自己就只能一個進程執行,這樣難免下降執行效率。
1>不管使用哪一種方法進行多線程開發,每一個線程啓動後並不必定當即執行相應的操做,具體何時由系統調度(CPU空閒時就會執行)。
2>更新UI應該在主線程(UI線程)中進行,而且推薦使用同步調用,經常使用的方法以下:
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更加簡單,推薦使用後者。