iOS 多線程: NSThread的介紹

NSThread的封裝性在多線程中是最差的, 最偏向于底層, 主要基于thread使用

一: 多線程的概念:

線程: 指一個獨立執(zhí)行的代碼路徑(比如: 一個工作的人)
進(jìn)程: 指一個可執(zhí)行程序, 它可以包含多個線程(比如: 一個運行的部門)
當(dāng)一個可執(zhí)行程序中擁有多個獨立執(zhí)行的代碼路徑的時候, 這就叫做多線程

二: 線程的創(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;

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

三: 線程的操作:

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

1: 啟動

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

- (void)start;

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

2: 睡眠

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

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

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

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

3: 取消

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

@property (readonly, getter=isCancelled) BOOL cancelled;

- (void)cancel;

不過大家千萬不要被它的名字迷惑, 調(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將沒有任何意義.

示例:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
//開啟線程
[thread start];
//睡眠3秒
[NSThread sleepForTimeInterval:3];
//取消線程
[thread cancel];
//判斷是否已經(jīng)取消
if (thread.isCancelled) {
    CCLog(@"線程已經(jīng)取消");
}
else {
    CCLog(@"線程沒有取消");
}

- (void)demo {
    CCLog(@"1-----");
    for (int i = 0; i < 5; i++) {
        //睡眠1秒
        [NSThread sleepForTimeInterval:1];
        CCLog(@"第%d個索引",i);
    }
    //判斷是否已經(jīng)取消
    if (NSThread.currentThread.isCancelled) {
        return;
    }
    CCLog(@"2-----");
}


輸出結(jié)果:
1——
第0個索引
第1個索引
線程已經(jīng)取消
第2個索引
第3個索引
第4個索引

可以看到:在取消子線程之后, 子線程仍然在執(zhí)行, 直到判斷子線程已經(jīng)取消后, 我們手動終止線程, 子線程才停止

4: 退出

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

+ (void)exit;

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

示例:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
//開啟線程
[thread start];

- (void)demo {
    CCLog(@"1-----");
    for (int i = 0; i < 5; i++) {
        //睡眠1秒
        [NSThread sleepForTimeInterval:1];
        if (i == 3) {
            [NSThread exit];
        }
        CCLog(@"第%d個索引",i);
    }
    CCLog(@"2-----");
}

輸出:
1——
第0個索引
第1個索引
第2個索引

四: 線程通訊

線程準(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 API_AV;
④
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

①: 將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ù)也不并不是那么緊急, 這時就需要設(shè)置優(yōu)先級, NSThread有4個優(yōu)先級的API

+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
@property double threadPriority;
@property NSQualityOfService qualityOfService;

前兩個是類方法, 用于設(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,  
    NSQualityOfServiceUtility = 0x11,         
    NSQualityOfServiceBackground = 0x09,
    NSQualityOfServiceDefault = -1
};

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

  • NSQualityOfServiceUserInteractive: 最高優(yōu)先級, 主要用于直接參與提供交互式UI的工作, 例如: 處理點擊事件或繪制圖像到屏幕上
  • NSQualityOfServiceUserInitiated: 次高優(yōu)先級, 用于執(zhí)行用戶明確請求的工作, 并且必須立即顯示結(jié)果, 以便進(jìn)行進(jìn)一步的用戶交互
  • 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;
@property (class, readonly) BOOL isMainThread;
@property (class, readonly, strong) NSThread *mainThread;
  • isMainThread: 判斷當(dāng)前線程是否是主線程;
  • mainThread: 獲取主線程的thread

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

@property (class, readonly, strong) NSThread *currentThread;

七: 線程通知:

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

FOUNDATION_EXPORT NSNotificationName const NSWillBecomeMultiThreadedNotification;
FOUNDATION_EXPORT NSNotificationName const NSDidBecomeSingleThreadedNotification;
FOUNDATION_EXPORT NSNotificationName const NSThreadWillExitNotification;

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

八: NSThread實例:

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

1: 線程的創(chuàng)建:

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

//①創(chuàng)建線程
self.thred = [NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];
//②設(shè)置線程的優(yōu)先級
self.thread.qualityOfService = NSQualityOfServiceDefault;
//③啟動線程
[self.thread start];

①: 創(chuàng)建線程: 并指定入口main函數(shù)為-threadMain
②: 設(shè)置線程的優(yōu)先級: qualityOfService屬性必須在線程啟動之前設(shè)置, 啟動之后將無法再設(shè)置
③: 調(diào)用start方法啟動線程

由于線程的創(chuàng)建和銷毀非常消耗性能, 大多數(shù)情況下, 我們需要復(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 runUntilDate:[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)用-cabncel方法將isCancelled設(shè)置為YES的時候, 線程可以退出;
⑤: 啟動runloop

2: 線程通訊:

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

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

3: 結(jié)束線程:

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

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

不過這個方法必須在self.thread線程下調(diào)用, 如果當(dāng)前是主線程, 可以perform到self.thread下調(diào)用這個方法結(jié)束線程;

原文:https://www.open-open.com/lib/view/open1452993005808.html#articleHeader14

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末韧献,一起剝皮案震驚了整個濱河市衣陶,隨后出現(xiàn)的幾起案子兑凿,更是在濱河造成了極大的恐慌盏筐,老刑警劉巖旺遮,帶你破解...
    沈念sama閱讀 211,423評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捞附,死亡現(xiàn)場離奇詭異铺遂,居然都是意外死亡主穗,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,147評論 2 385
  • 文/潘曉璐 我一進(jìn)店門堤如,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蒲列,“玉大人,你說我怎么就攤上這事搀罢』柔” “怎么了?”我有些...
    開封第一講書人閱讀 157,019評論 0 348
  • 文/不壞的土叔 我叫張陵榔至,是天一觀的道長抵赢。 經(jīng)常有香客問我,道長洛退,這世上最難降的妖魔是什么瓣俯? 我笑而不...
    開封第一講書人閱讀 56,443評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮兵怯,結(jié)果婚禮上彩匕,老公的妹妹穿的比我還像新娘。我一直安慰自己媒区,他們只是感情好驼仪,可當(dāng)我...
    茶點故事閱讀 65,535評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著袜漩,像睡著了一般绪爸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上宙攻,一...
    開封第一講書人閱讀 49,798評論 1 290
  • 那天奠货,我揣著相機(jī)與錄音,去河邊找鬼座掘。 笑死递惋,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的溢陪。 我是一名探鬼主播萍虽,決...
    沈念sama閱讀 38,941評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼形真!你這毒婦竟也來了杉编?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,704評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎邓馒,沒想到半個月后嘶朱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,152評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡绒净,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,494評論 2 327
  • 正文 我和宋清朗相戀三年见咒,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挂疆。...
    茶點故事閱讀 38,629評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖下翎,靈堂內(nèi)的尸體忽然破棺而出缤言,到底是詐尸還是另有隱情,我是刑警寧澤视事,帶...
    沈念sama閱讀 34,295評論 4 329
  • 正文 年R本政府宣布胆萧,位于F島的核電站,受9級特大地震影響俐东,放射性物質(zhì)發(fā)生泄漏跌穗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,901評論 3 313
  • 文/蒙蒙 一虏辫、第九天 我趴在偏房一處隱蔽的房頂上張望蚌吸。 院中可真熱鬧,春花似錦砌庄、人聲如沸羹唠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽佩微。三九已至,卻和暖如春萌焰,著一層夾襖步出監(jiān)牢的瞬間哺眯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,978評論 1 266
  • 我被黑心中介騙來泰國打工扒俯, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留奶卓,地道東北人。 一個月前我還...
    沈念sama閱讀 46,333評論 2 360
  • 正文 我出身青樓陵珍,卻偏偏與公主長得像寝杖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子互纯,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,499評論 2 348

推薦閱讀更多精彩內(nèi)容