iOS 多線程
iOS程式啟動時,在建立一個程序的同時,會開啟一個線程,該線程被稱為主線程。
在一般情況下,我們所做的操作都在主線程執行,特别強調的是UI元素的更新、顯示等操作必須在主線程中完成(隻有主線程有直接修改UI的能力)。然而,在應用程式中,往往存在比較耗時的操作,如請求網絡圖檔、歌曲等資源。這些操作若在主線程中執行,由于耗時比較長,将會影響使用者的其他互動(如點選按鈕,無反應等)。
針對上述情況,我們應該單獨開啟輔助線程來執行這些耗時的操作,以保證主線程的流暢。
在iOS中提供了三種多線程技術,分别為:
1) NSThread
NSThread比其他兩個輕量級,使用簡單,但是需要自己管理線程的生命周期、睡眠以及喚醒等。
2) NSOperation、NSOperationQueue
不需要關心線程管理,可以把精力放在自己需要執行的操作上。
3) GCD(Crand Central Dispatch)
基于C語言的架構,可以充分利用多核,是官方推薦的多線程技術。
4) 此外,NSObject也提供了對多線程的簡單支援;
1、 NSObject
在使用多線程時,需要注意多線程的記憶體洩露問題。每個線程都維護它自己的 NSAutoreleasePool的棧對象。Cocoa希望在每個目前線程的棧裡面有一個可用的自動釋放池。如果一個自動釋放池不可用,對象将不會給釋放,進而造成記憶體洩露。對于 Application Kit 的主線程通常它會自動建立并消耗一個自動釋放池,但是輔助線程(和其他隻有 Foundationd 的程式)在使用Cocoa前必須自己手工建立。如果你的線程是長時間運作的,那麼有可能潛在産生很多自動 釋放的對象,你應該周期性的銷毀它們并建立自動釋放池(就像Application Kit 對 主線程那樣)。否則,自動釋放對象将會積累并造成記憶體大量占用。如果你的脫離線 程沒有使用Cocoa,你不需要建立一個自動釋放池。
是以在使用多線程執行的方法前要嵌套@autoreleasepool,否則可能會出現記憶體洩露問題。
NSObject提供了對多線程的支援,如下
1) performSelectorInBackground:withObject:
通常,由于線程管理相對比較繁瑣,而耗時的任務又無法知道其準确的完成時間,是以可以使用該方法直接建立一個背景線程。例如,在大型互動式遊戲中,通常使用此方法播放音效。
2) performSelectorOnMainThread:withObject:waitUntilDone:
在背景線程或輔助線程中,需要更新UI時,可以使用此方法,如下
…
//在主線程更新UI
//imageView.image=[UIImageimageNamed:imageName];
[imageView performSelectorOnMainThread:@selector(setImage:)withObject:[UIImageimageNamed:imageName]waitUntilDone:YES];
2、 NSThread
NSThread建立線程的方法有:
1) + (void)detachNewThreadSelector:toTarget: withObject:
該方法會建立一個線程,并立即啟動該線程;
2) - (id)initWithTarget:selector:object:
該方法隻是建立一個線程,需要調用start方法才能執行線程,如下
//多線程,随機更新UIImageView的圖檔
-(void) freshImages
{
for (UIImageView *imageViewin _imagesViews)
{
//建立新的線程,并立即執行
//[NSThreaddetachNewThreadSelector:@selector(freshImage:) toTarget:selfwithObject:imageView];
//建立新的線程,但并不會立即執行,需要調用start方法
NSThread * thread=[[NSThreadalloc] initWithTarget:selfselector:@selector(freshImage:)object:imageView];
[thread start];
}
}
-(void) freshImage:(UIImageView *) imageView;
{
@autoreleasepool
{
int index=arc4random_uniform(17)+1;
NSString * imageName=nil;
if (index<10)
{
imageName=[NSStringstringWithFormat:@"NatGeo0%d",index];
}
else
{
imageName=[NSStringstringWithFormat:@"NatGeo%d",index];
}
//在主線程更新UI
[imageView performSelectorOnMainThread:@selector(setImage:)withObject:[UIImageimageNamed:imageName] waitUntilDone:YES];
}
}
3、 NSOperation和NSOperationQueue
NSOperation和NSOperationQueue工作原理:
1) NSOperation封裝要執行的操作;
2) 将建立好的NSOperation對象放在NSOperationQueue中;
3) 啟動NSOperationQueue開始新的線程執行隊列中的操作;
NSOperation子類:
1) NSInvocationOperation;
2) NSBlockOperation;
注意:使用多線程時,通常需要控制多線程的并發數,因為線程會消耗系統資源,同時運作的線程過多,系統會變慢。NSOperationQueue可以設定線程的最大并發數 setMaxConcurrentOperationCount。
//多線程,随機更新UIImageView的圖檔,需要在主隊列中更新UI
-(void)operationFreshImages
{
//操作隊列可以控制線程的并發數量,操作隊列也可以設定操作間的依賴關系,而控制操作的執行順序,但要避免循環依賴
//[_operationQueuesetMaxConcurrentOperationCount:4];
for (UIImageView *imageView in _imagesViews)
{
NSOperation * op=[NSBlockOperationblockOperationWithBlock:^{
[self blockOperationFreshImage:imageView];
}];
//将操作添加到操作隊列,就會開啟新的線程
[_operationQueue addOperation:op];
}
}
-(void) operationFreshImage:(UIImageView *) imageView
{
@autoreleasepool
{
int index=arc4random_uniform(17)+1;
NSString * imageName=nil;
if (index<10)
{
imageName=[NSStringstringWithFormat:@"NatGeo0%d",index];
}
else
{
imageName=[NSStringstringWithFormat:@"NatGeo%d",index];
}
//在主線程更新UI
//imageView.image=[UIImageimageNamed:imageName];
//[imageViewperformSelectorOnMainThread:@selector(setImage:) withObject:[UIImageimageNamed:imageName] waitUntilDone:YES];
[[NSOperationQueuemainQueue] addOperationWithBlock:^{
imageView.image=[UIImageimageNamed:imageName];
}];
}
}
-(void) blockOperationFreshImage:(UIImageView *) imageView
{
@autoreleasepool
{
int index=arc4random_uniform(17)+1;
NSString * imageName=nil;
if (index<10)
{
imageName=[NSStringstringWithFormat:@"NatGeo0%d",index];
}
else
{
imageName=[NSStringstringWithFormat:@"NatGeo%d",index];
}
//在主線程更新UI
//imageView.image=[UIImageimageNamed:imageName];
//[imageViewperformSelectorOnMainThread:@selector(setImage:) withObject:[UIImageimageNamed:imageName] waitUntilDone:YES];
//若NSOperation執行個體為NSBlockOperation,需要在主隊列中更新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
imageView.image=[UIImageimageNamed:imageName];
}];
}
}
4、 GCD
GCD是基于C語言的架構,在GCD中線程的執行主要依靠執行線程的隊列,在GCD中,有三中隊列:
1) 全局隊列;所有添加到主隊列中的任務都是并發執行的;dispatch_get_global_queue(QOS_CLASS_DEFAULT,0);
2) 串行隊列:所有添加到該隊列中的任務都是順序執行的;
dispatch_queue_create("sq",DISPATCH_QUEUE_SERIAL);
3) 主隊列:所有添加到該隊列中的任務都是在主線程中執行的;
dispatch_get_main_queue();
由此可知,任務是否能夠并發執行,關鍵在于任務所在的隊列,而隻有全局隊列中的任務才能并發執行。
工作原理:
1) 讓程式平行排隊任務,根據可用的處理資源,安排他們在任何可用處理器上執行任務;
2) 要執行的任務可以是一個函數或者一個block;
3) dispatch_async(異步操作),dispatch_sync(同步操作);
4) dispatch_notify可以實作監聽一組任務是否完成,完成後可以得到通知。
GCD的優點:
1) 充分利用多核;
2) 所有的多線程代碼集中在一起,便于維護;
3) GCD中無需使用@autoreleasepool;
注意:在主隊列中更新UI,最好使用同步方法。
//多線程,随機更新UIImageView的圖檔,需要在主隊列中更新UI
-(void) gcd
{
//1. 擷取全局隊列,所有任務是并發執行的
dispatch_queue_t queue=dispatch_get_global_queue(QOS_CLASS_DEFAULT,0);
//2. 在全局隊列上,異步執行
for (UIImageView * imageViewin _imagesViews)
{
dispatch_async(queue, ^{
NSLog(@"%@",[NSThreadcurrentThread]);
int index=arc4random_uniform(17)+1;
NSString * imageName=nil;
if (index<10)
{
imageName=[NSStringstringWithFormat:@"NatGeo0%d",index];
}
else
{
imageName=[NSStringstringWithFormat:@"NatGeo%d",index];
}
//3. 在主隊列中更新UI
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"%@",[NSThreadcurrentThread]);
imageView.image=[UIImageimageNamed:imageName];
});
});
}
}
5、 多線程同步
當多個線程對同一個資源出現争奪的時候,就會出現線程安全問題。
5.1、volatile
為了更好的了解多線程競争資源所可能出現的問題,首先要了解下“指令重排”和“記憶體可見性”。
為了達到最佳的性能,編譯器通常會對彙編基本的指令進行重新排序來盡可能保持處理器的指令流水線。作為優化的一部分,編譯器有可能會對通路記憶體的指令(不存在依賴關系)進行重新排序。不幸的是,靠編譯器檢測到所有的記憶體依賴的操作幾乎總是不大可能,那麼編譯器調整指令順序,将導緻不正确的結果。
記憶體屏障是一個用來確定記憶體操作按照正确順序工作的非阻塞同步工具(由硬體實作)。記憶體屏障的作用就像 一個栅欄,迫使處理器來完成位于障礙掐面的任何加載和存儲操作,才允許它執行位于屏障之後的加載和存儲操作。
至于“記憶體可見性”,将涉及到線程的記憶體空間。線程之間共享的變量存儲在主記憶體中,除此之外,每個線程都有一個私有的本地記憶體,本地記憶體存儲線程間共享變量的副本。此後,每個線程之間都直接操作本地記憶體空間中的副本(至于何時重新整理到主記憶體是不确定的),若線程之間需要互動則通過主記憶體進行資料互動,也即線程重新整理副本至主記憶體或從主記憶體重新讀取。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiMxgDNwMzMwETOwcDM1EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
“記憶體可見性”是指當一個變量值改變時,改變後的值確定對其他線程可見的(也即立即重新整理會主記憶體)。
而volatile修飾的變量,隻是表明該變量任何時候對其他線程都是可見的,也即volatile修改的變量值的存儲都會直接在主記憶體進行。
5.2、NSLock
鎖是最常用的同步工具。在Cocoa程式中NSLock中實作了一個簡單的互斥鎖。所有鎖的接口實際上都是通過NSLocking協定定義的,它定義了lock和unlock方法,來擷取或釋放鎖。
除了标準的鎖行為,NSLock類還增加了tryLock和lockBeforeData:方法。Cocoa還提供了其他類型的鎖,如NSRecursiveLock等,在此不再詳述。
5.3、@synchronized
@synchronized指令是在OC代碼中建立一個互斥鎖非常友善的方法。@synchronized指令做和互斥鎖一樣的工作。然而,在這種情況下,不需要顯示建立一個互斥鎖或鎖對象。相反,隻需要簡單的使用OC對象作為鎖的令牌即可。
建立給@synchronized指定的令牌是一個用來差別保護塊的唯一辨別符。如果有兩個不同的線程,每次在一個線程傳遞了一個不同的對象給 @synchronized 參數,那麼每次都将會擁有它的鎖,并持續處理,中間不被其他線程阻塞。然 而,如果你傳遞的是同一個對象,那麼多個線程中的一個線程會首先獲得該鎖,而其它線程将會被阻塞知道第一個線程退出臨界區。
作為一種預防措施,@synchronized塊還隐式的添加一個異常處理來保護代碼,該異常處理會在異常抛出的時候自動的釋放互斥鎖。
5.4、案例
下面是一個簡單的多線程賣票程式案例。
#import <Foundation/Foundation.h>
@interface Ticket : NSObject
@property(atomic,assign)NSUInteger ticketNum;
+(instancetype) sharedTicket;
@end
static Ticket * instance=nil;
@implementation Ticket
+ (instancetype)allocWithZone:(struct_NSZone *)zone
{
//確定執行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//記錄執行個體化對象
instance=[superallocWithZone:zone];
});
return instance;
}
+(instancetype) sharedTicket
{
//確定執行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance=[[Ticketalloc] init];
});
return instance;
}
@end
#import <UIKit/UIKit.h>
@interface TicketViewController :UIViewController
@end
#import "TicketViewController.h"
#import "Ticket.h"
@interface TicketViewController ()
{
UITextView * _textView;
}
@end
@implementation TicketViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.title=@"賣票";
self.view.backgroundColor=[UIColorwhiteColor];
//
_textView=[[UITextViewalloc] initWithFrame:self.view.frame];
_textView.editable=NO;
[self.viewaddSubview:_textView];
//資料
[Ticket sharedTicket].ticketNum=30;
[self gcd];
}
-(void) appendText:(NSString *) text
{
//1. 擷取textView文本
NSMutableString * str=[[NSMutableStringalloc] initWithString:_textView.text];
//2. 追加文本到textView中
[str appendString:[NSStringstringWithFormat:@"%@\n",text]];
//3. 更新textView文本
_textView.text=str;
//4.使textView滾動到文本底部
NSRange range=NSMakeRange(str.length-1,1);
[_textView scrollRangeToVisible:range];
}
-(void) sales:(NSString *) name
{
while (true)
{
if ([name isEqualToString:@"gcd-1"])
{
[NSThread sleepForTimeInterval:1.0f];
}
else
{
[NSThread sleepForTimeInterval:0.2f];
}
//同步鎖synchronized要鎖的範圍,對被搶奪的資源進行修改/讀取的代碼部分
@synchronized(self)
{
if ([TicketsharedTicket].ticketNum>0)
{
[Ticket sharedTicket].ticketNum--;
NSString * text=[NSStringstringWithFormat:@"剩餘票數:%lu,線程:%@",[TicketsharedTicket].ticketNum,name];
//在主隊列,更新界面
dispatch_async(dispatch_get_main_queue(), ^{
[self appendText:text];
});
}
else
{
break;
}
}
}
}
#pragma mark - gcd
-(void) gcd
{
//1.擷取全局隊列
dispatch_queue_t queue=dispatch_get_global_queue(QOS_CLASS_DEFAULT,0);
//2.異步執行
//3. gcd可以将關聯的操作,定義到一個群組中,定義到群組中之後,當所有線程執行完,可以獲得通知
//3.1 建立群組
dispatch_group_t group=dispatch_group_create();
dispatch_group_async(group, queue, ^{
[self sales:@"gcd-1"];
});
dispatch_group_async(group, queue, ^{
[self sales:@"gcd-2"];
});
dispatch_group_async(group, queue, ^{
[self sales:@"gcd-3"];
});
dispatch_group_notify(group, queue, ^{
NSLog(@"售完!");
});
}
@end