多線程之NSThread

iOS中的多線程技術主要有NSThread, GCD和NSOperation。他們的封裝層次依次遞增,其中

NSThread封裝性最差,最偏向于底層,主要基于thread使用

GCD是基于C的API,直接使用比較方便,主要基于task使用

NSOperation是基于GCD封裝的NSObject對象,對于復雜的多線程項目使用比較方便,主要基于隊列使用

這篇文章是這個多線程系列的第一篇,主要介紹NSThread, NSThread是上面三項技術中唯一基于thread的,每一個NSThread對象代表著一個線程,理解NSThread更有利于理解多線程的含義

多線程的概念

曾經面試的時候被問到過什么是線程和進程?當時感覺自己似乎知道這是什么東西,但是比劃了半天就是說不上來
根據Apple官方的定義:

The term thread is used to refer to a separate path of execution for code.
The term process is used to refer to a running executable, which can encompass multiple threads.

線程用于指代一個獨立執行的代碼路徑
進程用于指代一個可執行程序,他可以包含多個線程

當一個可執行程序中擁有多個獨立執行的代碼路徑的時候,這就叫做多線程

111.jpg

NSThread API
線程創建
對于NSThread來說,每一個對象就代表著一個線程,NSThread提供了2種創建線程的方法:

+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);

detach方法直接創建并啟動一個線程去Selector,由于沒有返回值,如果需要獲取新創建的Thread,需要在執行的Selector中調用-[NSThread currentThread]獲取
init方法初始化線程并返回,線程的入口函數由Selector傳入。線程創建出來之后需要手動調用-start方法啟動

線程操作
創建好線程之后當然需要對線程進行操作,NSThread給線程提供的主要操作方法有啟動,睡眠,取消,退出

啟動
我們使用init方法將線程創建出來之后,線程并不會立即運行,只有我們手動調用-start方法才會啟動線程

- (void)start NS_AVAILABLE(10_5, 2_0);

這里要注意的是:部分線程屬性需要在啟動前設置,線程啟動之后再設置會無效。如qualityOfService屬性

睡眠
NSThread提供了2個讓線程睡眠的方法,一個是根據NSDate傳入睡眠時間,一個是直接傳入NSTimeInterval

+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

看到sleepUntilDate:大家可能會想起runloop的runUntilDate:。他們都有阻塞線程的效果,但是阻塞之后的行為又有不一樣的地方,使用的時候,我們需要根據具體需求選擇合適的API。

sleepUntilDate:相當于執行一個sleep的任務。在執行過程中,即使有其他任務傳入runloop,runloop也不會立即響應,必須sleep任務完成之后,才會響應其他任務

runUntilDate:雖然會阻塞線程,阻塞過程中并不妨礙新任務的執行。當有新任務的時候,會先執行接收到的新任務,新任務執行完之后,如果時間到了,再繼續執行runUntilDate:之后的代碼

取消
對于線程的取消,NSThread提供了一個取消的方法和一個屬性

@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);- (void)cancel NS_AVAILABLE(10_5, 2_0);

不過大家千萬不要被它的名字迷惑,調用-cancel方法并不會立刻取消線程,它僅僅是將cancelled屬性設置為YES。cancelled也僅僅是一個用于記錄狀態的屬性。線程取消的功能需要我們在main函數中自己實現
要實現取消的功能,我們需要自己在線程的main函數中定期檢查isCancelled狀態來判斷線程是否需要退出,當isCancelled為YES的時候,我們手動退出。如果我們沒有在main函數中檢查isCancelled狀態,那么調用-cancel將沒有任何意義

退出
與充滿不確定性的-cancel相比,-exit函數可以讓線程立即退出。

+ (void)exit;

-exit屬于核彈級別終極API,調用之后會立即終止線程,即使任務還沒有執行完成也會中斷。這就非常有可能導致內存泄露等嚴重問題,所以一般不推薦使用。

