小笨狼漫談多線程:NSThread

多線程是程序開發(fā)中非?;A(chǔ)的一個概念,大家在開發(fā)過程中應(yīng)該或多或少用過相關(guān)的東西。同時這恰恰又是一個比較棘手的概念,一切跟多線程掛鉤的東西都會變得復(fù)雜。如果使用過程中對多線程不夠熟悉,很可能會埋下一些難以預(yù)料的坑。

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

  • NSThread封裝性最差,最偏向于底層,主要基于thread使用
  • GCD是基于C的API,直接使用比較方便,主要基于task使用
  • NSOperation是基于GCD封裝的NSObject對象,對于復(fù)雜的多線程項目使用比較方便,主要基于隊列使用

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

多線程的概念

曾經(jīng)面試的時候被問到過什么是線程和進(jìn)程?當(dāng)時感覺自己似乎知道這是什么東西,但是比劃了半天就是說不上來

根據(jù)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.
  • 線程用于指代一個獨立執(zhí)行的代碼路徑
  • 進(jìn)程用于指代一個可執(zhí)行程序,他可以包含多個線程

當(dāng)一個可執(zhí)行程序中擁有多個獨立執(zhí)行的代碼路徑的時候,這就叫做多線程


NSThread API

線程創(chuàng)建

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

+ (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方法直接創(chuàng)建并啟動一個線程去Selector,由于沒有返回值,如果需要獲取新創(chuàng)建的Thread,需要在執(zhí)行的Selector中調(diào)用-[NSThread currentThread]獲取
  • init方法初始化線程并返回,線程的入口函數(shù)由Selector傳入。線程創(chuàng)建出來之后需要手動調(diào)用-start方法啟動

線程操作

創(chuàng)建好線程之后當(dāng)然需要對線程進(jìn)行操作,NSThread給線程提供的主要操作方法有啟動,睡眠,取消,退出

啟動

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

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

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

睡眠

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

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

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

  • sleepUntilDate:相當(dāng)于執(zhí)行一個sleep的任務(wù)。在執(zhí)行過程中,即使有其他任務(wù)傳入runloop,runloop也不會立即響應(yīng),必須sleep任務(wù)完成之后,才會響應(yīng)其他任務(wù)
  • runUntilDate:雖然會阻塞線程,阻塞過程中并不妨礙新任務(wù)的執(zhí)行。當(dāng)有新任務(wù)的時候,會先執(zhí)行接收到的新任務(wù),新任務(wù)執(zhí)行完之后,如果時間到了,再繼續(xù)執(zhí)行runUntilDate:之后的代碼

取消

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

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

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

不過大家千萬不要被它的名字迷惑,調(diào)用-cancel方法并不會立刻取消線程,它僅僅是將cancelled屬性設(shè)置為YES。cancelled也僅僅是一個用于記錄狀態(tài)的屬性。線程取消的功能需要我們在main函數(shù)中自己實現(xiàn)

要實現(xiàn)取消的功能,我們需要自己在線程的main函數(shù)中定期檢查isCancelled狀態(tài)來判斷線程是否需要退出,當(dāng)isCancelled為YES的時候,我們手動退出。如果我們沒有在main函數(shù)中檢查isCancelled狀態(tài),那么調(diào)用-cancel將沒有任何意義

退出

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

+ (void)exit;

-exit屬于核彈級別終極API,調(diào)用之后會立即終止線程,即使任務(wù)還沒有執(zhí)行完成也會中斷。這就非常有可能導(dǎo)致內(nèi)存泄露等嚴(yán)重問題,所以一般不推薦使用。

對于有runloop的線程,可以使用CFRunLoopStop()結(jié)束runloop配合-cancel結(jié)束線程

[2016.1.19更新]
感謝@NSHYJ的提醒。runloop啟動的方法中runrunUntilDate:都無法使用CFRunLoopStop()退出,只有runMode:beforeDate:可以響應(yīng)CFRunLoopStop(),所以要想使用CFRunLoopStop()退出runloop,必須使用runMode:beforeDate:啟動

線程通訊

線程準(zhǔn)備好之后,經(jīng)常需要從主線程把耗時的任務(wù)丟給輔助線程,當(dāng)任務(wù)完成之后輔助線程再把結(jié)果傳回主線程傳,這些線程通訊一般用的都是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丟給主線程執(zhí)行,可以指定runloop mode
  • ②:將selector丟給主線程執(zhí)行,runloop mode默認(rèn)為common mode
  • ③:將selector丟個指定線程執(zhí)行,可以指定runloop mode
  • ④:將selector丟個指定線程執(zhí)行,runloop mode默認(rèn)為default mode

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

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

線程優(yōu)先級

每個線程的緊急程度是不一樣的,有的線程中任務(wù)你也許希望盡快執(zhí)行,有的線程中任務(wù)也許并不是那么緊急,所以線程需要有優(yōu)先級。優(yōu)先級高線程中的任務(wù)會比優(yōu)先級低的線程先執(zhí)行

NSThread有4個優(yōu)先級的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個是類方法,用于設(shè)置和獲取當(dāng)前線程的優(yōu)先級
  • threadPriority屬性可以通過對象設(shè)置和獲取優(yōu)先級
  • 由于線程優(yōu)先級是一個比較抽線的東西,沒人能知道0.5和0.6到底有多大區(qū)別,所以iOS8之后新增了qualityOfService枚舉屬性,大家可以通過枚舉值設(shè)置優(yōu)先級
typedef NS_ENUM(NSInteger, NSQualityOfService) {
    NSQualityOfServiceUserInteractive = 0x21,
    NSQualityOfServiceUserInitiated = 0x19,
    NSQualityOfServiceDefault = -1
    NSQualityOfServiceUtility = 0x11,
    NSQualityOfServiceBackground = 0x09,
}

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

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

一般主線程和沒有設(shè)置優(yōu)先級的線程都是默認(rèn)優(yōu)先級

主線程和當(dāng)前線程

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:判斷當(dāng)前線程是否是主線程
  • mainThread:獲取主線程的thread

除了獲取主線程,我們也可以使用-currentThread獲取當(dāng)前線程

+ (NSThread *)currentThread;

線程通知

NSThread有三個線程相關(guān)的通知

NSString * const NSWillBecomeMultiThreadedNotification;
NSString * const NSDidBecomeSingleThreadedNotification;
NSString * const NSThreadWillExitNotification;
  • NSWillBecomeMultiThreadedNotification:由當(dāng)前線程派生出第一個其他線程時發(fā)送,一般一個線程只發(fā)送一次
  • NSDidBecomeSingleThreadedNotification:這個通知目前沒有實際意義,可以忽略
  • NSThreadWillExitNotification線程退出之前發(fā)送這個通知

NSThread實例

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

線程創(chuàng)建

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

self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil]; // ①創(chuàng)建線程
self.thread.qualityOfService = NSQualityOfServiceDefault; //②設(shè)置線程優(yōu)先級
[self.thread start]; //③啟動線程
  • ①:創(chuàng)建線程,并指定入口main函數(shù)為-threadMain
  • ②:設(shè)置線程的優(yōu)先級,qualityOfService屬性必須在線程啟動之前設(shè)置,啟動之后將無法再設(shè)置
  • ③:調(diào)用start方法啟動線程。

