iOS多線程編程(三) NSThread

多線程系列篇章計(jì)劃內(nèi)容:
iOS多線程編程(一) 多線程基礎(chǔ)
iOS多線程編程(二) Pthread
iOS多線程編程(三) NSThread
iOS多線程編程(四) GCD
iOS多線程編程(五) GCD的底層原理
iOS多線程編程(六) NSOperation
iOS多線程編程(七) 同步機(jī)制與鎖
iOS多線程編程(八) RunLoop

NSThread 是蘋果提供的一種面向?qū)ο蟮妮p量級(jí)多線程解決方案,一個(gè) NSThread 對(duì)象代表一個(gè)線程,使用比較簡單,但是需要手動(dòng)管理線程的生命周期、處理線程同步等問題。

1. 創(chuàng)建、啟動(dòng)NSThread線程

  • 創(chuàng)建一個(gè)NSThread線程有類方法和實(shí)例方法。

類方法創(chuàng)建:

+ (void)detachNewThreadWithBlock:(void (^)(void))block ;
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

實(shí)例方法創(chuàng)建:

- (instancetype)initWithBlock:(void (^)(void))block;
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;

使用實(shí)例方法創(chuàng)建線程返回線程對(duì)象,可以根據(jù)需要設(shè)置相應(yīng)屬性參數(shù)。

需要注意:block形式的創(chuàng)建方式 需在iOS10之后使用

  • 創(chuàng)建完畢后不要忘記開啟線程!
線程的狀態(tài)

線程創(chuàng)建完畢后對(duì)應(yīng)線程狀態(tài)的新建態(tài),我們需要調(diào)用 start方法啟動(dòng)線程(使用類方法創(chuàng)建的線程隱式的啟動(dòng)了線程),否則線程是不會(huì)執(zhí)行的。

但是使用類方法創(chuàng)建或者使用實(shí)例方法創(chuàng)建并且調(diào)用start方法之后,線程并不會(huì)立即執(zhí)行,只是將線程加入可調(diào)度線程池,進(jìn)入就緒狀態(tài),具體何時(shí)執(zhí)行需要等待CPU的調(diào)度。(關(guān)于線程狀態(tài)可以參閱 多線程基礎(chǔ) 中的線程生命周期)

2. NSThread線程屬性

  • name 屬性:設(shè)置線程的名字
NSThread *thread = [[NSThread alloc] initWithBlock:^{
    NSLog(@"線程:%@ start",[NSThread currentThread]);
 }];
thread.name = @"測(cè)試線程";
[thread start];

打印結(jié)果如下:

線程:<NSThread: 0x600001227200>{number = 6, name = 測(cè)試線程} start
  • qualityOfService屬性:設(shè)置線程優(yōu)先級(jí)

原本線程優(yōu)先級(jí)threadPriority屬性,是一個(gè)double類型,取值范圍為0.0~1.0,值越大,優(yōu)先級(jí)越高。不過,該屬性已被qualityOfService取代。qualityOfService是一個(gè)枚舉值。定義如下:

typedef NS_ENUM(NSInteger, NSQualityOfService) {
    NSQualityOfServiceUserInteractive = 0x21,
    NSQualityOfServiceUserInitiated = 0x19,
    NSQualityOfServiceUtility = 0x11,
    NSQualityOfServiceBackground = 0x09,
    NSQualityOfServiceDefault = -1
} API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));

NSQualityOfServiceUserInteractive 優(yōu)先級(jí)最高,從上到下依次降低,NSQualityOfServiceDefault 為默認(rèn)優(yōu)先級(jí)。

使用如下:

    NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
        NSLog(@"\n 線程:%@ start",[NSThread currentThread]);
    }];
    thread1.name = @"測(cè)試線程 1 ";
    [thread1 start];
    
    NSThread *thread2 = [[NSThread alloc] initWithBlock:^{
        NSLog(@"\n 線程:%@ start",[NSThread currentThread]);
    }];
    thread2.qualityOfService = NSQualityOfServiceUserInteractive;
    thread2.name = @"測(cè)試線程 2 ";
    [thread2 start];

雖然 thread1 先于 thread2 start,但thread1優(yōu)先級(jí)為默認(rèn),而thread2優(yōu)先級(jí)為NSQualityOfServiceUserInteractive,在執(zhí)行時(shí),thread2 先于 thread1執(zhí)行。