對于有runloop的線程,可以使用CFRunLoopStop()結束runloop配合-cancel結束線程
[2016.1.19更新]感謝@NSHYJ的提醒。
runloop啟動的方法中run和runUntilDate:都無法使用CFRunLoopStop()退出,
只有runMode:beforeDate:可以響應CFRunLoopStop(),所以要想使用CFRunLoopStop()退出runloop,必須使用runMode:beforeDate:啟動

線程通訊
線程準備好之后,經常需要從主線程把耗時的任務丟給輔助線程,當任務完成之后輔助線程再把結果傳回主線程傳,這些線程通訊一般用的都是perform方法

//①
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array; 
//②
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait; 
//③
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
//④
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);

①:將selector丟給主線程執行,可以指定runloop mode
②:將selector丟給主線程執行,runloop mode默認為common mode
③:將selector丟個指定線程執行,可以指定runloop mode
④:將selector丟個指定線程執行,runloop mode默認為default mode

所以我們一般用③④方法將任務丟給輔助線程,任務執行完成之后再使用①②方法將結果傳回主線程。

注意:perform方法只對擁有runloop的線程有效,如果創建的線程沒有添加runloop,perform的selector將無法執行。

線程優先級
每個線程的緊急程度是不一樣的,有的線程中任務你也許希望盡快執行,有的線程中任務也許并不是那么緊急,所以線程需要有優先級。優先級高線程中的任務會比優先級低的線程先執行
NSThread有4個優先級的API

+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
@property double threadPriority NS_AVAILABLE(10_6, 4_0); 
// To be deprecated; use qualityOfService below@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0); 
// read-only after the thread is started

前2個是類方法,用于設置和獲取當前線程的優先級
threadPriority屬性可以通過對象設置和獲取優先級
由于線程優先級是一個比較抽線的東西,沒人能知道0.5和0.6到底有多大區別,所以iOS8之后新增了qualityOfService枚舉屬性,大家可以通過枚舉值設置優先級

typedef NS_ENUM(NSInteger, NSQualityOfService)
 { NSQualityOfServiceUserInteractive = 0x21, 
   NSQualityOfServiceUserInitiated = 0x19, 
   NSQualityOfServiceDefault = -1 
   NSQualityOfServiceUtility = 0x11, 
   NSQualityOfServiceBackground = 0x09,
}

NSQualityOfService主要有5個枚舉值,優先級別從高到低排布:

NSQualityOfServiceUserInteractive:最高優先級,主要用于提供交互UI的操作,比如處理點擊事件,繪制圖像到屏幕上
NSQualityOfServiceUserInitiated:次高優先級,主要用于執行需要立即返回的任務
NSQualityOfServiceDefault:默認優先級,當沒有設置優先級的時候,線程默認優先級
NSQualityOfServiceUtility:普通優先級,主要用于不需要立即返回的任務
NSQualityOfServiceBackground:后臺優先級,用于完全不緊急的任務

一般主線程和沒有設置優先級的線程都是默認優先級
主線程和當前線程
NSThread也提供了非常方便的獲取和判斷主線程的API

@property (readonly) BOOL isMainThread NS_AVAILABLE(10_5, 2_0);
+ (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0); 
// reports whether current thread is main
+ (NSThread *)mainThread NS_AVAILABLE(10_5, 2_0);

isMainThread:判斷當前線程是否是主線程
mainThread:獲取主線程的thread

除了獲取主線程,我們也可以使用-currentThread獲取當前線程

+ (NSThread *)currentThread;

線程通知
NSThread有三個線程相關的通知

NSString * const NSWillBecomeMultiThreadedNotification;NSString * const NSDidBecomeSingleThreadedNotification;NSString * const NSThreadWillExitNotification;

NSWillBecomeMultiThreadedNotification:由當前線程派生出第一個其他線程時發送,一般一個線程只發送一次
NSDidBecomeSingleThreadedNotification:這個通知目前沒有實際意義,可以忽略
NSThreadWillExitNotification線程退出之前發送這個通知

