NSThead的進階使用和簡單探討

雅之道法自然發表於2018-11-15

NSThead的進階使用和簡單探討

概述

NSThread類是一個繼承於NSObjct類的輕量級類。一個NSThread物件就代表一個執行緒。它需要管理執行緒的生命週期、同步、加鎖等問題,因此會產生一定的效能開銷。 使用NSThread類可以在特定的執行緒中被呼叫某個OC方法。當需要執行一個冗長的任務,並且不想讓這個任務阻塞應用中的其他部分,尤其為了避免阻塞app的主執行緒(因為主執行緒用於處理使用者介面展示互動和事件相關的操作),這個時候非常適合使用多執行緒。執行緒也可以將一個龐大的任務分為幾個較小的任務,從而提高多核計算機的效能。

NSThread類在執行期監聽一個執行緒的語義和NSOperation類是相似的。比如取消一個執行緒或者決定一個任務執行完後這個執行緒是否存在。

本文將會從這幾個方面開始探討NSThread

NSThead的進階使用和簡單探討
方法屬性的介紹

初始化(建立)一個NSThread物件

// 返回一個初始化的NSThread物件
- (instancetype)init
// 返回一個帶有多個引數的初始化的NSThread物件
// selector :執行緒執行的方法,最多隻能接收一個引數
// target :selector訊息傳送的物件
// argument : 傳給selector的唯一引數,也可以是nil
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument );
// iOS 10
- (instancetype)initWithBlock:(void (^)(void))block;
複製程式碼

啟動一個執行緒。

// 開闢一個新的執行緒,並且使用特殊的選擇器Selector作為執行緒入口,呼叫完畢後,會馬上建立並開啟新執行緒
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
 // iOS 10
+ (void)detachNewThreadWithBlock:(void (^)(void))block;
// 啟動接受者
- (void)start;
// 執行緒體方法,執行緒主要入口,start 後執行
// 該方法預設實現了目標(target)和選擇器(selector),用於初始化接受者和呼叫指定目標(target)的方法。如果子類化NSThread,需要重寫這個方法並且用它來實現這個執行緒主體。在這種情況下,是不需要呼叫super方法的。
// 不應該直接呼叫這個方法。你應該通過呼叫啟動方法開啟一個執行緒。
- (void)main;
複製程式碼

使用initWithTarget:selector:initWithBlock:detachNewThreadSelector:detachNewThreadWithBlock:建立執行緒都是非同步執行緒。

停止一個執行緒

// 阻塞當前執行緒,直到特定的時間。
+ (void)sleepUntilDate:(NSDate *)date;
// 讓執行緒處於休眠狀態,直到經過給定的時間間隔
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 終止當前執行緒
+ (void)exit;
// 改變接收者的取消狀態,來表示它應該終止
- (void)cancel;
複製程式碼

決定執行緒狀態

// 接收者是否存在
@property (readonly, getter=isExecuting) BOOL executing;
// 接收者是否結束執行
@property (readonly, getter=isFinished) BOOL finished;
// 接收者是否取消
@property (readonly, getter=isCancelled) BOOL cancelled;
複製程式碼

主執行緒相關

// 當前執行緒是否是主執行緒
@property (class, readonly) BOOL isMainThread;
// 接受者是否是主執行緒
@property (readonly) BOOL isMainThread;
// 獲取主執行緒的物件
@property (class, readonly, strong) NSThread *mainThread;
複製程式碼

執行環境