線程:<NSThread: 0x600001e557c0>{number = 7, name = 測(cè)試線程 2 } start
線程:<NSThread: 0x600001e55700>{number = 6, name = 測(cè)試線程 1 } start
  • callStackReturnAddressescallStackSymbols屬性:

callStackReturnAddresses 屬性定義如下:

@property (class, readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses

線程的調(diào)用會(huì)有函數(shù)的調(diào)用,該屬性返回的就是 該線程中函數(shù)調(diào)用的虛擬地址數(shù)組。

callStackSymbols 屬性定義如下:

@property (class, readonly, copy) NSArray<NSString *> *callStackSymbols

該屬性以符號(hào)的形式返回該線程調(diào)用函數(shù)。

callStackReturnAddresscallStackSymbols這兩個(gè)函數(shù)可以同NSLog聯(lián)合使用來跟蹤線程的函數(shù)調(diào)用情況,是編程調(diào)試的重要手段。

  • threadDictionary屬性:

每個(gè)線程有自己的堆??臻g,線程內(nèi)維護(hù)了一個(gè)鍵-值的字典,它可以在線程里面的任何地方被訪問。
你可以使用該字典來保存一些信息,這些信息在整個(gè)線程的執(zhí)行過程中都保持不變。
比如,你可以使用它來存儲(chǔ)在你的整個(gè)線程過程中 Run loop 里面多次迭代的狀態(tài)信息。

  • 其他屬性
@property (class, readonly, strong) NSThread *mainThread; // 獲取主線程
@property (class, readonly, strong) NSThread *currentThread;// 獲取當(dāng)前線程
@property NSUInteger stackSize; // 線程使用堆棧大小,默認(rèn)512k
@property (readonly) BOOL isMainThread; // 是否是主線程
@property (class, readonly) BOOL isMainThread ; // reports whether current thread is main
@property (readonly, getter=isExecuting) BOOL executing ; // 線程是否正在執(zhí)行
@property (readonly, getter=isFinished) BOOL finished ;  // 線程是否執(zhí)行完畢
@property (readonly, getter=isCancelled) BOOL cancelled;  // 線程是否取消

3. NSThread線程的阻塞

NSThread提供了2個(gè)類方法,

+ (void)sleepUntilDate:(NSDate *)date; // 休眠到指定日期
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;// 休眠執(zhí)行時(shí)常

對(duì)于上面設(shè)置線程優(yōu)先級(jí)的示例代碼,我們稍做些更改。

    NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
        NSLog(@"\n 線程:%@ start",[NSThread currentThread]);
    }];
    thread1.name = @"測(cè)試線程 1 ";
    [thread1 start];
    // 加入休眠函數(shù)
    [NSThread sleepForTimeInterval:1];
    NSThread *thread2 = [[NSThread alloc] initWithBlock:^{
        NSLog(@"\n 線程:%@ start",[NSThread currentThread]);
    }];
    thread2.qualityOfService = NSQualityOfServiceUserInteractive;
    thread2.name = @"測(cè)試線程 2 ";
    [thread2 start];

在 thread1 與 thread2 之間加入 [NSThread sleepForTimeInterval:1]; 讓主線程阻塞1秒,那么 thread1 將 先于 thread2 執(zhí)行,即使thread2 的優(yōu)先級(jí)是高于thread1。

這是因?yàn)?,thread1先start進(jìn)入就緒狀態(tài),此時(shí),主線程休眠,在CPU時(shí)間到來之時(shí),可調(diào)度線程池中只有thread1,thread1 被調(diào)度執(zhí)行,此時(shí)主線程休眠時(shí)間結(jié)束,thread2 進(jìn)入就緒態(tài),并在下一次CPU時(shí)間時(shí)被調(diào)度執(zhí)行。

4. NSThread的終止

  • 取消線程
- (void)cancel ;

對(duì)于已被調(diào)度的線程是無法通過cancel取消的。

  • 退出線程
+ (void)exit;

強(qiáng)制退出線程,使線程進(jìn)入死亡態(tài)。

5. 線程的通信