NSThread實例
只看API畢竟比較抽象,下面我用一個例子給大家展示NSThread的使用方法

線程創建
我們首先來創建一個線程,并用self.thread持有,以便后面操作線程和線程通訊使用

self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];
 // ①創建線程
self.thread.qualityOfService = NSQualityOfServiceDefault;
 //②設置線程優先級
[self.thread start]; 
//③啟動線程

①:創建線程,并指定入口main函數為-threadMain

②:設置線程的優先級,qualityOfService屬性必須在線程啟動之前設置,啟動之后將無法再設置
③:調用start方法啟動線程。

由于線程的創建和銷毀非常消耗性能,大多情況下,我們需要復用一個長期運行的線程來執行任務。
在線程啟動之后會首先執行-threadMain,正常情況下threadMain方法執行結束之后,線程就會退出。為了線程可以長期復用接收消息,我們需要在threadMain中給thread添加runloop

- (void)threadMain { [[NSThread currentThread] setName:@"myThread"]; // ①給線程設置名字
 NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; // ②給線程添加runloop 
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; //③給runloop添加數據源 
while (![[NSThread currentThread] isCancelled]) {
 //④:檢查isCancelled
 [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; 
//⑤啟動runloop
 }
}

文/小笨狼(簡書作者)
原文鏈接:http://www.lxweimin.com/p/8ed06312d8bd
著作權歸作者所有,轉載請聯系作者獲得授權,并標注“簡書作者”。

①:設置線程的名字,這一步不是必須的,主要是為了debug的時候更方便,可以直接看出這是哪個線程
②:自定義的線程默認是沒有runloop的,調用-currentRunLoop,方法內部會為線程創建runloop
③:如果沒有數據源,runloop會在啟動之后會立刻退出。所以需要給runloop添加一個數據源,這里添加的是NSPort數據源
④:定期檢查isCancelled,當外部調用-cancel方法將isCancelled置為YES的時候,線程可以退出
⑤:啟動runloop

線程通訊
線程創建好了之后我們就可以給線程丟任務了,當我們有一個需要比較耗時的任務的時候,我們可以調用perform方法將task丟給這個線程。

[self performSelector:@selector(threadTask) onThread:self.thread withObject:nil waitUntilDone:NO]

結束線程
當我們想要結束線程的時候,我們可以使用CFRunLoopStop()配合-cancel來結束線程。

- (void)cancelThread{
 [[NSThread currentThread] cancel]; 
 CFRunLoopStop(CFRunLoopGetCurrent());
}

不過這個方法必須在self.thread線程下調用。如果當前是主線程。可以perform到self.thread下調用這個方法結束線程
Extension
runloop相關知識可以閱讀ibireme的深入理解RunLoop
NSThread使用實例可以閱讀AFNetWroking 2的源碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1、簡介:1.1 iOS有三種多線程編程的技術,分別是:1.、NSThread2、Cocoa NSOperatio...
    LuckTime閱讀 1,374評論 0 1
  • 開啟線程 分離主線程創建:創建線程后會自動執行,但是線程外部不可獲取到該線程對象detachNewThreadWi...
    Mr_Pt閱讀 1,094評論 0 1
  • Pthread 使用pthread必須盜用頭文件#import <pthread.h> 可以使用[NSThread...
    是我始終拒絕成長嗎閱讀 708評論 0 0
  • iOS多線程開發基礎概念 進程 VS 線程 進程:程序的一次執行,是正在執行的程序的實例,它是Unix的一個基本概...
    qingmarch閱讀 387評論 0 1
  • 斷斷續續讀完《橋上的孩子》,有種不夠暢快之感。閱讀的過程中時而精彩引人,時而又有些啰嗦有些俗氣,令人立即“跳戲”。...
    Julia影閱讀 515評論 0 2