多線程系列篇章計(jì)劃內(nèi)容:
iOS多線程編程(一) 多線程基礎(chǔ)
iOS多線程編程(二) Pthread
iOS多線程編程(三) NSThread
iOS多線程編程(四) GCD
iOS多線程編程(五) GCD的底層原理
iOS多線程編程(六) NSOperation
iOS多線程編程(七) 同步機(jī)制與鎖
iOS多線程編程(八) RunLoop
NSThread 是蘋果提供的一種面向?qū)ο蟮妮p量級(jí)多線程解決方案,一個(gè) NSThread 對(duì)象代表一個(gè)線程,使用比較簡單,但是需要手動(dòng)管理線程的生命周期、處理線程同步等問題。
1. 創(chuàng)建、啟動(dòng)NSThread線程
- 創(chuàng)建一個(gè)NSThread線程有類方法和實(shí)例方法。
類方法創(chuàng)建:
+ (void)detachNewThreadWithBlock:(void (^)(void))block ;
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
實(shí)例方法創(chuàng)建:
- (instancetype)initWithBlock:(void (^)(void))block;
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;
使用實(shí)例方法創(chuàng)建線程返回線程對(duì)象,可以根據(jù)需要設(shè)置相應(yīng)屬性參數(shù)。
需要注意:block形式的創(chuàng)建方式 需在iOS10之后使用
- 創(chuàng)建完畢后不要忘記開啟線程!