在開發(fā)中,我們有時(shí)需要在子線程進(jìn)行耗時(shí)操作,操作結(jié)束后切換到主線程進(jìn)行刷新UI。這就涉及到線程間的通信,NSThread線程提供了對(duì)NSObject的拓展函數(shù)。

5.1 NSObject方式

// 在主線程上執(zhí)行操作 wait表示是否阻塞該方法,等待主線程空閑再運(yùn)行,modes表示運(yùn)行模式kCFRunLoopCommonModes
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array;
// equivalent to the first method with kCFRunLoopCommonModes

// 在指定線程上執(zhí)行操作
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes

// 隱式創(chuàng)建一個(gè)線程并執(zhí)行
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;

// NSObject函數(shù): 在當(dāng)前線程上執(zhí)行操作,調(diào)用 NSObject 的 performSelector:相關(guān)方法
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

舉個(gè)例子,我們來模擬子線程下載圖片回到線程刷新 UI 的實(shí)現(xiàn)

// 開辟子線程模擬網(wǎng)絡(luò)請(qǐng)求
- (void)downloadImage { 
   [NSThread detachNewThreadWithBlock:^{
   // 1. 獲取圖片 imageUrl
   NSURL *imageUrl = [NSURL URLWithString:@"https://xxxxx.jpg"];
   // 2. 從 imageUrl 中讀取數(shù)據(jù)(下載圖片) -- 耗時(shí)操作
   NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
   // 通過二進(jìn)制 data 創(chuàng)建 image
   UIImage *image = [UIImage imageWithData:imageData];

   // 主線程刷新UI
   [self performSelectorOnMainThread:@selector(mainThreadRefreshUI) withObject:image waitUntilDone:YES];
    }];
}

// 主線程刷新 UI 調(diào)用方法
- (void)mainThreadRefreshUI:(UIImage *)image {
    self.imageView.image = image;
}

5.2 端口通信方式

端口通信需要使用 NSPort ,NSPort 是一個(gè)抽象類,具體使用的時(shí)候可以使用其子類NSMachPort。

通過下面方法傳遞將要在線程間通信的信息數(shù)據(jù)。

- (BOOL)sendBeforeDate:(NSDate *)limitDate components:(nullable NSMutableArray *)components from:(nullable NSPort *) receivePort reserved:(NSUInteger)headerSpaceReserved;
- (BOOL)sendBeforeDate:(NSDate *)limitDate msgid:(NSUInteger)msgID components:(nullable NSMutableArray *)components from:(nullable NSPort *)receivePort reserved:(NSUInteger)headerSpaceReserved;

實(shí)現(xiàn)NSPortDelegate 的方法,接受端口傳遞過來的數(shù)據(jù)。

- (void)handlePortMessage:(NSPortMessage *)message

注意:在使用端口的時(shí)候,需要注意將端口將入當(dāng)前Runloop,否則消息無法傳遞

