參考:http://ios.jobbole.com/83804/
多線程是程式開發中非常基礎的一個概念,大家在開發過程中應該或多或少用過相關的東西。同時這恰恰又是一個比較棘手的概念,一切跟多線程挂鈎的東西都會變得複雜。如果使用過程中對多線程不夠熟悉,很可能會埋下一些難以預料的坑。
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.
- 線程用于指代一個獨立執行的代碼路徑
- 程序用于指代一個可執行程式,他可以包含多個線程
當一個可執行程式中擁有多個獨立執行的代碼路徑的時候,這就叫做多線程
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。
-
相當于執行一個sleep的任務。在執行過程中,即使有其他任務傳入runloop,runloop也不會立即響應,必須sleep任務完成之後,才會響應其他任務sleepUntilDate:
-
雖然會阻塞線程,阻塞過程中并不妨礙新任務的執行。當有新任務的時候,會先執行接收到的新任務,新任務執行完之後,如果時間到了,再繼續執行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的線程,可以使用結束runloop配合
CFRunLoopStop()
-cancel
結束線程
[2016.1.19更新] 感謝@NSHYJ的提醒。runloop啟動的方法中
和
run
都無法使用
runUntilDate:
退出,隻有
CFRunLoopStop()
可以響應
runMode:beforeDate:
,是以要想使用
CFRunLoopStop()
退出runloop,必須使用
CFRunLoopStop()
啟動
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
}
}
- ①:設定線程的名字,這一步不是必須的,主要是為了debug的時候更友善,可以直接看出這是哪個線程
- ②:自定義的線程預設是沒有runloop的,調用
,方法内部會為線程建立runloop-currentRunLoop
- ③:如果沒有資料源,runloop會在啟動之後會立刻退出。是以需要給runloop添加一個資料源,這裡添加的是NSPort資料源
- ④:定期檢查isCancelled,當外部調用
方法将isCancelled置為YES的時候,線程可以退出-cancel
- ⑤:啟動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下調用這個方法結束線程