// 這個app是否是多執行緒
+ (BOOL)isMultiThreaded;
// 返回當前執行執行緒的執行緒物件。
@property (class, readonly, strong) NSThread *currentThread;
// 返回一個陣列,包括回撥堆疊返回的地址
@property (class, readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses ;
// 返回一個陣列,包括回撥堆疊訊號
@property (class, readonly, copy) NSArray<NSString *> *callStackSymbols;
複製程式碼

執行緒屬性相關

// 執行緒物件的字典
@property (readonly, retain) NSMutableDictionary *threadDictionary;

NSAssertionHandlerKey
// 接收者的名字
@property (nullable, copy) NSString *name;
// 接收者的物件大小,以byte為單位
@property NSUInteger stackSize;
複製程式碼

執行緒優先順序

// 執行緒開啟後是個只讀屬性
@property NSQualityOfService qualityOfService;
// 返回當前執行緒的優先順序
+ (double)threadPriority;
// 接受者的優先順序,已經廢棄,使用qualityOfService代替
@property double threadPriority;
// 設定當前執行緒的優先順序。設定執行緒的優先順序(0.0 - 1.0,1.0最高階)
+ (BOOL)setThreadPriority:(double)p;
複製程式碼

通知

// 未被實現,沒有實際意義,保留項
NSDidBecomeSingleThreadedNotification
// 線上程退出前,一個NSThread物件收到到退出訊息時會傳送這個通知。
NSThreadWillExitNotification
// 當第一個執行緒啟動時會傳送這個通知。這個通知最多傳送一次。當NSThread第一次傳送用`detachNewThreadSelector:toTarget:withObject:`,`detachNewThreadWithBlock:`,`start`訊息時,傳送通知。後續呼叫這些方法是不會傳送通知。
NSWillBecomeMultiThreadedNotification
複製程式碼

執行緒間通訊, 在NSObject的分類NSThreadPerformAdditions中的方法(NSThread.h檔案中)具有這些特性:

  1. 無論是在主執行緒還是在子執行緒中都可執行,並且均會呼叫主執行緒的aSelector方法;
  2. 方法是非同步的
@interface NSObject (NSThreadPerformAdditions)
// 如果設定wait為YES: 等待當前執行緒執行完以後,主執行緒才會執行aSelector方法;
// 如果設定wait為NO:不等待當前執行緒執行完,就在主執行緒上執行aSelector方法。
// 如果,當前執行緒就是主執行緒,那麼aSelector方法會馬上執行,wait是YES引數無效。
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;

// 等於第一個方法中modes是kCFRunLoopCommonModes的情況。指定了執行緒中 Runloop 的 Modes =  kCFRunLoopCommonModes。
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;

// 在指定執行緒上操作,因為子執行緒預設未新增NSRunloop,線上程未新增runloop時,是不會呼叫選擇器中的方法的。
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:( NSArray<NSString *> *)array ;
// 等於第一個方法中modes是kCFRunLoopCommonModes的情況。
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait ;

// 隱式建立子執行緒,在後臺建立。並且是個同步執行緒。
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg ;
@end
複製程式碼

直接給接受者發訊息的其他方法。

  1. 協議NSObject中的方法,可在主執行緒或者子執行緒執行。因為是在當前執行緒執行的同步任務,因此會阻塞當前執行緒。這幾個方法等同於直接呼叫方法。
// 當前執行緒操作。
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
複製程式碼
  1. 延遲操作&按照順序操作

NSRunLoop.h檔案中

// 延遲操作
/**************** 	Delayed perform	 ******************/

@interface NSObject (NSDelayedPerforming)
// 非同步方法,不會阻塞當前執行緒,只能在主執行緒中執行。是把`Selector`加到主佇列裡,當 `delay`之後執行`Selector`。如果主執行緒在執行業務,那隻能等到執行完所有業務之後才會去執行`Selector`,就算`delay`等於 0。
// 那`delay `從什麼時候開始計算呢?從傳送`performSelector`訊息的時候。就算這時主執行緒在阻塞也會計算時間,當阻塞結束之後,如果到了`delay`那就執行`Selector`,如果沒到就繼續 `delay`。
// 只能在主執行緒中執行,在子執行緒中不會調到aSelector方法
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
// 等於第一個方法中modes是kCFRunLoopCommonModes的情況。指定了執行緒中 Runloop 的 Modes =  kCFRunLoopCommonModes。
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
// 在方法未到執行時間之前,取消方法。呼叫這2個方法當前target執行dealloc之前,以確保不會Crash。
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;

@end
// 按照排序順序執行
@interface NSRunLoop (NSOrderedPerform)
// 按某種順序order執行方法。引數order越小,優先順序越高,執行越早
// selector都是target的方法,argument都是target的引數
// 這2個方法會設定一個定時器去在下個runloop迴圈的開始時讓target執行aSelector訊息。 定時器根據modes確認模式。當定時器觸發,定時器嘗試佇列從runloop中拿出訊息並執行。
如果run loop 正在執行,並且是指定modes的一種,則是成功的,否則定時器一直等待直到runloop是modes 中的一種。
- (void)performSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg order:(NSUInteger)order modes:(NSArray<NSRunLoopMode> *)modes;
- (void)cancelPerformSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg;
- (void)cancelPerformSelectorsWithTarget:(id)target;

@end
複製程式碼

本文介紹大部分的知識點如思維導圖:

NSThead的進階使用和簡單探討

使用

  1. 建立執行緒 用initXXX初始化的需要呼叫start方法來啟動執行緒。而detachXXX初始化方法,直接啟動執行緒。這個2中方式建立的執行緒都是顯式建立執行緒。
//1. 手動開啟,action-target 方式
NSThread * actionTargetThread = [[NSThread alloc] initWithTarget:self selector:@selector(add:) object:nil];
[actionTargetThread start];
//2. 手動開啟, block方式
NSThread *blockThread = [[NSThread alloc] initWithBlock:^{
    NSLog(@"%s",__func__);
}];
[blockThread start];
//3. 建立就啟動, action-target方式
[NSThread detachNewThreadSelector:@selector(add2:) toTarget:self withObject:@"detachNewThreadSelector"];
//4. 建立就啟動, block 方式
[NSThread detachNewThreadWithBlock:^{
    NSLog(@"%s",__func__);
}];
複製程式碼
  1. 執行緒中通訊

2.1 NSThreadPerformAdditions分類方法,非同步呼叫方法 // 無論在子執行緒還是主執行緒,都會呼叫主執行緒方法。

a. 主執行緒

    [self performSelectorOnMainThread:@selector(add:) withObject:nil waitUntilDone:YES];
    //[self performSelectorOnMainThread:@selector(add:) withObject:@"arg" waitUntilDone:YES modes:@[(NSRunLoopMode)kCFRunLoopDefaultMode]];
複製程式碼

子執行緒預設沒有開啟runloop。需要手動新增,不然選擇器方法無法呼叫。

b. 子執行緒

使用initWithBlock:方式建立。

//1. 開闢一個子執行緒
NSThread *subThread1 = [[NSThread alloc] initWithBlock:^{
  // 2.子執行緒方法中新增runloop
  // 3.實現執行緒方法
    [[NSRunLoop currentRunLoop] run];
}];
//1.2. 啟動一個子執行緒
[subThread1 start];
// 2. 在子執行緒中呼叫方法
// [self performSelector:@selector(add:) onThread:subThread1 withObject:@"22" waitUntilDone:YES];
[self performSelector:@selector(add:) onThread:subThread1 withObject:@"arg" waitUntilDone:YES modes:@[(NSRunLoopMode)kCFRunLoopDefaultMode]];
複製程式碼

使用initWithTarget:selector:object:建立。

// 1. 開闢一個子執行緒
NSThread *subThread2 = [[NSThread alloc] initWithTarget:self selector:@selector(startThread) object:nil];
// 1.2 啟動一個子執行緒
[subThread2 start];
// 3. 在子執行緒中呼叫方法
// [self performSelector:@selector(add:) onThread:subThread2 withObject:@"22" waitUntilDone:YES];
[self performSelector:@selector(add:) onThread:subThread1 withObject:@"arg" waitUntilDone:YES modes:@[(NSRunLoopMode)kCFRunLoopDefaultMode]];
// 2.子執行緒方法中新增runloop
- (void)startThread{
    [[NSRunLoop currentRunLoop] run];
}
複製程式碼

c. 後臺執行緒(隱式建立一個執行緒)

[self performSelectorInBackground:@selector(add:) withObject:@"arg"];
複製程式碼

2.2 協議NSObject方法 建立是的同步任務。

[NSThread detachNewThreadWithBlock:^{
    // 直接呼叫
    [self performSelector:@selector(add:) withObject:@"xxx"];
}];
複製程式碼

2.3 延遲 NSObject分類NSDelayedPerforming方法,新增非同步任務,並且是在主執行緒上執行。

[self performSelector:@selector(add:) withObject:self afterDelay:2];
複製程式碼

2.4 按照順序操作 NSRunLoop分類NSOrderedPerform中的方法

[NSThread detachNewThreadWithBlock:^{
    NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];
    // 記得新增埠。不然無法呼叫selector方法
    [currentRunloop addPort:[NSPort port] forMode:(NSRunLoopMode)kCFRunLoopCommonModes];
    [currentRunloop performSelector:@selector(add:) target:self argument:@"arg1" order:1 modes:@[(NSRunLoopMode)kCFRunLoopDefaultMode]];
    [currentRunloop performSelector:@selector(add:) target:self argument:@"arg3" order:3 modes:@[(NSRunLoopMode)kCFRunLoopDefaultMode]];
    [currentRunloop run];
}];
複製程式碼

