詳解多線程(實現篇——NSThread)

上一節中,我們詳細的學習了和多線程有關的概念,像進程、線程、多線程、CPU內核、併發、並行、串行、隊列、同步、異步等概念。這一節中,我們將用代碼來實現多線程。
如果對多線程概念不太清楚的,可以參考上一節內容,鏈接如下:
詳解多線程(概念篇——進程、線程以及多線程原理)

說明:源碼親測,拒絕搬磚,源碼可下載。
源碼地址:https://github.com/weiman152/Multithreading.git

在iOS中,多線程的實現方法有多種,有OC的也有C語言的,有常用的,也有不常用的。本節中,我們就先探究NSThread這個OC的類對於實現多線程是如何進行的。

多線程的實現方法

  1. NSThread(OC)
  2. GCD(C語言)
  3. NSOperation(OC)
  4. C語言的pthread(C語言)
  5. 其他實現多線程方法
1.NSThread(OC)

NSThread是蘋果提供的面向對象的操作線程的方法。簡單方便,可以直接操作線程對象。
我們查看一下NSThread的API,發現內容並不多,屬性和方法不是特別多,我們一個個來看看(根據字面意思理解的)。
注:不想看的可以跳過喲,直接到下面看代碼。
先看看類的聲明:


NSThread繼承自NSObject。

  • currentThread
    聲明的第一個屬性,currentThread,當前上下文所在的線程。這也是我們非常常用的一個屬性。


  • 類方法創建線程


  • isMultiThreaded 判斷是否有多個線程


  • threadDictionary 線程字典
    每個線程都維護了一個鍵-值的字典,它可以在線程裏面的任何地方被訪問。你可以使用該字典來保存一些信息,這些信息在整個線程的執行過程中都保持不變。

  • 讓當前線程阻塞一段時間


  • 退出線程


  • 線程的優先級


  • 這幾個字面上看不出來幹嘛的


  • 線程的名字


  • 棧的大小


  • 是否是主線程和獲取主線程


  • 初始化線程


  • 線程狀態(正在執行、結束、被取消)


  • 主線程


  • 線程有關的通知


上面的API都是我根據字面意思理解的,不一定正確,下面我們就用代碼來試驗一下NSThread實現多線程的過程吧。

1》類方法創建子線程,並在子線程中執行想要的操作

//類方法創建線程
- (IBAction)createThreadC:(id)sender {
    NSLog(@"------------detachNewThreadWithBlock-------");
    //block創建,並在子線程進行想要的操作
   [NSThread detachNewThreadWithBlock:^{
       NSLog(@"--block--%@",[NSThread currentThread]);
    }];
    NSLog(@"------------detachNewThreadSelector-------");
    //在子線程中執行某方法
    [NSThread detachNewThreadSelector:@selector(printHi) toTarget:self withObject:nil];
}

-(void)printHi {
    NSLog(@"---printHi---");
    NSLog(@"Hi, 我要在子線程中執行");
    NSLog(@"--Sel--%@",[NSThread currentThread]);
}

打印結果:


分析:
createThreadC在主線程中,因爲開闢子線程需要耗費時間,所以會先打印主線程的:
------------detachNewThreadWithBlock-------
------------detachNewThreadSelector-------
然後在打印子線程的內容。因爲子線程是併發的,誰先執行完並不確定,所以先打印哪個子線程的內容也是不確定的。
注意:如果主線程和子線程都有一個for循環,循環很多次,那麼主線程和子線程中的for循環打印很可能是交叉進行的。

我們再次運行,看看結果是否與上次一樣呢。



與上次結果不太一樣喲,與我們上面的分析是一致的。

2》判斷當前是否開啓了多個線程 isMultiThreaded

我們分別在子線程和主線程中使用isMultiThreaded,看看結果:


子線程中:


打印結果:


結果是 YES,就是開啓了多線程。我們把開啓的子線程註釋掉再看看。


看看打印結果:


結果也是1,也是YES,這是爲什麼呢?
多方搜索,也沒有找到答案。
我想,因爲我是在一個應用程序中,應用程序默認開啓主線程,是不是應用程序默認還開啓了別的線程?我們看一下系統的CPU佔用情況:


上圖是程序程序剛啓動的時候CPU的使用情況,我們並沒有開啓線程,但是系統卻開啓了5個線程,並且線程2是有使用的,所以我們打印是否開啓了多線程的時候,會是YES。
我們靜置了一會兒,再看看系統的線程情況:



現在就剩下線程1和線程8了。
我們自己開啓了線程之後,看看CPU中線程開啓情況:


在圖中我們找到了我們自己創建的線程一,編號爲12 。
現在,我們明白了,爲什麼在應用程序中打印 [NSThread isMultiThreaded]結果爲什麼一直是YES了。

那麼,我們新建一個控制檯項目,打印看看:


果然,打印是0,也就是NO,認爲沒有多個線程。

