iOS多執行緒學習---NSThread
摘自:http://www.cnblogs.com/kenshincui/p/3983982.html
多執行緒
簡介
當用戶播放音訊、下載資源、進行影象處理時往往希望做這些事情的時候其他操作不會被中斷或者希望這些操作過程中更加順暢。在單執行緒中一個執行緒只能做一件事情,一件事情處理不完另一件事就不能開始,這樣勢必影響使用者體驗。早在單核處理器時期就有多執行緒,這個時候多執行緒更多的用於解決執行緒阻塞造成的使用者等待(通常是操作完UI後用戶不再幹涉,其他執行緒在等待佇列中,CPU一旦空閒就繼續執行,不影響使用者其他UI操作),其處理能力並沒有明顯的變化。如今無論是移動作業系統還是PC、伺服器都是多核處理器,於是“並行運算”就更多的被提及。一件事情我們可以分成多個步驟,在沒有順序要求的情況下使用多執行緒既能解決執行緒阻塞又能充分利用多核處理器執行能力。
下圖反映了一個包含8個操作的任務在一個有兩核心的CPU中建立四個執行緒執行的情況。假設每個核心有兩個執行緒,那麼每個CPU中兩個執行緒會交替執行,兩個CPU之間的操作會並行運算。單就一個CPU而言兩個執行緒可以解決執行緒阻塞造成的不流暢問題,其本身執行效率並沒有提高,多CPU的並行運算才真正解決了執行效率問題,這也正是併發和並行的區別。當然,不管是多核還是單核開發人員不用過多的擔心,因為任務具體分配給幾個CPU運算是由系統排程的,開發人員不用過多關心繫統有幾個CPU。開發人員需要關心的是執行緒之間的依賴關係,因為有些操作必須在某個操作完成完才能執行,如果不能保證這個順序勢必會造成程式問題。
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:當前執行緒是否等待指定執行緒的方法執行完畢後再繼續執行,針對這裡是:是否等待在主執行緒中的updateImage方法執行完畢後在接著執行loadImage方法。 */ [self performSelectorOnMainThread:@selector(updateImage:) withObject:data waitUntilDone:YES]; NSLog(@"當前執行緒後續內容");
} #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 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
通過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]; [_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分類擴充套件方法
為了簡化多執行緒開發過程,蘋果官方對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:在主執行緒上執行一個方法(前面已經使用過)。