[[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];

6. NSThread通知

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

7. NSThread 線程安全案例

只要涉及到多線程就有可能存在非線程安全的情況。根本原因就是多條線程同時(shí)操作一片臨界區(qū),導(dǎo)致臨界區(qū)資源錯(cuò)亂。

我們來模擬多線程經(jīng)典的售票案例:兩個(gè)售票窗口同時(shí)售賣50張車票

- (void)initTicketStatusNotSave {
    // 1. 設(shè)置剩余火車票為 50
    self.ticketSurplusCount = 50;

    // 2. 模擬窗口1售票的線程
    self.ticketSaleWindow1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
    self.ticketSaleWindow1.name = @"售票窗口1";

    // 3. 模擬窗口2售票的線程
    self.ticketSaleWindow2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
    self.ticketSaleWindow2.name = @"售票窗口2";

    // 4. 開始售賣火車票
    [self.ticketSaleWindow1 start];
    [self.ticketSaleWindow2 start];
}

/**
* 售賣火車票(非線程安全)
*/
- (void)saleTicketNotSafe {
    while (1) {
    //如果還有票,繼續(xù)售賣
    if (self.ticketSurplusCount > 0) {
        self.ticketSurplusCount --;
        NSLog(@"%@", [NSString stringWithFormat:@"剩余票數(shù):%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]);
       [NSThread sleepForTimeInterval:0.2];
    }
    //如果已賣完,關(guān)閉售票窗口
    else {
        NSLog(@"所有火車票均已售完");
        break;
    }
  }
}

截取部分結(jié)果如下:

2020-11-19 15:55:53.222575+0800 pthread[5018:211393] 剩余票數(shù):49 窗口:售票窗口1
2020-11-19 15:55:53.222589+0800 pthread[5018:211394] 剩余票數(shù):48 窗口:售票窗口2
2020-11-19 15:55:53.426619+0800 pthread[5018:211394] 剩余票數(shù):46 窗口:售票窗口2
2020-11-19 15:55:53.426626+0800 pthread[5018:211393] 剩余票數(shù):47 窗口:售票窗口1
2020-11-19 15:55:53.630102+0800 pthread[5018:211394] 剩余票數(shù):45 窗口:售票窗口2
2020-11-19 15:55:53.630144+0800 pthread[5018:211393] 剩余票數(shù):44 窗口:售票窗口1
2020-11-19 15:55:53.832564+0800 pthread[5018:211393] 剩余票數(shù):43 窗口:售票窗口1
2020-11-19 15:55:53.832649+0800 pthread[5018:211394] 剩余票數(shù):42 窗口:售票窗口2
2020-11-19 15:55:54.033279+0800 pthread[5018:211393] 剩余票數(shù):41 窗口:售票窗口1
2020-11-19 15:55:54.033360+0800 pthread[5018:211394] 剩余票數(shù):40 窗口:售票窗口2
2020-11-19 15:55:54.237370+0800 pthread[5018:211393] 剩余票數(shù):39 窗口:售票窗口1
2020-11-19 15:55:54.237370+0800 pthread[5018:211394] 剩余票數(shù):39 窗口:售票窗口2
2020-11-19 15:55:54.440124+0800 pthread[5018:211393] 剩余票數(shù):38 窗口:售票窗口1
2020-11-19 15:55:54.440200+0800 pthread[5018:211394] 剩余票數(shù):37 窗口:售票窗口2
2020-11-19 15:55:54.643881+0800 pthread[5018:211393] 剩余票數(shù):35 窗口:售票窗口1
2020-11-19 15:55:54.643889+0800 pthread[5018:211394] 剩余票數(shù):36 窗口:售票窗口2
2020-11-19 15:55:54.845543+0800 pthread[5018:211393] 剩余票數(shù):33 窗口:售票窗口1
......

這就是多線程同時(shí)操作同一片臨界區(qū)的結(jié)果,得到的票數(shù)是錯(cuò)亂的,這是不符合我們的預(yù)期的。

線程安全的解決方案,就是線程同步機(jī)制。比較常用的是使用【鎖】。在一個(gè)線程占用臨界區(qū)的時(shí)候,不允許其他線程進(jìn)入。

iOS 實(shí)現(xiàn)線程加鎖有很多種方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic等等。更詳細(xì)的鎖相關(guān)知識(shí)參見iOS多線程編程(七)-鎖。這里我們使用@synchronized對(duì)此案例進(jìn)行線程安全優(yōu)化。

- (void)initTicketStatusNotSave {
    // 1. 設(shè)置剩余火車票為 50
    self.ticketSurplusCount = 50;

    // 2. 模擬窗口1售票的線程
    self.ticketSaleWindow1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
    self.ticketSaleWindow1.name = @"售票窗口1";

    // 3. 模擬窗口2售票的線程
    self.ticketSaleWindow2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketNotSafe) object:nil];
    self.ticketSaleWindow2.name = @"售票窗口2";

    // 4. 開始售賣火車票
    [self.ticketSaleWindow1 start];
    [self.ticketSaleWindow2 start];
}

/**
* 售賣火車票(線程安全)
*/
- (void)saleTicketNotSafe {
    while (1) {
       // 互斥鎖
       @synchronized (self) {
           //如果還有票,繼續(xù)售賣
           if (self.ticketSurplusCount > 0) {
              self.ticketSurplusCount --;
              NSLog(@"%@", [NSString stringWithFormat:@"剩余票數(shù):%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]);
             [NSThread sleepForTimeInterval:0.2];
           }
           //如果已賣完,關(guān)閉售票窗口
           else {
              NSLog(@"所有火車票均已售完");
              break;
           }
        }
    }
}

運(yùn)行后結(jié)果是正常的,這里就不貼了。

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

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

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