3》是否是主線程,打印主線程

- (void)viewDidLoad {
  [super viewDidLoad];
  
  NSLog(@"000  %d", [NSThread isMultiThreaded]);
  NSLog(@"isMainThread: %d", [NSThread isMainThread]);
  NSLog(@"currentThread: %@", [NSThread currentThread]);
}

4》對象方法創建子線程

對象方法初始化子線程,我們可以得到一個子線程對象,然後使用這個子線程對象。如果我們要開啓子線程,一定要調用start方法,不然線程是不會開啓的。

- (IBAction)createThreadO:(id)sender {
    NSLog(@"新建多線程");
    //對象方法創建多線程 一
    self.thread1 = [[NSThread alloc] initWithBlock:^{
        NSLog(@"thread1: %@",[NSThread currentThread]);
        for (int i=0; i<100; i++) {
            NSLog(@"i= %d", i);
            [NSThread sleepForTimeInterval:1];
        }
    }];
    self.thread1.name = @"線程一";
    //對象方法創建多線程 二
    NSThread * thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(hello:) object:@"小明"];
    thread2.name = @"線程二";
    [thread2 start];
}

-(void)hello:(NSString *)name {
    NSLog(@"你好!%@",name);
    NSLog(@"當前線程是: %@",[NSThread currentThread]);
}

看看打印結果:


因爲線程一沒有開啓,只是初始化了,所以不會執行線程一的內容。
使用對象方法創建子線程,要想讓線程執行,必須調用start方法開啓子線程。

5》取消線程——cancel,並不能取消一個子線程

我們在NSThread中找到一個方法叫做cancel,看起來像是可以取消一個線程,我們來試一試。

- (IBAction)createThreadO:(id)sender {
    NSLog(@"新建多線程");
    //對象方法創建多線程 一
    self.thread1 = [[NSThread alloc] initWithBlock:^{
        NSLog(@"thread1: %@",[NSThread currentThread]);
        for (int i=0; i<10000; i++) {
            NSLog(@"i= %d", i);
        }
    }];
    self.thread1.name = @"線程一";
    
    //對象方法創建多線程 二
    NSThread * thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(hello:) object:@"小明"];
    thread2.name = @"線程二";
    [thread2 start];
}

- (IBAction)threadStart:(id)sender {
    NSLog(@"thread1開始");
    [self.thread1 start];
}

- (IBAction)threadCancel:(id)sender {
    NSLog(@"thread1 取消");
    [self printState:self.thread1];
    [self.thread1 cancel];
    NSLog(@"cancel 後:");
    [self printState:self.thread1];
    if([self.thread1 isCancelled]==YES){
        NSLog(@"thread1 被取消了,開始銷燬它");
        [NSThread exit];
        self.thread1 = nil;
    }
}

執行後發現,根本不能取消,線程還是在執行完循環之後才停止的。我們看看該方法的官方文檔:
Instance Method
cancel
Changes the cancelled state of the receiver to indicate that it should exit.

意思是說,這個方法只是把cancelled的屬性置爲YES,並不能真正的取消當前線程。

看看打印結果:


我們要想取消一個子線程,只是使用cancel是做不到的,cancel只是把屬性isCancelled設置爲YES,並不能真正的取消一個子線程。我們可以配合isCancelled屬性,使用類方法exit,取消一個子線程。
注意:上面我們的案例中,由於使用的是按鈕取消,按鈕方法是在主線程中進行的,在主線程中執行exit是不會有效果的。所以,在這種狀態下,我們的線程一是不能被取消的。要想取消線程一,我們需要在子線程內部進行。
例如:

