你要知道的iOS多線程NSThread、GCD、NSOperation、RunLoop都在這里
轉載請注明出處 http://m.itdecent.cn/p/973f0a5e0ec3
本系列文章主要講解iOS中多線程的使用,包括:NSThread、GCD、NSOperation以及RunLoop的使用方法詳解,本系列文章不涉及基礎的線程/進程、同步/異步、阻塞/非阻塞、串行/并行,這些基礎概念,有不明白的讀者還請自行查閱。本系列文章將分以下幾篇文章進行講解,讀者可按需查閱。
- iOS多線程——你要知道的NSThread都在這里
- iOS多線程——你要知道的GCD都在這里
- iOS多線程——你要知道的NSOperation都在這里
- iOS多線程——你要知道的RunLoop都在這里
- iOS多線程——RunLoop與GCD、AutoreleasePool
組織架構說明
本系列文章是按照相關多線程類的抽象層次撰寫的,也就是說NSThread是Foundation框架提供的最基礎的多線程類,每一個NSThread類的對象即代表一個線程,接下來蘋果為開發(fā)者封裝了GCD(Grand Central Dispatch),GCD相比于NSThread來說,提供了便捷的操作方法,開發(fā)者不需要再關注于管理線程的生命周期,也不需要自行管理一個線程池用于線程的復用,但GCD是以C函數對外提供接口,因此Foundation框架在GCD的基礎上進行了面向對象的封裝,提供了面向對象的多線程類NSOperation和NSOperationQueue,抽象層次更高。
由于OC是C語言的超集,開發(fā)者也可以選擇使用POSIX標準的線程pthread,pthread和NSThread都是對內核mach kernel的mach thread的封裝,所以在開發(fā)時一般不會使用pthread。
RunLoop是與線程相關的一個基本組成,想要線程在執(zhí)行完任務后不退出,在沒有任務時睡眠以節(jié)省CPU資源都需要RunLoop的實現,因此,正確的理解線程就需要深入理解RunLoop相關知識。
NSThread的使用姿勢全解
在組織架構說明中講到,NSThread是對內核mach kernel中的mach thread的封裝,所以,每一個NSThread的對象其實就是一個線程,我們創(chuàng)建一個NSThread對象也就意味著我們創(chuàng)建了一個新的線程。初始化創(chuàng)建NSThread的方法有如下幾種:
/*
使用target對象的selector作為線程的任務執(zhí)行體,該selector方法最多可以接收一個參數,該參數即為argument
*/
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
/*
使用block作為線程的任務執(zhí)行體
*/
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
/*
類方法,返回值為void
使用一個block作為線程的執(zhí)行體,并直接啟動線程
上面的實例方法返回NSThread對象需要手動調用start方法來啟動線程執(zhí)行任務
*/
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
/*
類方法,返回值為void
使用target對象的selector作為線程的任務執(zhí)行體,該selector方法最多接收一個參數,該參數即為argument
同樣的,該方法創(chuàng)建完線程后會自動啟動線程不需要手動觸發(fā)
*/
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
下面分別舉幾個栗子:
/*
說明: 本文的栗子都是在單視圖的工程中執(zhí)行,防止主線程退出后,其他線程被退出,不方便實驗。
*/
//線程的任務執(zhí)行體并接收一個參數arg
- (void)firstThread:(id)arg
{
for (int i = 0; i < 10; i++)
{
NSLog(@"Task %@ %@", [NSThread currentThread], arg);
}
NSLog(@"Thread Task Complete");
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear: YES];
/*
創(chuàng)建一個線程,線程任務執(zhí)行體為firstThread:方法
該方法可以接收參數@"Hello, World"
*/
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(firstThread:) object:@"Hello, World"];
//設置線程的名字,方便查看
[thread setName:@"firstThread"];
//啟動線程
[thread start];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear: YES];
NSLog("ViewDidAppear");
}
上面的栗子沒有什么實際意義,僅僅為了展示如何創(chuàng)建并啟動線程,啟動程序后就可以看到程序輸出了10次
Task <NSThread: 0x1c446f780>{number = 4, name = firstThread} Hello, World
上面輸出了線程的名稱,還輸出了我們傳入的參數,通過很簡單的代碼就可以創(chuàng)建一個新的線程來執(zhí)行任務,在開發(fā)中盡量將耗時的操作放在其他線程中執(zhí)行,只將更新UI的操作放在主線程中執(zhí)行。
一般情況下,通過上述方法創(chuàng)建的線程在執(zhí)行完任務執(zhí)行體后就會退出并銷毀,可以在firstThread:方法的第二個NSLog方法和viewDidAppear:方法的輸出上打斷點,然后運行程序查看線程信息,在第一個斷點時即firstThread:方法的斷點中,程序中線程信息如下圖:

