本篇文章將會簡單介紹 iOS 多線程相關的內容。對 NSOperation、NSOperationQueue 的使用進行介紹總結。還將會介紹線程鎖相關的內容。
iOS 多線程
多線程在開發(fā)中被廣泛使用,創(chuàng)建多個線程,每個線程上同時執(zhí)行不同的任務,從而更快更好使用 CPU 來進行工作。iOS 中提供了多種創(chuàng)建線程的方法,方便開發(fā)者操作使用。
1、pthread
POSIX 線程,定義了創(chuàng)建和操縱線程的一套 C語言的 API,使用方法如下:
//#import <pthread.h>
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
pthread_t thread;
pthread_create(&thread, NULL, calculate, NULL);
}
void *calculate() {
NSLog(@"%@", [NSThread currentThread]);
return NULL;
}
//<NSThread: 0x600002d28c40>{number = 7, name = (null)}
2、NSThread
NSThread 是 OC 對 pthread 的一個封裝。通過封裝,可以更方便的操作線程。
NSThread * thread=[[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"abc"];
//NSThread *thread = [[NSThread alloc] initWithBlock:^{ }]; //iOS 10
thread.name=@"子線程";
[thread start];
// <NSThread: 0x600000e17a80>{number = 8, name = 子線程} -- abc
//自啟動創(chuàng)建子線程的方法
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"abc"];
//[NSThread detachNewThreadWithBlock:^{ }]; //iOS 10
//<NSThread: 0x600000b21040>{number = 8, name = (null)} -- abc
//為了更加簡化我們創(chuàng)建一個子線程的操作, NSObject對創(chuàng)建線程封裝了一些方法
//內部會自動的創(chuàng)建一個子線程,并且把@selector中的方法交給子線程去做,返回值void
[self performSelectorInBackground:@selector(run:) withObject:@"abc"];
// <NSThread: 0x600001fd4c40>{number = 8, name = (null)} -- abc
//[self performSelector:@selector(run:) withObject:@"abc"];
// <NSThread: 0x60000174cf00>{number = 1, name = main} -- asdf
//線程間通信
[NSThread detachNewThreadWithBlock:^{
[self run:@"yyy"];
NSLog(@"on thread");
[NSThread sleepForTimeInterval:2];
NSLog(@"thread end sleep");
[self performSelector:@selector(run:) withObject:@"xxx"];
[self performSelectorOnMainThread:@selector(run:) withObject:@"abc" waitUntilDone:YES]; //從子線程轉回主線程
[self performSelectorInBackground:@selector(run:) withObject:@"123"];
}];
// <NSThread: 0x600000015240>{number = 8, name = (null)} -- yyy
// on thread
// thread end sleep
// <NSThread: 0x600000015240>{number = 8, name = (null)} -- xxx
// <NSThread: 0x600000058f00>{number = 1, name = main} -- abc
// <NSThread: 0x600003bdaf00>{number = 9, name = (null)} -- 123
- (void)run:(id)obj {
NSLog(@"%@ -- %@", [NSThread currentThread], obj);
}
使用 pthread 或者 NSThread 是直接對線程操作,可能會引發(fā)的一個問題,如果你的代碼和所基于的框架代碼都創(chuàng)建自己的線程,那么活動的線程數(shù)量有可能以指數(shù)級增長,每個線程都會消耗內存和內核資源。這樣管理多個線程比較困難,所以不推薦在多線程任務多的情況下使用。
蘋果官方推薦使用 GCD、NSOperation 和 NSOperationQueue ,這樣就不用直接跟線程打交道了,只需要向隊列中添加代碼塊即可,GCD 在后端管理著一個線程池。GCD 不僅決定著你的代碼塊將在哪個線程被執(zhí)行,它還根據(jù)可用的系統(tǒng)資源對這些線程進行管理。這樣可以將開發(fā)者從線程管理的工作中解放出來,通過集中的管理線程,來緩解大量線程被創(chuàng)建的問題。
有關 GCD 的介紹可查看之前的文章。
下面來介紹有關 NSOperation 和 NSOperationQueue 的操作。
3、NSOperation、NSOperationQueue
NSOperation、NSOperationQueue 是 iOS 中一種多線程實現(xiàn)方式,實際上是基于 GCD 更高一層的封裝,NSOperation 和 NSOperationQueue 分別對應 GCD 的任務和隊列。面向對象,比 GCD 更簡單易用。
3.1、NSOperation
NSOperation是一個和任務相關的抽象類,不具備封裝操作的能力,必須使用其子類 NSBlockOperation、NSInvocationOperation 或者使用自定義的繼承自 NSOperation 的子類。
NSInvocationOperation
NSInvocationOperation *iop = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run:) object:@"123"];
[iop start]; //在主線程上運行,相當于同步執(zhí)行
// <NSThread: 0x600003e8cf00>{number = 1, name = main} -- 123
NSBlockOperation
NSBlockOperation * bop=[NSBlockOperation blockOperationWithBlock:^{
[self run:@"blockOperationWithBlock"];
}];
[bop addExecutionBlock:^{
[self run:@"addExecutionBlock"];
}];
bop.completionBlock=^{
NSLog(@"所有任務都執(zhí)行完成了");
};
[bop start];
// <NSThread: 0x600003e8cf00>{number = 1, name = main} -- blockOperationWithBlock
// <NSThread: 0x600003edd640>{number = 5, name = (null)} -- addExecutionBlock
// 所有任務都執(zhí)行完成了
如果添加的操作多的話,blockOperationWithBlock:
中的操作也可能會在其他線程(非當前線程)中執(zhí)行,這是由系統(tǒng)決定的,并不是說添加到 blockOperationWithBlock:
中的操作一定會在當前線程中執(zhí)行。addExecutionBlock: 的在哪個線程執(zhí)行也不一定。一般都是把操作加入隊列,通過隊列來控制執(zhí)行方式,對于線程的操作不用我們來處理。正如前面提到的,我們不用直接跟線程打交道,只需添加任務即可。
自定義 NSOperation
我們可以通過重寫 main 或者 start 方法 來定義自己的 operations 。
重寫 main 這種方法簡單,不需要管理一些狀態(tài)屬性(例如 isExecuting 和 isFinished),當 main 方法返回的時候,這個 operation 就結束了。這種方式使用起來非常簡單,但是靈活性相對重寫 start 來說要少一些。
@interface MyOperation : NSOperation
@end
@implementation MyOperation
- (void)main {
NSLog(@"my operation main() -- %@", [NSThread currentThread]);
//為了能使用操作隊列所提供的取消功能,
//在長時間操作中時不時地檢查 isCancelled 屬性
while (notDone && !self.isCancelled) {
// 進行處理
}
}
@end
MyOperation *op = [[MyOperation alloc] init];
[op start];
//my operation main() -- <NSThread: 0x6000009780c0>{number = 1, name = main}
如果想擁有更多的控制權,以及在一個操作中可以執(zhí)行異步任務,可以通過重寫 start 方法實現(xiàn):
@interface MyOperation ()
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@end
@implementation MyOperation
@synthesize executing = _executing;
@synthesize finished = _finished;
- (void)start {
self.executing = YES;
self.finished = NO;
NSLog(@"start - %@", [NSThread currentThread]);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"do - %@", [NSThread currentThread]);
[self done];
});
}
- (void)done {
self.finished = YES;
self.executing = NO;
}
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
[self didChangeValueForKey:@"isExecuting"];
}
@end
MyOperation *op = [[MyOperation alloc] init];
[op start];
//start - <NSThread: 0x600000f54d80>{number = 1, name = main}
//do - <NSThread: 0x600000f0df00>{number = 6, name = (null)}
這種情況下,你必須手動管理操作的狀態(tài)。 為了讓操作隊列能夠捕獲到操作的改變,需要將狀態(tài)的屬性以配合 KVO 的方式進行實現(xiàn)。如果你不使用它們默認的 setter 來進行設置的話,你就需要在合適的時候發(fā)送合適的 KVO 消息。
3.2、NSOperationQueue
NSOperationQueue 有兩種不同類型的隊列:主隊列和自定義隊列。主隊列運行在主線程之上,而自定義隊列在后臺執(zhí)行。在兩種類型中,這些隊列所處理的任務都使用 NSOperation 的子類來表述。
NSInvocationOperation * iop1=[[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run:) object:@"queue iop1"];
NSBlockOperation * bop=[NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@ -- queue bop",[NSThread currentThread]);
}];
//添加依賴關系
//iop1 依賴于 bop 一定是在 bop 任務執(zhí)行完成之后才會執(zhí)行 iop1 中的任務。相當于間接的設定了任務的執(zhí)行順序。
//看下面的打印內容,添加依賴的兩個任務,在同一個線程中執(zhí)行,順序執(zhí)行。這個不確定,不用管線程
[iop1 addDependency:bop];
//創(chuàng)建一個隊列, 把任務交給隊列管理
NSOperationQueue * queue=[[NSOperationQueue alloc] init];
[queue addOperation:iop1];
[queue addOperation:bop];
[queue addOperationWithBlock:^{
NSLog(@"%@ -- queue add block",[NSThread currentThread]);
}];
//waitUntilFinished 是否等待隊列中的執(zhí)行任務完成之后再去執(zhí)行后面的邏輯代碼
//[queue addOperations:@[iop1, bop] waitUntilFinished:YES];
/** 不能重復加入隊列,不然崩潰
reason: operations are finished, executing, or already in a queue, and cannot be enqueued' */
NSLog(@" end -- ");
//任務加入隊列,隊列創(chuàng)建子線程并發(fā)執(zhí)行,不需要調用 start 方法
// end -- //不阻塞主線程,這個先打印
// <NSThread: 0x600000ce8380>{number = 7, name = (null)} -- queue add block
// <NSThread: 0x600002851140>{number = 5, name = (null)} -- queue bop
// <NSThread: 0x60000287cf00>{number = 5, name = (null)} -- queue iop1
添加依賴關系
[iop1 addDependency:bop];
iop1 依賴于 bop 一定是在 bop 任務執(zhí)行完成之后才會執(zhí)行 iop1 中的任務。相當于間接的設定了任務的執(zhí)行順序。
根據(jù)上面打印內容,添加依賴的兩個任務,在同一個線程中執(zhí)行,順序執(zhí)行。這個不確定,不用管線程。
maxConcurrentOperationCount
queue.maxConcurrentOperationCount = 2 ;
用來控制一個特定隊列中可以有多少個操作參與并發(fā)執(zhí)行。
若將其設置為 1 的話,你將得到一個串行隊列,這在以隔離為目的的時候會很有用。
addBarrierBlock
類似于 GCD 中的 dispatch_barrier_async 柵欄。類似分界線,阻礙后面的任務執(zhí)行,直到 barrier block 執(zhí)行完畢。
NSOperationQueue * queue=[[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
NSLog(@"%@ --1 ",[NSThread currentThread]);
}];
[queue addBarrierBlock:^{
NSLog(@"%@ -- barrier ",[NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
NSLog(@"%@ --2 ",[NSThread currentThread]);
}];
NSLog(@" end -- ");
// end --
// <NSThread: 0x600002de8b00>{number = 7, name = (null)} --1
// <NSThread: 0x600002dbc180>{number = 5, name = (null)} -- barrier
// <NSThread: 0x600002dbd280>{number = 4, name = (null)} --2
操作之間的通信
NSOperationQueue * queue=[[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
NSLog(@"%@ -- do something ",[NSThread currentThread]);
[NSThread sleepForTimeInterval:2];
//任務完成,回到主線程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"%@ -- completed ",[NSThread currentThread]);
}];
}];
NSLog(@" end -- ");
// end --
// <NSThread: 0x600003550980>{number = 4, name = (null)} -- do something
// <NSThread: 0x60000351cd80>{number = 1, name = main} -- completed
線程安全
在多線程中訪問共享資源,可能會遇到一些問題。比如,線程 A 和 B 都從內存中讀取出了計數(shù)器的值,線程 A 將計數(shù)器值加一,同時線程 B 也將計數(shù)器值加一,這時計數(shù)器被加了兩次,因為同時操作,結果只加一,這樣就導致了數(shù)據(jù)的混亂。
為了防止出現(xiàn)這樣的問題,多線程需要一種互斥的機制來訪問共享資源,保證線程安全。
互斥訪問就是同一時刻,只允許一個線程訪問某個特定資源。為了保證這一點,每個希望訪問共享資源的線程,首先需要獲得一個共享資源的互斥鎖,一旦某個線程對資源完成了操作,就釋放掉這個互斥鎖,這樣別的線程就有機會訪問該共享資源了。
加鎖方式,常見的有,@synchronized、NSLock、dispatch_semaphore。
@synchronized
//創(chuàng)建兩個操作,去訪問 self.count
NSOperationQueue * queue1=[[NSOperationQueue alloc] init];
[queue1 addOperationWithBlock:^{
NSLog(@"1 -- %@",[NSThread currentThread]);
[self addCount];
}];
NSOperationQueue * queue2=[[NSOperationQueue alloc] init];
[queue2 addOperationWithBlock:^{
NSLog(@"2 -- %@ ",[NSThread currentThread]);
[self addCount];
}];
- (void)addCount {
@synchronized (self) {
self.count += 1;
}
}
@synchronized (self) 括號里的 self 為該鎖的標識,只有當標識相同時,才滿足互斥。
NSLock
//self.lock = [[NSLock alloc] init];
- (void)addCount {
[self.lock lock];
self.count += 1;
[self.lock unlock];
}
NSLock 也是我們經(jīng)常所使用的鎖,除 lock 和 unlock 方法外,還有方法:
tryLock :嘗試加鎖,如果鎖不可用(已經(jīng)被鎖住),剛并不會阻塞線程,并返回NO。
lockBeforeDate: 會在所指定 Date 之前嘗試加鎖,如果在指定時間之前都不能加鎖,則返回NO。
類似的鎖還有,NSConditionLock、NSRecursiveLock、NSCondition
dispatch_semaphore
GCD 中的 dispatch_semaphore 信號量,也可以用來加鎖。
//@property (strong, nonatomic, nonnull) dispatch_semaphore_t someLock;
//self.someLock = dispatch_semaphore_create(1);
- (void)addCount {
dispatch_semaphore_wait(self.someLock, DISPATCH_TIME_FOREVER); //加鎖
self.count += 1;
dispatch_semaphore_signal(self.someLock); //解鎖
}
1、dispatch_semaphore_create 函數(shù)可以生成信號量,參數(shù)是信號量計數(shù)的初始值。
2、dispatch_semaphore_wait 函數(shù),當信號量值為 0 時等待,等待直到超時,參數(shù)可設置超時時長。信號量值大于等于 1 時,不等待,同時將信號量值減 1。
3、dispatch_semaphore_signal 函數(shù)會讓信號量值加 1,如果有通過dispatch_semaphore_wait 函數(shù)等待信號量值增加的線程,會由系統(tǒng)喚醒最先等待的線程執(zhí)行。
除了以上這些方法之外,還有 pthread_mutex、OSSpinLock 等方法,這里不再介紹,自行查閱資料。
避免死鎖
互斥鎖解決了內存讀寫安全的問題,但這也引入了其他問題,其中一個就是死鎖。當多個線程在相互等待著對方的結束時,就會發(fā)生死鎖,這時程序可能會被卡住。
在線程之間共享的資源越多,使用的鎖越多,程序被死鎖的概率也越大。所以要盡量減少線程間資源共享,確保共享的資源盡量簡單。
多線程注意事項
1、控制線程數(shù)量
使用并行隊列,當任務過多且耗時較長時,隊列會創(chuàng)建大量線程,而部分線程里面的耗時任務已經(jīng)耗盡了 CPU 資源,所以其他的線程也只能等待 CPU 時間片,過多的線程也會讓線程調度過于頻繁。
GCD 中并行隊列并不能限制線程數(shù)量,可以創(chuàng)建多個串行隊列來模擬并行的效果。
2、減少隊列切換
當線程數(shù)量超過 CPU 核心數(shù)量,CPU 核心通過線程調度切換用戶態(tài)線程,意味著有上下文的轉換(寄存器數(shù)據(jù)、棧等),過多的上下文切換會帶來資源開銷。雖然內核態(tài)線程的切換理論上不會是性能負擔,開發(fā)中還是應該盡量減少線程的切換。
使用隊列切換并不總是意味著線程的切換,代碼層面可以減少隊列切換來優(yōu)化。
NSOperationQueue * queue=[[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
//...
}];
}];
References
蘋果官網(wǎng):Operation Queues
并發(fā)編程:API 及挑戰(zhàn)
iOS中保證線程安全的幾種方式與性能對比
iOS 如何高效的使用多線程