//再次測試取消線程
- (IBAction)cancelThreadAgain:(id)sender {
    [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}

- (void)run {
    NSLog(@"當前線程%@", [NSThread currentThread]);

    for (int i = 0 ; i < 100; i++) {
        NSLog(@"i = %d", i);
        if (i == 20) {
            //取消線程
            [[NSThread currentThread] cancel];
            NSLog(@"取消線程%@", [NSThread currentThread]);
        }

        if ([[NSThread currentThread] isCancelled]) {
            NSLog(@"結束線程%@", [NSThread currentThread]);
            //結束線程
            [NSThread exit];
            NSLog(@"這行代碼不會打印的");
        }

    }
}

看看結果:



只打印了前20個數字,說明線程取消了。

網上有人說,如果在線程中使用了sleep方法,就不能取消線程了,我們試一試:



看看結果:


跟之前一樣,還是可以取消的。說明sleep是不會影響線程的取消退出操作的。

6》線程狀態

使用NSThread創建的子線程,我們可以得到線程的三個狀態:是否結束、是否取消、是否正在執行


-(void)printState:(NSThread *)thread{
    NSLog(@"狀態,isCancelled: %d",[thread isCancelled]);
    NSLog(@"狀態,isFinished: %d",[thread isFinished]);
    NSLog(@"狀態,isExecuting: %d",[thread isExecuting]);
}

7》讓線程阻塞一段時間

有的時候,我們希望線程等待一會兒再執行,這個時候,我們可以使用
+(void)sleepUntilDate:(NSDate *)date;
+(void)sleepForTimeInterval:(NSTimeInterval)ti;
這兩個方法,讓線程阻塞一會兒在執行。觀察後發現,這兩個方法也是類方法,那麼我們調用的時候,會阻塞當前線程,還是把所有線程都阻塞呢?我們試一試吧。

- (IBAction)sleepAction:(id)sender {
    NSThread * threadA = [[NSThread alloc] initWithBlock:^{
        //threadA 阻塞2秒後執行
        [NSThread sleepForTimeInterval:2.0];
        for (int i=0; i<10; i++) {
            NSLog(@"%@, i = %d", [NSThread currentThread].name, i);
        }
        NSLog(@"threadA 結束了");
    }];
    threadA.name = @"線程A";
    [threadA start];
    
    NSThread * threadB = [[NSThread alloc] initWithBlock:^{
        for (int i=0; i<10; i++) {
            NSLog(@"%@, i = %d", [NSThread currentThread].name, i);
        }
        NSLog(@"threadB 結束了");
    }];
    threadB.name = @"線程B";
    [threadB start];
    
}

打印結果:



先打印了線程B的內容,說明sleep方法並不會阻塞所有的線程,只會阻塞當前的線程。

另一個方法傳入一個日期類型,也就是等到某一個特殊日期的時候纔會執行。

    //讓這個線程等到某個日期的時候在執行,這裏給的是當前時間的2秒後執行,只是爲了測試。
    [NSThread detachNewThreadWithBlock:^{
        NSDate * date = [NSDate dateWithTimeIntervalSinceNow:2];
        [NSThread sleepUntilDate:date];
        NSLog(@"終於等到這一天啦!我執行啦!");
    }];

結果:


8》案例:售票問題
描述:
假如我們有三個售票員ABC同時都在售票,每售出一張票,就從庫存中減去一張,直到所有的票售完。

我們用代碼去模擬這個過程。
分析一下:三個售票員我們用三個線程模擬,設置總票數爲100,每個線程都執行一個總票數減1的操作,直到總票數爲0 。

實現代碼如下:

//售票
- (IBAction)sellTickets:(id)sender {
    self.totalTickets = 100;
    
    NSThread * t1 = [[NSThread alloc] initWithTarget:self selector:@selector(sell) object:nil];
    t1.name = @"售票員:王美美";
    [t1 start];
    
    NSThread * t2 = [[NSThread alloc] initWithTarget:self selector:@selector(sell) object:nil];
    t2.name = @"售票員:李帥帥";
    [t2 start];
    
    NSThread * t3 = [[NSThread alloc] initWithTarget:self selector:@selector(sell) object:nil];
    t3.name = @"售票員:張靚靚";
    [t3 start];
}

- (void)sell{
    NSLog(@"開始售票,當前餘票:%d", self.totalTickets);
    while (self.totalTickets > 0) {
        [NSThread sleepForTimeInterval:1.0];
        self.totalTickets--;
        NSLog(@"%@ 賣出一張,餘票:%d", [NSThread currentThread].name, self.totalTickets);
    }
}

看看打印結果:


我們發現,結果並不像我們預期的那樣啊,輸出有點錯亂,而且居然出現了-1,這實在是不能容忍的。
爲什麼會出現這樣的問題呢?
因爲三個線程同時訪問我們的公共資源self.totalTickets,當線程一訪問了,還沒有減1的時候,線程二或者線程三也進來訪問了,這個時候,線程二或者線程三讀取的還是之前的self.totalTickets,所以就會出現打印兩次甚至三次相同餘票的情況。
爲了解決這個問題,我們在線程訪問公共資源的時候加個鎖,也就是說,當線程一準備訪問公共資源的時候,我們就把公共資源鎖住,不讓其他線程進來。當線程一訪問完了,再進行解鎖,其他線程繼續訪問。
代碼如下:

- (void)sell{
    NSLog(@"開始售票,當前餘票:%d", self.totalTickets);
    while (self.totalTickets > 0) {
        [NSThread sleepForTimeInterval:1.0];
        //互斥鎖--鎖內的代碼在同一時間只有一個線程在執行
        @synchronized (self) {
            if(self.totalTickets > 0){
                self.totalTickets--;
                NSLog(@"%@ 賣出一張,餘票:%d", [NSThread currentThread].name, self.totalTickets);
            }else{
                NSLog(@"餘票不足,出票失敗!");
            }
            
        }
    }
}

爲了儘快打印,所以把總票數改成10張。
看看打印結果:


解決了問題。

關於NSThread就先到這裏吧,有任何問題請留言,謝謝!
祝大家生活愉快!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章