由于線程的創(chuàng)建和銷毀非常消耗性能,大多情況下,我們需要復(fù)用一個長期運行的線程來執(zhí)行任務(wù)。

在線程啟動之后會首先執(zhí)行-threadMain,正常情況下threadMain方法執(zhí)行結(jié)束之后,線程就會退出。為了線程可以長期復(fù)用接收消息,我們需要在threadMain中給thread添加runloop

- (void)threadMain {
    [[NSThread currentThread] setName:@"myThread"]; // ①給線程設(shè)置名字
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];    // ②給線程添加runloop
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];   //③給runloop添加數(shù)據(jù)源
    while (![[NSThread currentThread] isCancelled]) {           //④:檢查isCancelled
        [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];  //⑤啟動runloop
    }
}
  • ①:設(shè)置線程的名字,這一步不是必須的,主要是為了debug的時候更方便,可以直接看出這是哪個線程
  • ②:自定義的線程默認(rèn)是沒有runloop的,調(diào)用-currentRunLoop,方法內(nèi)部會為線程創(chuàng)建runloop
  • ③:如果沒有數(shù)據(jù)源,runloop會在啟動之后會立刻退出。所以需要給runloop添加一個數(shù)據(jù)源,這里添加的是NSPort數(shù)據(jù)源
  • ④:定期檢查isCancelled,當(dāng)外部調(diào)用-cancel方法將isCancelled置為YES的時候,線程可以退出
  • ⑤:啟動runloop

線程通訊

線程創(chuàng)建好了之后我們就可以給線程丟任務(wù)了,當(dāng)我們有一個需要比較耗時的任務(wù)的時候,我們可以調(diào)用perform方法將task丟給這個線程。

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

結(jié)束線程

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

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

不過這個方法必須在self.thread線程下調(diào)用。如果當(dāng)前是主線程??梢詐erform到self.thread下調(diào)用這個方法結(jié)束線程

Extension

About Me

個人博客 簡書 微博 QQ群:159974494

歡迎大家關(guān)注我,共同學(xué)習(xí)iOS,如果你覺得我寫得不錯,可以打賞我5毛錢,哈哈~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 從哪說起呢? 單純講多線程編程真的不知道從哪下嘴。。 不如我直接引用一個最簡單的問題,以這個作為切入點好了 在ma...
    Mr_Baymax閱讀 2,911評論 1 17
  • Object C中創(chuàng)建線程的方法是什么?如果在主線程中執(zhí)行代碼,方法是什么?如果想延時執(zhí)行代碼、方法又是什么? 1...
    AlanGe閱讀 1,919評論 0 17
  • 本文選譯自《Threading Programming Guide》。 導(dǎo)語 線程技術(shù)作為在單個應(yīng)用程序中并發(fā)執(zhí)行...
    巧巧的二表哥閱讀 2,591評論 4 24
  • (一) 他被幾個人念著一,二,三小心翼翼抬下了移動鐵床,安置在病床上。-喉嚨上,鼻上,手腕,下腹部,尿道上,插滿了...
    我叫她小胖子閱讀 932評論 0 0
  • 許久,父親吐出一串煙圈,然后緩緩掐滅煙頭,接著,又慢慢點燃一支,深深地吸了一口。殘陽透過窗戶,照在父親蒼老的臉上,...
    天鐸1916閱讀 353評論 1 2

友情鏈接更多精彩內(nèi)容