天天看點

多線程之NSThead的進階使用和探讨

多線程之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

參考

  • nsthread apple 文檔
  • 線程的生命周期
  • 線程的生命周期及五種基本狀态