iOS多線程之1. NSThread

?? 當(dāng)新打開一個APP的時候,系統(tǒng)會新創(chuàng)建一個進(jìn)程。這個進(jìn)程會默認(rèn)新創(chuàng)建一個線程,把這個線程命名為主線程。多線程主要應(yīng)用于與服務(wù)器進(jìn)行數(shù)據(jù)傳輸?shù)纫恍┖臅r操作。為了防止阻塞主線程,影響用戶交互,我們必須要新建子線程來執(zhí)行一些耗時操作。本文主要通過介紹NSThread的使用方法,來探討線程的生命周期、線程安全,線程間通信。

1.線程的生命周期

??線程的生命周期分為:1.創(chuàng)建線程;2.調(diào)度任務(wù);3.銷毀線程。一個NSThread對象就是一條線程,獲得一個NSThread對象的方式有兩種。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 方式1
    NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
        //調(diào)度任務(wù),例如下載圖片,往服務(wù)器上傳文件等一切耗費時間的操作
        NSLog(@"thread1--------執(zhí)行任務(wù)");
     }];
    // 開始執(zhí)行線程中的任務(wù),相當(dāng)于調(diào)度任務(wù)
    [thread1 start];
    
    //方式 2
   //argument:id類型,為向方法(executeTask:)中傳遞的參數(shù)
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(executeTask:) object:@{@"key" : @"value"}];
    [thread2 start];
}

- (void)executeTask:(id)argument
{
    /*
    thread2--------執(zhí)行任務(wù)-------argument = {
    key = value;
    }
    **/
    NSLog(@"thread2--------執(zhí)行任務(wù)-------argument = %@",argument);
}

??創(chuàng)建一個NSThread對象并往線程中添加了任務(wù)之后,必須執(zhí)行[thread start];才會執(zhí)行線程中的任務(wù)。[thread start];只能執(zhí)行一次,否則會報attempt to start the thread again的錯誤。
??當(dāng)線程中的任務(wù)執(zhí)行完之后,系統(tǒng)會自動執(zhí)行[NSThread exit]銷毀線程,釋放內(nèi)存。在銷毀線程之前,[NSThread exit]這個方法會發(fā)送一個通知NSThreadWillExitNotification通知觀察者線程即將銷毀。由于通知的發(fā)出是同步的,所以回調(diào)的執(zhí)行在線程銷毀之前。所以我們可以用下面的方法來監(jiān)測線程的銷毀。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSThread *thread1 = [[NSThread alloc] initWithBlock:^{
        //調(diào)度任務(wù),例如下載圖片,往服務(wù)器上傳文件等一切耗費時間的操作
        NSLog(@"thread1--------執(zhí)行任務(wù)");
     }];
    // 開始執(zhí)行線程中的任務(wù),相當(dāng)于任務(wù)調(diào)度
    thread1.name = @"thread1";
    [thread1 start];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(threadWillExit:) name:NSThreadWillExitNotification object:nil];
}

- (void)threadWillExit:(NSNotification *)noti
{
  //   日志: thread1 馬上退出了
  NSLog(@"%@ 馬上退出了",[[NSThread currentThread] name]);
}

??NSThread提供了很多屬性和方法,方便我們使用。下面主要介紹常用的幾個屬性和方法。

// 類屬性,獲取當(dāng)前線程  [NSThread currentThread]
@property (class, readonly, strong) NSThread *currentThread;
// 類屬性,獲取主線程  [NSThread mainThread]
@property (class, readonly, strong) NSThread *mainThread;
//線程的名字
@property (nullable, copy) NSString *name;
2.線程安全

??一塊數(shù)據(jù)如果被多個線程同時訪問,就容易發(fā)生數(shù)據(jù)錯亂和數(shù)據(jù)安全問題。下面舉一個賣票的例子。

- (void)viewDidLoad {
    [super viewDidLoad];

    // 一共50張票
    self.ticketNum  = 50;
   
    // 三個售票員同時開始賣票
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
    thread1.name = @"售票員A";
    self.thread1 = thread1;
    
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
    thread2.name = @"售票員B";
    self.thread2 = thread2;
    
    NSThread *thread3 = [[NSThread alloc] initWithTarget:self selector:@selector(sellTickets) object:nil];
    thread3.name = @"售票員C";
    self.thread3 = thread3;
    
    [thread1 start];
    [thread2 start];
    [thread3 start];
    
}

- (void)sellTickets
{
    while (1) {
        int count = self.ticketNum;
        
        //1.先檢查票數(shù)
        if (count > 0) {
            //暫停一段時間
            [NSThread sleepForTimeInterval:0.02];
            //2.票數(shù)-1
            
            self.ticketNum = count-1;
            //獲取當(dāng)前線程
            NSThread *current= [NSThread currentThread];
            NSLog(@"%@--賣了一張票,還剩余%d張票",current,self.ticketNum);
        }
        
        if (self.ticketNum == 0){
            //退出線程
            [NSThread exit];
        }
    }
}

??因為日志太長,僅貼出部分日志。

