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

多線程-NSThread.png

上一節中,我們詳細的學習了和多線程有關的概念,像進程、線程、多線程、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,發現內容并不多,屬性和方法不是特別多,我們一個個來看看(根據字面意思理解的)。
注:不想看的可以跳過喲,直接到下面看代碼。
先看看類的聲明:

image.png

NSThread繼承自NSObject。

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


    image.png
  • 類方法創建線程


    image.png
  • isMultiThreaded 判斷是否有多個線程


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

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


    image.png
  • 退出線程


    image.png
  • 線程的優先級


    image.png
  • 這幾個字面上看不出來干嘛的


    image.png
  • 線程的名字


    image.png
  • 棧的大小


    image.png
  • 是否是主線程和獲取主線程


    image.png
  • 初始化線程


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


    image.png
  • 線程主函數 在線程中執行的函數 都要在-main函數中調用,自定義線程中重寫-main方法


    image.png
  • 線程有關的通知


    image.png

上面的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]);
}

打印結果:


image.png

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

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


image.png

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

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

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


image.png

子線程中:


image.png

打印結果:


image.png

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


image.png

看看打印結果:


image.png

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


程序剛啟動CPU占用情況.png

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


image.png

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


image.png

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

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


image.png

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

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

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

image.png

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]);
}

看看打印結果:


image.png

因為線程一沒有開啟,只是初始化了,所以不會執行線程一的內容。
使用對象方法創建子線程,要想讓線程執行,必須調用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,并不能真正的取消當前線程。

看看打印結果:


image.png

我們要想取消一個子線程,只是使用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(@"這行代碼不會打印的");
        }

    }
}

看看結果:


image.png

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

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


image.png

看看結果:


image.png

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

6》線程狀態

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


image.png
-(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];
    
}

打印結果:


image.png

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

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

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

結果:


image.png

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);
    }
}

看看打印結果:


image.png
image.png

我們發現,結果并不像我們預期的那樣啊,輸出有點錯亂,而且居然出現了-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張。
看看打印結果:


image.png

解決了問題。

NSThread小結:
NSThread是官方提供的,面向對象的創建多線程的方法。

  1. NSThread可以使用類方法快速創建子線程,但是得不到子線程對象,線程自動開啟。
  2. NSThread可以使用對象方法創建子線程,能夠得到子線程對象,但是要手動開啟子線程。
  3. NSThread可以取消子線程、可以隨時查看線程的狀態(正在執行、被取消、結束)。
  4. NSThread可以隨時查看當前代碼所在的線程。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370