線程創(chuàng)建完畢后對(duì)應(yīng)線程狀態(tài)的新建態(tài),我們需要調(diào)用 start方法啟動(dòng)線程(使用類方法創(chuàng)建的線程隱式的啟動(dòng)了線程),否則線程是不會(huì)執(zhí)行的。
但是使用類方法創(chuàng)建或者使用實(shí)例方法創(chuàng)建并且調(diào)用start方法之后,線程并不會(huì)立即執(zhí)行,只是將線程加入可調(diào)度線程池,進(jìn)入就緒狀態(tài),具體何時(shí)執(zhí)行需要等待CPU的調(diào)度。(關(guān)于線程狀態(tài)可以參閱 多線程基礎(chǔ) 中的線程生命周期)
2. NSThread線程屬性
-
name屬性:設(shè)置線程的名字
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"線程:%@ start",[NSThread currentThread]);
}];
thread.name = @"測(cè)試線程";
[thread start];
打印結(jié)果如下:
線程:<NSThread: 0x600001227200>{number = 6, name = 測(cè)試線程} start
-
qualityOfService屬性:設(shè)置線程優(yōu)先級(jí)
原本線程優(yōu)先級(jí)threadPriority屬性,是一個(gè)double類型,取值范圍為0.0~1.0,值越大,優(yōu)先級(jí)越高。不過,該屬性已被qualityOfService取代。qualityOfService是一個(gè)枚舉值。定義如下:
typedef NS_ENUM(NSInteger, NSQualityOfService) {
NSQualityOfServiceUserInteractive = 0x21,
NSQualityOfServiceUserInitiated = 0x19,
NSQualityOfServiceUtility = 0x11,
NSQualityOfServiceBackground = 0x09,
NSQualityOfServiceDefault = -1
} API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
NSQualityOfServiceUserInteractive 優(yōu)先級(jí)最高,從上到下依次降低,NSQualityOfServiceDefault 為默認(rèn)優(yōu)先級(jí)。
使用如下:
NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
NSLog(@"\n 線程:%@ start",[NSThread currentThread]);
}];
thread1.name = @"測(cè)試線程 1 ";
[thread1 start];
NSThread *thread2 = [[NSThread alloc] initWithBlock:^{
NSLog(@"\n 線程:%@ start",[NSThread currentThread]);
}];
thread2.qualityOfService = NSQualityOfServiceUserInteractive;
thread2.name = @"測(cè)試線程 2 ";
[thread2 start];
雖然 thread1 先于 thread2 start,但thread1優(yōu)先級(jí)為默認(rèn),而thread2優(yōu)先級(jí)為NSQualityOfServiceUserInteractive,在執(zhí)行時(shí),thread2 先于 thread1執(zhí)行。
線程:<NSThread: 0x600001e557c0>{number = 7, name = 測(cè)試線程 2 } start
線程:<NSThread: 0x600001e55700>{number = 6, name = 測(cè)試線程 1 } start
-
callStackReturnAddresses和callStackSymbols屬性:
callStackReturnAddresses 屬性定義如下:
@property (class, readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses
線程的調(diào)用會(huì)有函數(shù)的調(diào)用,該屬性返回的就是 該線程中函數(shù)調(diào)用的虛擬地址數(shù)組。
callStackSymbols 屬性定義如下:
@property (class, readonly, copy) NSArray<NSString *> *callStackSymbols
該屬性以符號(hào)的形式返回該線程調(diào)用函數(shù)。
callStackReturnAddress和callStackSymbols這兩個(gè)函數(shù)可以同NSLog聯(lián)合使用來跟蹤線程的函數(shù)調(diào)用情況,是編程調(diào)試的重要手段。
-
threadDictionary屬性:
每個(gè)線程有自己的堆??臻g,線程內(nèi)維護(hù)了一個(gè)鍵-值的字典,它可以在線程里面的任何地方被訪問。
你可以使用該字典來保存一些信息,這些信息在整個(gè)線程的執(zhí)行過程中都保持不變。
比如,你可以使用它來存儲(chǔ)在你的整個(gè)線程過程中 Run loop 里面多次迭代的狀態(tài)信息。
- 其他屬性
@property (class, readonly, strong) NSThread *mainThread; // 獲取主線程
@property (class, readonly, strong) NSThread *currentThread;// 獲取當(dāng)前線程
@property NSUInteger stackSize; // 線程使用堆棧大小,默認(rèn)512k
@property (readonly) BOOL isMainThread; // 是否是主線程
@property (class, readonly) BOOL isMainThread ; // reports whether current thread is main
@property (readonly, getter=isExecuting) BOOL executing ; // 線程是否正在執(zhí)行
@property (readonly, getter=isFinished) BOOL finished ; // 線程是否執(zhí)行完畢
@property (readonly, getter=isCancelled) BOOL cancelled; // 線程是否取消
3. NSThread線程的阻塞
NSThread提供了2個(gè)類方法,
+ (void)sleepUntilDate:(NSDate *)date; // 休眠到指定日期
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;// 休眠執(zhí)行時(shí)常
對(duì)于上面設(shè)置線程優(yōu)先級(jí)的示例代碼,我們稍做些更改。
NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
NSLog(@"\n 線程:%@ start",[NSThread currentThread]);
}];
thread1.name = @"測(cè)試線程 1 ";
[thread1 start];
// 加入休眠函數(shù)
[NSThread sleepForTimeInterval:1];
NSThread *thread2 = [[NSThread alloc] initWithBlock:^{
NSLog(@"\n 線程:%@ start",[NSThread currentThread]);
}];
thread2.qualityOfService = NSQualityOfServiceUserInteractive;
thread2.name = @"測(cè)試線程 2 ";
[thread2 start];
在 thread1 與 thread2 之間加入 [NSThread sleepForTimeInterval:1]; 讓主線程阻塞1秒,那么 thread1 將 先于 thread2 執(zhí)行,即使thread2 的優(yōu)先級(jí)是高于thread1。
這是因?yàn)?,thread1先start進(jìn)入就緒狀態(tài),此時(shí),主線程休眠,在CPU時(shí)間到來之時(shí),可調(diào)度線程池中只有thread1,thread1 被調(diào)度執(zhí)行,此時(shí)主線程休眠時(shí)間結(jié)束,thread2 進(jìn)入就緒態(tài),并在下一次CPU時(shí)間時(shí)被調(diào)度執(zhí)行。
4. NSThread的終止
- 取消線程
- (void)cancel ;
對(duì)于已被調(diào)度的線程是無法通過cancel取消的。
- 退出線程
+ (void)exit;
強(qiáng)制退出線程,使線程進(jìn)入死亡態(tài)。
5. 線程的通信
在開發(fā)中,我們有時(shí)需要在子線程進(jìn)行耗時(shí)操作,操作結(jié)束后切換到主線程進(jìn)行刷新UI。這就涉及到線程間的通信,NSThread線程提供了對(duì)NSObject的拓展函數(shù)。
5.1 NSObject方式
// 在主線程上執(zhí)行操作 wait表示是否阻塞該方法,等待主線程空閑再運(yùn)行,modes表示運(yùn)行模式kCFRunLoopCommonModes
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array;
// equivalent to the first method with kCFRunLoopCommonModes
// 在指定線程上執(zhí)行操作
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes
// 隱式創(chuàng)建一個(gè)線程并執(zhí)行
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
// NSObject函數(shù): 在當(dāng)前線程上執(zhí)行操作,調(diào)用 NSObject 的 performSelector:相關(guān)方法
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
舉個(gè)例子,我們來模擬子線程下載圖片回到線程刷新 UI 的實(shí)現(xiàn)
// 開辟子線程模擬網(wǎng)絡(luò)請(qǐng)求
- (void)downloadImage {
[NSThread detachNewThreadWithBlock:^{
// 1. 獲取圖片 imageUrl
NSURL *imageUrl = [NSURL URLWithString:@"https://xxxxx.jpg"];
// 2. 從 imageUrl 中讀取數(shù)據(jù)(下載圖片) -- 耗時(shí)操作
NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
// 通過二進(jìn)制 data 創(chuàng)建 image
UIImage *image = [UIImage imageWithData:imageData];
// 主線程刷新UI
[self performSelectorOnMainThread:@selector(mainThreadRefreshUI) withObject:image waitUntilDone:YES];
}];
}
// 主線程刷新 UI 調(diào)用方法
- (void)mainThreadRefreshUI:(UIImage *)image {
self.imageView.image = image;
}
5.2 端口通信方式
端口通信需要使用 NSPort ,NSPort 是一個(gè)抽象類,具體使用的時(shí)候可以使用其子類NSMachPort。
通過下面方法傳遞將要在線程間通信的信息數(shù)據(jù)。
- (BOOL)sendBeforeDate:(NSDate *)limitDate components:(nullable NSMutableArray *)components from:(nullable NSPort *) receivePort reserved:(NSUInteger)headerSpaceReserved;
- (BOOL)sendBeforeDate:(NSDate *)limitDate msgid:(NSUInteger)msgID components:(nullable NSMutableArray *)components from:(nullable NSPort *)receivePort reserved:(NSUInteger)headerSpaceReserved;
實(shí)現(xiàn)NSPortDelegate 的方法,接受端口傳遞過來的數(shù)據(jù)。
- (void)handlePortMessage:(NSPortMessage *)message
注意:在使用端口的時(shí)候,需要注意將端口將入當(dāng)前Runloop,否則消息無法傳遞
[[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
6. NSThread通知
NSWillBecomeMultiThreadedNotification:由當(dāng)前線程派生出第一個(gè)其他線程時(shí)發(fā)送,一般一個(gè)線程只發(fā)送一次
NSDidBecomeSingleThreadedNotification:這個(gè)通知目前沒有實(shí)際意義,可以忽略
NSThreadWillExitNotification線程退出之前發(fā)送這個(gè)通知
7. NSThread 線程安全案例
只要涉及到多線程就有可能存在非線程安全的情況。根本原因就是多條線程同時(shí)操作一片臨界區(qū),導(dǎo)致臨界區(qū)資源錯(cuò)亂。
我們來模擬多線程經(jīng)典的售票案例:兩個(gè)售票窗口同時(shí)售賣50張車票
- (void)initTicketStatusNotSave {
// 1. 設(shè)置剩余火車票為 50
self.ticketSurplusCount = 50;
// 2. 模擬窗口1售票的線程
self.ticketSaleWindow1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
self.ticketSaleWindow1.name = @"售票窗口1";
// 3. 模擬窗口2售票的線程
self.ticketSaleWindow2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
self.ticketSaleWindow2.name = @"售票窗口2";
// 4. 開始售賣火車票
[self.ticketSaleWindow1 start];
[self.ticketSaleWindow2 start];
}
/**
* 售賣火車票(非線程安全)
*/
- (void)saleTicketNotSafe {
while (1) {
//如果還有票,繼續(xù)售賣
if (self.ticketSurplusCount > 0) {
self.ticketSurplusCount --;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票數(shù):%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]);
[NSThread sleepForTimeInterval:0.2];
}
//如果已賣完,關(guān)閉售票窗口
else {
NSLog(@"所有火車票均已售完");
break;
}
}
}
截取部分結(jié)果如下:
2020-11-19 15:55:53.222575+0800 pthread[5018:211393] 剩余票數(shù):49 窗口:售票窗口1
2020-11-19 15:55:53.222589+0800 pthread[5018:211394] 剩余票數(shù):48 窗口:售票窗口2
2020-11-19 15:55:53.426619+0800 pthread[5018:211394] 剩余票數(shù):46 窗口:售票窗口2
2020-11-19 15:55:53.426626+0800 pthread[5018:211393] 剩余票數(shù):47 窗口:售票窗口1
2020-11-19 15:55:53.630102+0800 pthread[5018:211394] 剩余票數(shù):45 窗口:售票窗口2
2020-11-19 15:55:53.630144+0800 pthread[5018:211393] 剩余票數(shù):44 窗口:售票窗口1
2020-11-19 15:55:53.832564+0800 pthread[5018:211393] 剩余票數(shù):43 窗口:售票窗口1
2020-11-19 15:55:53.832649+0800 pthread[5018:211394] 剩余票數(shù):42 窗口:售票窗口2
2020-11-19 15:55:54.033279+0800 pthread[5018:211393] 剩余票數(shù):41 窗口:售票窗口1
2020-11-19 15:55:54.033360+0800 pthread[5018:211394] 剩余票數(shù):40 窗口:售票窗口2
2020-11-19 15:55:54.237370+0800 pthread[5018:211393] 剩余票數(shù):39 窗口:售票窗口1
2020-11-19 15:55:54.237370+0800 pthread[5018:211394] 剩余票數(shù):39 窗口:售票窗口2
2020-11-19 15:55:54.440124+0800 pthread[5018:211393] 剩余票數(shù):38 窗口:售票窗口1
2020-11-19 15:55:54.440200+0800 pthread[5018:211394] 剩余票數(shù):37 窗口:售票窗口2
2020-11-19 15:55:54.643881+0800 pthread[5018:211393] 剩余票數(shù):35 窗口:售票窗口1
2020-11-19 15:55:54.643889+0800 pthread[5018:211394] 剩余票數(shù):36 窗口:售票窗口2
2020-11-19 15:55:54.845543+0800 pthread[5018:211393] 剩余票數(shù):33 窗口:售票窗口1
......
這就是多線程同時(shí)操作同一片臨界區(qū)的結(jié)果,得到的票數(shù)是錯(cuò)亂的,這是不符合我們的預(yù)期的。
線程安全的解決方案,就是線程同步機(jī)制。比較常用的是使用【鎖】。在一個(gè)線程占用臨界區(qū)的時(shí)候,不允許其他線程進(jìn)入。
iOS 實(shí)現(xiàn)線程加鎖有很多種方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic等等。更詳細(xì)的鎖相關(guān)知識(shí)參見iOS多線程編程(七)-鎖。這里我們使用@synchronized對(duì)此案例進(jìn)行線程安全優(yōu)化。
- (void)initTicketStatusNotSave {
// 1. 設(shè)置剩余火車票為 50
self.ticketSurplusCount = 50;
// 2. 模擬窗口1售票的線程
self.ticketSaleWindow1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
self.ticketSaleWindow1.name = @"售票窗口1";
// 3. 模擬窗口2售票的線程
self.ticketSaleWindow2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
self.ticketSaleWindow2.name = @"售票窗口2";
// 4. 開始售賣火車票
[self.ticketSaleWindow1 start];
[self.ticketSaleWindow2 start];
}
/**
* 售賣火車票(線程安全)
*/
- (void)saleTicketNotSafe {
while (1) {
// 互斥鎖
@synchronized (self) {
//如果還有票,繼續(xù)售賣
if (self.ticketSurplusCount > 0) {
self.ticketSurplusCount --;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票數(shù):%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]);
[NSThread sleepForTimeInterval:0.2];
}
//如果已賣完,關(guān)閉售票窗口
else {
NSLog(@"所有火車票均已售完");
break;
}
}
}
}
運(yùn)行后結(jié)果是正常的,這里就不貼了。