2019-03-27 15:43:03.848178+0800 TestAppIOS[399:12301] <NSThread: 0x28354d0c0>{number = 4, name = 售票員B}--賣了一張票,還剩余49張票
2019-03-27 15:43:03.848178+0800 TestAppIOS[399:12302] <NSThread: 0x28354cf40>{number = 5, name = 售票員C}--賣了一張票,還剩余49張票
2019-03-27 15:43:03.848194+0800 TestAppIOS[399:12300] <NSThread: 0x28354d200>{number = 3, name = 售票員A}--賣了一張票,還剩余49張票
2019-03-27 15:43:03.868778+0800 TestAppIOS[399:12301] <NSThread: 0x28354d0c0>{number = 4, name = 售票員B}--賣了一張票,還剩余48張票
2019-03-27 15:43:03.872740+0800 TestAppIOS[399:12302] <NSThread: 0x28354cf40>{number = 5, name = 售票員C}--賣了一張票,還剩余48張票
2019-03-27 15:43:03.872797+0800 TestAppIOS[399:12300] <NSThread: 0x28354d200>{number = 3, name = 售票員A}--賣了一張票,還剩余48張票

??通過日志打印我們可以看出,一共50張票,但是ABC每個售票員都賣了50張票,這明顯是不對的。那問題出在哪里呢?三個線程同時訪問門票的數(shù)量(共享數(shù)據(jù)),發(fā)生了數(shù)據(jù)錯亂。
1.售票員A查詢門票數(shù)量的時候,發(fā)現(xiàn)門票還有50張,賣了一張,還剩49張。
2.售票員B查詢門票數(shù)量的時候(此時售票員A還沒有把門票賣出去),發(fā)現(xiàn)門票還有50張,賣了一張,還剩49張。
3.售票員C查詢門票數(shù)量的時候(此時售票員A.B還沒有把門票賣出去),發(fā)現(xiàn)門票還有50張,賣了一張,還剩49張。
4.這就出現(xiàn)了一個怪現(xiàn)象,每個售票員都賣出1張票,卻還剩49張門票的原因。
??那怎樣解決這個問題呢?一個售票員賣票的時候,其他兩個人等著。等這個售票員賣完了,這倆售票員其中的一個再賣。

- (void)sellTickets
{
    while (1) {
        // 加一把鎖
        @synchronized (self) {
            int count = self.ticketNum;
            //1.先檢查票數(shù)
            if (count > 0) {
                //暫停一段時間
                [NSThread sleepForTimeInterval:0.02];
                //2.票數(shù)-1
                
                self.ticketNum = count-1;
                //獲取當(dāng)前線程
                NSThread *current= [NSThread currentThread];
                NSLog(@"%@--賣了一張票,還剩余%d張票",current,self.ticketNum);
            }
        }
        
        if (self.ticketNum == 0){
            //退出線程
            [NSThread exit];
        }
    }
}

??除了@synchronized,還有NSLock也可以達(dá)到相同的效果,解決多線程資源共享產(chǎn)生的數(shù)據(jù)安全問題。@synchronized就是對括號里的代碼加鎖,一個線程執(zhí)行完了之后,另一個線程才能執(zhí)行。(售票員A賣票呢,此時BC不能賣。等A賣完了,BC才能賣。BC再去查詢票的數(shù)量的時候就變成了49張(因為A賣了一張))。
??鎖完美解決了多線程帶來的數(shù)據(jù)安全問題,不過鎖需要消耗大量的CPU資源,所以我們開發(fā)的時候盡量避免這種場景。

3.線程間通信

??理論上講線程都不是孤立存在的,需要相互傳遞消息。最常見的就是我們把一些耗時操作放在子線程,例如下載圖片,但是下載完畢我們不能在子線程更新UI,因為只有主線程才可以更新UI和處理用戶的觸摸事件,否則程序會崩潰。此時,我們就需要把子線程下載完畢的數(shù)據(jù)傳遞到主線程,讓主線程更新UI,這就是線程間的通信。

原理


002MKIl6zy75OmRx7rP61.jpeg

代碼

// 點擊屏幕開始下載圖片
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"當(dāng)前線程1=%@",[NSThread currentThread]);
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
    NSLog(@"當(dāng)前線程2=%@",[NSThread currentThread]);
       NSString *strURL = @"http://pic33.nipic.com/20130916/3420027_192919547000_2.jpg";
        UIImage *image = [self downloadImageWithURL:strURL];
        if (image) {
            [self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO];
        }
    }];
    [thread start];
}

日志

2016-11-04 13:47:04.532 TTTTTTTTTT[10584:122182] 當(dāng)前線程1=<NSThread: 0x600000260c80>{number = 1, name = main}
2016-11-04 13:47:04.533 TTTTTTTTTT[10584:122269] 當(dāng)前線程2=<NSThread: 0x600000265d80>{number = 3, name = (null)}

??以上就是關(guān)于NSThread的大部分知識了。在開發(fā)中,我們真正應(yīng)用NSThread的時候并不多,因為NSThread需要我們自己創(chuàng)建線程,調(diào)度任務(wù)。而線程并不是創(chuàng)建的越多越好,雖然多線程可以提高CPU的利用效率,但是創(chuàng)建多了,反而會拉低CPU運行速度,因為線程本身也需要消耗內(nèi)存。所以創(chuàng)建幾個線程,得根據(jù)CPU的當(dāng)時狀況來判斷。這對iOS程序員來說是解決不了的問題,因為我們無法知道CPU的運行狀況。所以NSThread雖然簡單,但是有很多不確定的情況。我們經(jīng)常使用的還是GCD和NSOperation,至于原因,后文我會詳細(xì)講解。

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

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