從上圖可以看到,現在程序中有一個線程名為firstThread,該線程即為我們創(chuàng)建的NSThread對象,而com.apple.main-thread(serial)即為主線程的名稱,其中serial是指明主線程是串行的,這個內容會在GCD中進行講解,我們可以通過類方法[NSThread mainThread]來獲取主線程。接下來繼續(xù)執(zhí)行到第二個斷點,程序中線程信息如下圖:

從上圖可以看到,firstThread線程不見了,因為在執(zhí)行完任務執(zhí)行體后該線程就退出并被銷毀了,
通過這個栗子也說明了,我們無法復用NSThread,盡管線程的創(chuàng)建相比進程更加輕量級,但創(chuàng)建一個線程遠比創(chuàng)建一個普通對象要消耗資源,而主線程和接收事件處理的線程仍然存在,這正是因為RunLoop的作用,這個內容也會在RunLoop部分進行講解。
接下來繼續(xù)講解創(chuàng)建NSThread的其他方法,具體栗子如下:
//栗子2:
/*
通過傳入block的方式創(chuàng)建一個線程,線程執(zhí)行體即為block的內容
但該方式創(chuàng)建線程無法傳入參數
*/
NSThread *thread = [[NSThread alloc] initWithBlock:^{
for (int i = 0; i < 100; i++)
{
NSLog(@"Task %@", [NSThread currentThread]);
}
}];
//設置線程名稱
[thread setName:@"firstThread"];
//啟動線程
[thread start];
//栗子3:
/*
通過類方法創(chuàng)建并自動啟動一個線程
該線程的執(zhí)行體即為傳入的block
*/
[NSThread detachNewThreadWithBlock:^{
for (int i = 0; i < 100; i++)
{
NSLog(@"Task %@", [NSThread currentThread]);
}
}];
//栗子4:
/*
通過類方法創(chuàng)建并自動啟動一個線程
該線程的執(zhí)行體為self的firstThread:方法,并傳入相關參數
*/
[NSThread detachNewThreadSelector:@selector(firstThread:) toTarget:self withObject:@"Hello, World!"];
上述把所有NSThread的創(chuàng)建方法都講解了一遍,實例方法和類方法的區(qū)別就在于,實例方法會返回NSThread對象,當需要啟動線程時需要手動觸發(fā)start方法,而類方法沒有返回值,創(chuàng)建線程后立即啟動該線程。這里說的啟動線程start方法,僅僅是將線程的狀態(tài)從新建轉為就緒,何時執(zhí)行該線程的任務需要系統自行調度。
接下來再看NSThread中幾個比較常用的屬性和方法:
/*
類屬性,用于獲取當前線程
如果是在主線程調用則返回主線程對象
如果在其他線程調用則返回其他的當前線程
什么線程調用,就返回什么線程
*/
@property (class, readonly, strong) NSThread *currentThread;
//類屬性,用于返回主線程,不論在什么線程調用都返回主線程
@property (class, readonly, strong) NSThread *mainThread;
/*
設置線程的優(yōu)先級,范圍為0-1的doule類型,數字越大優(yōu)先級越高
我們知道,系統在進行線程調度時,優(yōu)先級越高被選中到執(zhí)行狀態(tài)的可能性越大
但是我們不能僅僅依靠優(yōu)先級來判斷多線程的執(zhí)行順序,多線程的執(zhí)行順序無法預測
*/
@property double threadPriority;
//線程的名稱,前面的栗子已經介紹過了
@property (nullable, copy) NSString *name
//判斷線程是否正在執(zhí)行
@property (readonly, getter=isExecuting) BOOL executing;
//判斷線程是否結束
@property (readonly, getter=isFinished) BOOL finished;
//判斷線程是否被取消
@property (readonly, getter=isCancelled) BOOL cancelled;
/*
讓線程睡眠,立即讓出當前時間片,讓出CPU資源,進入阻塞狀態(tài)
類方法,什么線程執(zhí)行該方法,什么線程就會睡眠
*/
+ (void)sleepUntilDate:(NSDate *)date;
//同上,這里傳入時間
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
//退出當前線程,什么線程執(zhí)行,什么線程就退出
+ (void)exit;
/*
實例方法,取消線程
調用該方法會設置cancelled屬性為YES,但并不退出線程
*/
- (void)cancel;
接下來再舉一個栗子:
//按鈕點擊事件處理器
- (void)btnClicked
{
//取消線程
[self.thread cancel];
}
- (void)viewWillAppear:(BOOL)animated
{
self.thread = [[NSThread alloc] initWithBlock:^{
for (int i = 0; i < 100; i++)
{
//獲取當前正在執(zhí)行的線程,即self.thread
NSThread *currentThread = [NSThread currentThread];
//判斷線程是否被取消
if ([currentThread isCancelled])
{
//如果被取消就退出當前正在執(zhí)行的線程,即self.thread
[NSThread exit];
}
NSLog(@"Task %@", currentThread);
//循環(huán)內,每次循環(huán)睡1s
[NSThread sleepForTimeInterval:1];
}
}];
[self.thread setName:@"firstThread"];
//啟動線程
[self.thread start];
}
上面的栗子也比較簡單,在視圖中加入了一個按鈕,點擊按鈕就會讓我們創(chuàng)建的線程執(zhí)行退出方法,在viewWillAppear:方法中創(chuàng)建并啟動了一個線程,這個線程每次循環(huán)都會判斷當前線程是否被取消,如果取消就退出當前線程,接下來線程就會被銷毀,每次循環(huán)執(zhí)行完后都會讓當前線程睡眠一秒,這里可能很多人都會有誤區(qū),讓線程睡眠會使得線程進入阻塞狀態(tài),當睡眠時間到后就會從阻塞狀態(tài)進入就緒狀態(tài),被系統線程調度為執(zhí)行狀態(tài)后才能繼續(xù)執(zhí)行,所以這里睡1s并不是說精準的1s后再繼續(xù)執(zhí)行,只是1s后從阻塞態(tài)進入就緒態(tài),之后何時執(zhí)行由系統調度決定。還需要說明的是cancel方法并不會讓線程退出,僅僅是將cancelled屬性置為YES,退出需要我們手動觸發(fā)exit方法。
所以執(zhí)行上述代碼后,每一秒多會輸出一次,當我們點擊按鈕后該線程就會將cancelled屬性置為YES,在線程下次執(zhí)行時就會執(zhí)行exit方法退出線程,退出線程會立即終止當前執(zhí)行的任務,也就是說exit方法后的代碼不會再執(zhí)行了。
退出線程有如下三種情況:
- 任務執(zhí)行體執(zhí)行完成后正常退出
- 任務執(zhí)行體執(zhí)行過程中發(fā)生異常也會導致當前線程退出
- 執(zhí)行NSThread類的exit方法退出當前線程
關于優(yōu)先級的栗子就不再贅述了,可以自行實驗,比如,啟動兩個線程,使用for循環(huán)來輸出文本,并設置不同的優(yōu)先級,可以發(fā)現,優(yōu)先級高的線程獲取到時間片即能夠執(zhí)行輸出的機會高于優(yōu)先級低的。
接下來舉一個多線程下載圖片的簡單栗子:
- (void)viewWillAppear:(BOOL)animated
{
//創(chuàng)建一個線程用來下載圖片
NSThread *thread = [[NSThread alloc] initWithBlock:^{
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1508398116220&di=ba2b7c9bf32d0ecef49de4fb19741edb&imgtype=0&src=http%3A%2F%2Fwscont2.apps.microsoft.com%2Fwinstore%2F1x%2Fea9a3c59-bb26-4086-b823-4a4869ffd9f2%2FScreenshot.398115.100000.jpg"]]];
//圖片下載完成之后使用主線程來執(zhí)行更新UI的操作
[self performSelectorOnMainThread:@selector(updateImage:) withObject:image waitUntilDone:NO];
}];
//啟動線程
[thread start];
}
//主線程執(zhí)行當前更新UI的方法
- (void)updateImage:(UIImage*)image
{
self.imageView.image = image;
}
上面使用了NSObject提供的performSelectorOnMainThread:WithObject:watiUntilDone:方法,該方法就是用于使用主線程執(zhí)行相關方法,iOS對于更新UI的操作有規(guī)定,必須放在主線程執(zhí)行,否則會產生運行時警告,最重要的是,不在主線程執(zhí)行無法預知什么時候才會進行更新操作,可能會產生各種意外。
NSThread 鎖機制 經典的生產者消費者問題
提到多線程必然會考慮競爭條件,OC也為我們提供了同步的機制以及鎖的機制,接下來舉一個炒雞經典的銀行取錢的栗子:
//定義一個Account類
@interface Account: NSObject
//賬號
@property (nonatomic, strong) NSString *accountNumber;
//余額
@property (nonatomic, assign) double balance;
//取錢操作
- (void)draw:(id)money;
@end
@implementation Account
@synthesize accountNumber = _accountNumber;
@synthesize balance = _balance;
- (void)draw:(id)money
{
double drawMoney = [money doubleValue];
//判斷余額是否足夠
if (self.balance >= drawMoney)
{
//當前線程睡1毫秒
//[NSThread sleepForTimeInterval:0.001];
self.balance -= drawMoney;
NSLog(@"%@ draw money %lf balance left %lf", [[NSThread currentThread] name], drawMoney, self.balance);
}
else
{
//余額不足,提示
NSLog(@"%@ Balance Not Enouth", [[NSThread currentThread] name]);
}
}
@end
//ViewController.m
- (void)viewWillAppear:(BOOL)animated
{
Account *account = [[Account alloc] init];
account.accountNumber = @"1603121434";
account.balance = 1500.0;
NSThread *thread1 = [[NSThread alloc] initWithTarget:account selector:@selector(draw:) object:@(1000)];
[thread1 setName:@"Thread1"];
NSThread *thread2 = [[NSThread alloc] initWithTarget:account selector:@selector(draw:) object:@(1000)];
[thread2 setName:@"Thread2"];
[thread1 start];
[thread2 start];
}
上面這個栗子很簡單,定義了一個Account類表示銀行賬戶,然后定義了取錢的操作,在draw:方法里,注釋了[NSThread sleepForTimeInterval:0.001];代碼,然后在視圖中創(chuàng)建了兩個線程,都去取錢,運行上述程序我們發(fā)現線程1取到錢了,線程2提示余額不足,但這個結果不一定正確,我們提到過,多線程的執(zhí)行順序是無法預測的,哪怕線程2的優(yōu)先級比線程1低,也有可能線程2先執(zhí)行,所以我們把注釋的一行去掉注釋,來模擬第一個線程進入到取錢的判斷條件體以后被系統線程調度切換,此時的輸出結果為:
Thread1 draw money 1000.000000 balance left 500.000000
Thread2 draw money 1000.000000 balance left -500.000000
這就是競爭條件,這里不再贅述什么是競爭條件,線程1進入判斷體后還沒有進行取錢的操作就被切換到就緒態(tài),系統切換線程2執(zhí)行,由于線程1還沒有進行取錢操作,所以余額是滿足要求的,線程2也進入了判斷體,這樣兩個線程都可以取到錢。
解決競爭條件的方法很多,比如鎖機制和同步代碼塊,接下來分別舉兩個栗子:
//栗子2:
- (void)draw:(id)money
{
@synchronized (self) {
double drawMoney = [money doubleValue];
if (self.balance >= drawMoney)
{
[NSThread sleepForTimeInterval:0.001];
self.balance -= drawMoney;
NSLog(@"%@ draw money %lf balance left %lf", [[NSThread currentThread] name], drawMoney, self.balance);
}
else
{
NSLog(@"%@ Balance Not Enouth", [[NSThread currentThread] name]);
}
}
}
//栗子3:
- (void)draw:(id)money
{
/*
self.lock在ViewController的初始化函數中進行初始化操作
self.lock = [[NSLock alloc] init];
*/
[self.lock lock];
double drawMoney = [money doubleValue];
if (self.balance >= drawMoney)
{
[NSThread sleepForTimeInterval:0.001];
self.balance -= drawMoney;
NSLog(@"%@ draw money %lf balance left %lf", [[NSThread currentThread] name], drawMoney, self.balance);
}
else
{
NSLog(@"%@ Balance Not Enouth", [[NSThread currentThread] name]);
}
[self.lock unlock];
}
在栗子2中,我們對draw:方法添加了一個同步代碼塊,使用@synchronized包圍的代碼即為同步代碼塊,同步代碼塊需要一個監(jiān)聽器,我們使用account對象本身作為監(jiān)聽器,因為是account對象產生的競爭條件,當執(zhí)行同步代碼塊時需要先獲取監(jiān)聽器,如果獲取不到則線程會被阻塞,當同步代碼塊執(zhí)行完成則釋放監(jiān)聽器,與java的synchronized同步代碼塊一樣。
栗子3,我們使用鎖機制,創(chuàng)建了一個NSLock類的鎖對象,lock方法用于獲取鎖,如果鎖被其他對象占用則線程被阻塞,unlock方法用于釋放鎖,以便其他線程加鎖。
線程的調度對于開發(fā)者來說是透明的,我們不能也無法預測線程執(zhí)行的順序,但有時我們需要線程按照一定條件來執(zhí)行,這時就需要線程間進行通信,NSCondition就提供了線程間通信的方法,查看一下NSCondition的聲明文件:
NS_CLASS_AVAILABLE(10_5, 2_0)
@interface NSCondition : NSObject <NSLocking> {
@private
void *_priv;
}
/*
調用NSCondition對象wait方法的線程會阻塞,直到其他線程調用該對象的signal方法或broadcast方法來喚醒
喚醒后該線程從阻塞態(tài)改為就緒態(tài),交由系統進行線程調度
執(zhí)行wait方法時內部會自動執(zhí)行unlock方法釋放鎖,并阻塞線程
*/
- (void)wait;
//同上,只是該方法是在limit到達時喚醒線程
- (BOOL)waitUntilDate:(NSDate *)limit;
/*
喚醒在當前NSCondition對象上阻塞的一個線程
如果在該對象上wait的有多個線程則隨機挑選一個,被挑選的線程則從阻塞態(tài)進入就緒態(tài)
*/
- (void)signal;
/*
同上,該方法會喚醒在當前NSCondition對象上阻塞的所有線程
*/
- (void)broadcast;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
NS_ASSUME_NONNULL_END
NSCondition實現了NSLocking協議,所以NSCondition同樣具有鎖的功能,與NSLock一樣可以獲取鎖與釋放鎖的操作。了解了NSCondition基本方法,就可以實現生產者消費者問題了:
@interface Account: NSObject
@property (nonatomic, strong) NSString *accountNumber;
@property (nonatomic, assign) double balance;
@property (nonatomic, strong) NSCondition *condition;
@property (nonatomic, assign) BOOL haveMoney;
- (void)deposite:(id)money;
- (void)draw:(id)money;
@end
@implementation Account
@synthesize accountNumber = _accountNumber;
@synthesize balance = _balance;
@synthesize condition = _condition;
@synthesize haveMoney = _haveMoney;
//NSCondition的getter,用于創(chuàng)建NSCondition對象
- (NSCondition*)condition
{
if (_condition == nil)
{
_condition = [[NSCondition alloc] init];
}
return _condition;
}
- (void)draw:(id)money
{
//設置消費者取錢20次
int count = 0;
while (count < 20)
{
//首先使用condition上鎖,如果其他線程已經上鎖則阻塞
[self.condition lock];
//判斷是否有錢
if (self.haveMoney)
{
//有錢則進行取錢的操作,并設置haveMoney為NO
self.balance -= [money doubleValue];
self.haveMoney = NO;
count += 1;
NSLog(@"%@ draw money %lf %lf", [[NSThread currentThread] name], [money doubleValue], self.balance);
//取錢操作完成后喚醒其他在次condition上等待的線程
[self.condition broadcast];
}
else
{
//如果沒有錢則在次condition上等待,并阻塞
[self.condition wait];
//如果阻塞的線程被喚醒后會繼續(xù)執(zhí)行代碼
NSLog(@"%@ wake up", [[NSThread currentThread] name]);
}
//釋放鎖
[self.condition unlock];
}
}
- (void)deposite:(id)money
{
//創(chuàng)建了三個取錢線程,每個取錢20次,則存錢60次
int count = 0;
while (count < 60)
{
//上鎖,如果其他線程上鎖了則阻塞
[self.condition lock];
//判斷如果沒有錢則進行存錢操作
if (!self.haveMoney)
{
//進行存錢操作,并設置haveMoney為YES
self.balance += [money doubleValue];
self.haveMoney = YES;
count += 1;
NSLog(@"Deposite money %lf %lf", [money doubleValue], self.balance);
//喚醒其他所有在condition上等待的線程
[self.condition broadcast];
}
else
{
//如果有錢則等待
[self.condition wait];
NSLog(@"Deposite Thread wake up");
}
//釋放鎖
[self.condition unlock];
}
}
@end
- (void)viewWillAppear:(BOOL)animate
{
[super viewWillAppear:YES];
Account *account = [[Account alloc] init];
account.accountNumber = @"1603121434";
account.balance = 0;
//消費者線程1,每次取1000元
NSThread *thread = [[NSThread alloc] initWithTarget:account selector:@selector(draw:) object:@(1000)];
[thread setName:@"consumer1"];
//消費者線程2,每次取1000元
NSThread *thread2 = [[NSThread alloc] initWithTarget:account selector:@selector(draw:) object:@(1000)];
[thread2 setName:@"consumer2"];
//消費者線程3,每次取1000元
NSThread *thread3 = [[NSThread alloc] initWithTarget:account selector:@selector(draw:) object:@(1000)];
[thread3 setName:@"consumer3"];
//生產者線程,每次存1000元
NSThread *thread4 = [[NSThread alloc] initWithTarget:account selector:@selector(deposite:) object:@(1000)];
[thread4 setName:@"productor"];
[thread start];
[thread2 start];
[thread3 start];
[thread4 start];
}
上面這個栗子也比較簡單,關于NSCondition需要注意的就是它的wait方法,在執(zhí)行wait方法前按照邏輯當然是要先獲取鎖,避免競爭條件,執(zhí)行wait方法后會阻塞當前線程,直到其他線程調用這個condition來喚醒被阻塞的線程,被阻塞的線程喚醒后進入就緒態(tài),當被調度執(zhí)行后會重新獲取鎖并在wait方法下一行代碼繼續(xù)執(zhí)行。還有一個要注意的地方就是是否有錢的haveMoney這個flag,這個flag存在的意義就是,當線程被喚醒后進入就緒態(tài),接下來系統線程調度具體調度哪個線程來執(zhí)行開發(fā)者是不知道的,也就是說我們無法預知接下來執(zhí)行的是生產者還是消費者,為了避免錯誤,加一個flag用于判斷。
上面代碼的寫法是按照蘋果官方文檔的順序寫的,更多關于NSCondition可查閱官方文檔:Apple NSCondition
備注
由于作者水平有限,難免出現紕漏,如有問題還請不吝賜教。