執行緒安全

問題:

多個執行緒可能會同時訪問同一塊資源。比如多個執行緒同時訪問同一個物件、同一個變數、同一個檔案等。當多個執行緒同時搶奪同一個資源,會引起執行緒不安全性,可能會造成資料錯亂和資料安全問題。

解決:

使用執行緒同步技術: 可以對可能會被搶奪的資源,在被被競爭的時候加鎖。讓其保證執行緒同步狀態。而鎖具有多種型別:比如讀寫鎖、自旋鎖、互斥鎖、訊號量、條件鎖等。在NSThread可能造成資源搶奪情況下,可以使用互斥鎖。互斥鎖就是多個執行緒任務按順序的執行。 如下就使用的情況之一:對需要讀寫操作的資源,進行加鎖操作。

for (NSInteger index = 0 ; index < 100; index ++) {
    @synchronized (self) {
        self.allCount -= 5;
        NSLog(@"%@賣出了車票,還剩%ld",[NSThread currentThread].name,self.allCount);
    }
}
複製程式碼

執行緒生命週期。

執行緒的生命週期是:新建 - 就緒 - 執行 - 阻塞 - 死亡。當執行緒啟動後,它不能一直“霸佔”著CPU獨自執行,所以CPU需要在多條執行緒之間切換,於是執行緒狀態也就會隨之改變。

NSThead的進階使用和簡單探討

  1. 新建和就緒狀態 顯式建立,使用initWithTarget:selector:initWithBlock:建立一個執行緒,未啟動,只有傳送start訊息才會啟動,然後處於就行狀態。 使用detachNewThreadWithBlock:detachNewThreadSelector:toTarget:顯示建立並立即啟動。 還有種建立方式,隱式建立並立即啟動:performSelectorInBackground:withObject:

  2. 執行和阻塞狀態 如果處於就緒狀態的執行緒獲得了CPU資源,開始執行可執行方法的執行緒執行體(block或者@Selector),則該執行緒處於執行狀態。

當發生如下情況下,執行緒將會進入阻塞狀態:

  • 執行緒呼叫sleep方法:sleepUntilDate: sleepForTimeInterval:主動放棄所佔用的處理器資源。
  • 執行緒呼叫了一個阻塞式IO方法,在該方法返回之前,該執行緒被阻塞。 執行緒試圖獲得一個同步監視器,但該同步監視器正被其他執行緒鎖持有。
  • 執行緒在等待某個通知(notify)。
  • 程式呼叫了執行緒的suspend方法將該執行緒掛起。不過這個方法容易導致死鎖,所以程式應該儘量避免使用該方法。 當前正在執行的執行緒被阻塞之後,其他執行緒就可以獲得執行的機會了。被阻塞的執行緒會在合適時候重新進入就緒狀態,注意是就緒狀態而不是執行狀態。也就是 說被阻塞執行緒的阻塞解除後,必須重新等待執行緒排程器再次排程它。 針對上面的幾種情況,當發生如下特定的情況將可以解除上面的阻塞,讓該執行緒重新進入就緒狀態:
  • 呼叫sleep方法的執行緒經過了指定時間。
  • 執行緒呼叫的阻塞式IO方法已經返回。
  • 執行緒成功地獲得了試圖取得同步監視器。
  • 執行緒正在等待某個通知時,其他執行緒發出了一個通知。
  • 處於掛起狀態的執行緒被呼叫了resume恢復方法。
  1. 執行緒死亡
  • 可執行方法執行完成,執行緒正常結束。
  • 程式的意外奔潰。
  • 該執行緒的傳送exit訊息來結束該執行緒。
// 1. 建立:New狀態
NSThread * actionTargetThread = [[NSThread alloc] initWithTarget:self selector:@selector(add:) object:nil];
// 2. 啟動:就緒狀態
[actionTargetThread start];
// 可執行方法
- (void)add:(id)info{
    // 3. 執行狀態
    NSLog(@"%s,info %@",__func__,info);
    // 5. 當前執行緒休眠
    [NSThread sleepForTimeInterval:1.0];
    NSLog(@"after");
    // 4. 程式正常退出
}
// 6. 打取消標籤
[actionTargetThread cancel];
// 7. 主動退出
[NSThread exit];
複製程式碼

注意:

  • NSThread 管理多個執行緒比較困難,所以不太推薦在多執行緒任務多的情況下使用。
  • 蘋果官方推薦使用GCD和NSOperation。
  • [NSTread currentThread] 跟蹤任務所線上程,適用於NSTread,NSOperation,和GCD
  • 用NSThread建立的執行緒,不會自動新增autoreleasepool

參考

相關文章