OC-多線程的理解和使用

談到編程,就離不開多線程。多線程提升了系統(tǒng)資源的利用率,使得程序在相同時間單位里可以做更多的事情背犯,是我們每個程序員都必須掌握的重要知識坏瘩。

  • 什么是線程

線程(英語:thread)是操作系統(tǒng)能夠進(jìn)行運算調(diào)度的最小單位盅抚。它被包含在進(jìn)程之中,是進(jìn)程中的實際運作單位倔矾。一條線程指的是進(jìn)程中一個單一順序的控制流妄均,一個進(jìn)程中可以并發(fā)多個線程柱锹,每條線程并行執(zhí)行不同的任務(wù)。

  • 什么是進(jìn)程

進(jìn)程(英語:process)丰包,是計算機中已運行程序的實體禁熏。進(jìn)程為曾經(jīng)是分時系統(tǒng)的基本運作單位。在面向進(jìn)程設(shè)計的系統(tǒng)(如早期的UNIX邑彪,Linux 2.4及更早的版本)中瞧毙,進(jìn)程是程序的基本執(zhí)行實體;在面向線程設(shè)計的系統(tǒng)(如當(dāng)代多數(shù)操作系統(tǒng)寄症、Linux 2.6及更新的版本)中宙彪,進(jìn)程本身不是基本運行單位,而是線程的容器有巧。程序本身只是指令释漆、數(shù)據(jù)及其組織形式的描述,進(jìn)程才是程序(那些指令和數(shù)據(jù))的真正運行實例篮迎。若干進(jìn)程有可能與同一個程序相關(guān)系男图,且每個進(jìn)程皆可以同步(循序)或異步(平行)的方式獨立運行。現(xiàn)代計算機系統(tǒng)可在同一段時間內(nèi)以進(jìn)程的形式將多個程序加載到內(nèi)存中甜橱,并借由時間共享(或稱時分復(fù)用)逊笆,以在一個處理器上表現(xiàn)出同時(平行性)運行的感覺。同樣的渗鬼,使用多線程技術(shù)(多線程即每一個線程都代表一個進(jìn)程內(nèi)的一個獨立執(zhí)行上下文)的操作系統(tǒng)或計算機架構(gòu)览露,同樣程序的平行線程,可在多CPU主機或網(wǎng)絡(luò)上真正同時運行(在不同的CPU上)譬胎。

  • 什么是程序

程序(英語:procedure)差牛,指特定的一系列動作、行動或操作堰乔,而這些活動偏化、動作或操作必須以相同方式運行,借此在相同環(huán)境下恒常得出相同的結(jié)果(例如緊急應(yīng)變程序)镐侯。粗略而言侦讨,程序可以指一序列的活動、作業(yè)苟翻、步驟韵卤、決斷、計算和工序崇猫,當(dāng)它們保證依照嚴(yán)格規(guī)定的順序發(fā)生時即產(chǎn)生所述的后果沈条、產(chǎn)品或局面。一個程序通常引致一個改變∽缏現(xiàn)在小孩也可以寫程序蜡歹。

我們經(jīng)常容易混淆或者不知道如何對這三個概念明確的分界屋厘,我這里有一個簡單的區(qū)分方法:程序真正運行在計算機中的實體,就變成了進(jìn)程月而,(正在運行的程序叫進(jìn)程)而進(jìn)程可以包含一個或多個線程汗洒。

今天,我們談?wù)勗贠C中父款,多線程的幾種方案溢谤。其中pthread是純C語言的一套線程管理方案,由于在iOS開發(fā)中基本上不會使用憨攒,我們這里不做討論溯香。我們重點討論以下幾種方案:

  • NSThread

NSThread有兩種創(chuàng)建方法,一種是類方法浓恶,一種是實例方法玫坛,類方法創(chuàng)建的時候需要確定好執(zhí)行的任務(wù),沒有任何返回包晰,它會自己創(chuàng)建一個新線程去執(zhí)行指定任務(wù)湿镀,而用實例方法則需要我們手動開啟這個線程。創(chuàng)建的時候我們既可以使用SEL也可以使用Block伐憾,為了簡化代碼勉痴,我們都使用Block來創(chuàng)建。

//類方法創(chuàng)建
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"%d",i);
            [NSThread sleepForTimeInterval:1];
        }
    }];
    //實例方法創(chuàng)建
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        for (int i = 97; i < 102; i++) {
            NSLog(@"%c",i);
            [NSThread sleepForTimeInterval:1];
        }
    }];
    [thread start];

運行結(jié)果:



我們可以看到這兩個線程是完全異步的树肃。運行幾次蒸矛,結(jié)果也不相同。

值得注意的是胸嘴,一個NSThread線程的啟動雏掠,有三種方式,我們前面已經(jīng)看到了兩種劣像,一種是類方法直接創(chuàng)建并啟動乡话,另一種是[thread start];方法,還有一種是[thread main];耳奕,但是蘋果并不建議我們直接使用main方法绑青,它可以在子類化的時候重寫并實現(xiàn)你的線程主體,而不用調(diào)用super屋群,任何時候闸婴,啟動線程都應(yīng)該用start方法。

前面我們簡單得用NSThread的方式創(chuàng)建了兩個線程芍躏,并且讓它們各自執(zhí)行了自己的任務(wù)邪乍。但是NSThread可不止這么幾個方法,它還有很多比較有用的方法:

    //設(shè)置名字
    [thread setName:@"myThread"];
    //設(shè)置優(yōu)先級,由0到1.0的浮點數(shù)指定溺欧,其中1.0是最高優(yōu)先級。
    [thread setThreadPriority:1];
    //退出當(dāng)前線程
    [NSThread exit];
    //睡眠 單位是秒
    [NSThread sleepForTimeInterval:1];
    //獲取當(dāng)前線程
    [NSThread currentThread];
    //獲取主線程
    [NSThread mainThread];
    //判斷是否在主線程
    [NSThread isMainThread];

接下來柏肪,我們用經(jīng)典的賣票問題來模擬并解決NSThread中的線程同步問題姐刁。有兩個售票員,同時開始賣票烦味,一共有20張票聂使,模擬該場景:

@property (nonatomic, assign) NSInteger tickets;

    //初始有20張票
    self.tickets = 20;

    //創(chuàng)建兩個線程來充當(dāng)兩個售票員
    [NSThread detachNewThreadWithBlock:^{
        while (self.tickets > 0) {
            [NSThread sleepForTimeInterval:1];
            self.tickets --;
            NSLog(@"還有%ld張票",(long)self.tickets);
        }
    }];
    [NSThread detachNewThreadWithBlock:^{
        while (self.tickets > 0) {
            [NSThread sleepForTimeInterval:1];
            self.tickets --;
            NSLog(@"還有%ld張票",(long)self.tickets);
        }
    }];

運行結(jié)果:


通過打印結(jié)果,可以看出谬俄,一共賣出去了26張票柏靶,超出了我們的票的總數(shù),這是同一張票被多次出售的結(jié)果溃论。為了避免這種情況屎蜓,通常的做法就是上鎖,當(dāng)某一條線程對數(shù)據(jù)進(jìn)行操作時钥勋,先給數(shù)據(jù)上鎖炬转,別的線程阻塞,等到這條線程操作結(jié)束算灸,在開鎖扼劈,別的線程再進(jìn)去,上鎖菲驴,操作荐吵,開鎖……這樣就保證了數(shù)據(jù)的安全性。

我們可以使用@synchronized (object) {}來進(jìn)行上鎖赊瞬,括號里的參數(shù)可以填任意對象先煎,但是要注意的是,必須填寫線程共有的變量才能實現(xiàn)上鎖巧涧,局部變量是無效的榨婆,原因是,如果用局部變量褒侧,就會創(chuàng)建多個鎖良风,這些鎖之間并無關(guān)聯(lián),所以與不上鎖沒有區(qū)別:

//創(chuàng)建兩個線程來充當(dāng)兩個售票員
    [NSThread detachNewThreadWithBlock:^{
        //對賣票過程加鎖
        @synchronized (self) {
            while (self.tickets > 0) {
                [NSThread sleepForTimeInterval:1];
                self.tickets --;
                NSLog(@"還有%ld張票",(long)self.tickets);
            }
        }
    }];
    [NSThread detachNewThreadWithBlock:^{
        //對賣票過程加鎖
        @synchronized (self) {
            while (self.tickets > 0) {
                [NSThread sleepForTimeInterval:1];
                self.tickets --;
                NSLog(@"還有%ld張票",(long)self.tickets);
            }
        }
    }];

運行結(jié)果:

因為在鎖內(nèi)進(jìn)行數(shù)據(jù)操作時闷供,其它線程都會阻塞在外面烟央,這個時候,其實線程不是并發(fā)執(zhí)行的歪脏,所以我們不難想到疑俭,鎖內(nèi)執(zhí)行的任務(wù)越少,那么這段代碼執(zhí)行的效率就越高婿失。在此基礎(chǔ)上钞艇,我們可以對前面的加鎖進(jìn)行一個小修改:

    //創(chuàng)建兩個線程來充當(dāng)兩個售票員
    [NSThread detachNewThreadWithBlock:^{
        //對賣票過程加鎖
        while (true) {
            [NSThread sleepForTimeInterval:1];
            @synchronized (self) {
                if (self.tickets < 1) {
                    break;
                }
                self.tickets --;
                NSLog(@"還有%ld張票",(long)self.tickets);
            }
        }
    }];
    [NSThread detachNewThreadWithBlock:^{
        //對賣票過程加鎖
        while (true) {
            [NSThread sleepForTimeInterval:1];
            @synchronized (self) {
                if (self.tickets < 1) {
                    break;
                }
                self.tickets --;
                NSLog(@"還有%ld張票",(long)self.tickets);
            }
        }
    }];

運行結(jié)果:

注意看一下時間啄寡,修改以后,我們的賣票效率提升了一倍哩照,之前那種方式要20秒才能賣完挺物,現(xiàn)在只需要10秒。

當(dāng)然也可以用NSLock來進(jìn)行上鎖飘弧,使用NSLock需要創(chuàng)建一個NSLock實例识藤,然后調(diào)用lockunlock方法來進(jìn)行加鎖和解鎖的操作:

@property (nonatomic, strong) NSLock *lock;

    //初始化鎖
    self.lock = [[NSLock alloc] init];

    //創(chuàng)建兩個線程來充當(dāng)兩個售票員
    [NSThread detachNewThreadWithBlock:^{
        //對賣票過程加鎖
        while (true) {
            [NSThread sleepForTimeInterval:1];
            [self.lock lock];
            if (self.tickets < 1) {
                break;
            }
            self.tickets --;
            NSLog(@"還有%ld張票",(long)self.tickets);
            [self.lock unlock];
        }
    }];
    [NSThread detachNewThreadWithBlock:^{
        //對賣票過程加鎖
        while (true) {
            [NSThread sleepForTimeInterval:1];
            [self.lock lock];
            if (self.tickets < 1) {
                break;
            }
            self.tickets --;
            NSLog(@"還有%ld張票",(long)self.tickets);
            [self.lock unlock];
        }
    }];

運行結(jié)果跟上面是一樣的。

不知道你還記得不記得atomic次伶,這個就修飾了屬性的原子性痴昧,如果直接把屬性修飾改為atomic,會不會就不需要我們加鎖了呢冠王?我試過赶撰,不行!這是因為atomic只會對該屬性的GetterSetter方法上鎖柱彻,而我們很顯然是在別的方法里面對數(shù)據(jù)進(jìn)行操作扣囊,所以并沒什么卵用。同時也因為atomic太耗性能绒疗,所以在實際開發(fā)中侵歇,我們一般都不使用它來修飾變量。

  • GCD

Grand Central Dispatch (GCD)是什么吓蘑?
GCD中文翻譯過來是宏偉的中樞調(diào)度惕虑,是一種基于C語言的并發(fā)編程技術(shù)。它是蘋果為多核的并行運算提出的解決方案磨镶,會自動調(diào)度系統(tǒng)資源溃蔫,所以它的效率很高。
GCD并不直接操作線程琳猫,而是操作隊列和任務(wù)伟叛。我們只需要把任務(wù)添加到隊列里,然后指定任務(wù)執(zhí)行的方式脐嫂,GCD就會自動調(diào)度線程執(zhí)行任務(wù)统刮。

GCD的任務(wù)都是以Block形式存在的。

隊列有兩種:串行隊列/并發(fā)隊列账千。

  • 串行隊列只能等一個任務(wù)執(zhí)行完畢才可以繼續(xù)調(diào)度下一個任務(wù)
    /*  創(chuàng)建一個串行隊列
     *  參數(shù):1.名字2.類型侥蒙,DISPATCH_QUEUE_SERIAL(串行隊列) DISPATCH_QUEUE_CONCURRENT(并發(fā)隊列)
     */
    dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
  • 并發(fā)隊列可以同時調(diào)度多個任務(wù)
    /*  創(chuàng)建一個并發(fā)隊列
     *  參數(shù):1.名字2.類型,DISPATCH_QUEUE_SERIAL(串行隊列) DISPATCH_QUEUE_CONCURRENT(并發(fā)隊列)
     */
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

執(zhí)行任務(wù)也有兩種方式:同步執(zhí)行/異步執(zhí)行匀奏。

  • 同步執(zhí)行會等待當(dāng)前任務(wù)完成才會執(zhí)行下一個任務(wù)鞭衩,不會開啟新線程。
    /*  同步執(zhí)行任務(wù)
     *  參數(shù):1.隊列2.block(任務(wù))
     */
    dispatch_sync(dispatch_queue_t  _Nonnull queue, ^(void)block);
  • 異步執(zhí)行不會等待當(dāng)前任務(wù)完成就會執(zhí)行下一個任務(wù),可以開啟新線程(如果是主隊列论衍,則不會開啟新線程瑞佩,因為主隊列的任務(wù)都會在主線程執(zhí)行)。
    /*  異步執(zhí)行任務(wù)
     *  參數(shù):1.隊列2.block(任務(wù))
     */
    dispatch_async(dispatch_queue_t  _Nonnull queue, ^(void)block);

隊列和任務(wù)都有兩種坯台,排列組合以后就有四種情況炬丸,在不同的情況下,執(zhí)行的結(jié)果可能會有差異捂人,如果不清楚原理比較容易混淆。這里有一個簡單的方法去分析執(zhí)行情況:隊列的類型決定了能不能同時執(zhí)行多個任務(wù)(串行隊列一次只能執(zhí)行一個任務(wù)矢沿,并發(fā)隊列一次可以執(zhí)行多個任務(wù))滥搭,執(zhí)行的方式?jīng)Q定了會不會開啟新線程(同步執(zhí)行不會開啟新線程,異步執(zhí)行可以開啟新線程)捣鲸。

了解以上的基礎(chǔ)瑟匆,我們就可以利用GCD進(jìn)行編程了,我們把創(chuàng)建好的隊列兩個隊列分別放到兩種執(zhí)行方式中栽惶,把這四種情況都演示一遍愁溜,任務(wù)都是在block中打印0-9:

1.同步執(zhí)行串行隊列任務(wù)

    //創(chuàng)建一個串行隊列
    dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);

    //同步執(zhí)行串行隊列任務(wù)
    for (int i = 0; i < 10; i ++) {
        dispatch_sync(serialQueue, ^{
            NSLog(@"%d %@",i,[NSThread currentThread]);
        });
    }

運行結(jié)果:

我們可以看到,同步執(zhí)行的方式并沒有開啟新線程外厂,打印結(jié)果也是順序的冕象。

2.同步執(zhí)行并發(fā)隊列任務(wù)

    //創(chuàng)建一個并發(fā)隊列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

    //同步執(zhí)行并發(fā)隊列任務(wù)
    for (int i = 0; i < 10; i ++) {
        dispatch_sync(concurrentQueue, ^{
            NSLog(@"%d %@",i,[NSThread currentThread]);
        });
    }

運行結(jié)果:

可以看到同步執(zhí)行的情況下,無論是串行隊列還是并發(fā)隊列汁蝶,結(jié)果并沒有區(qū)別渐扮,這是因為在同步執(zhí)行的情況下并不會開啟新的線程,所有任務(wù)都只能在一條線程上執(zhí)行掖棉,而同一條線程上的任務(wù)只能串行執(zhí)行墓律,所以即使并發(fā)隊列擁有同時調(diào)度多個任務(wù)的能力,但是在一條線程的情況下幔亥,也只能等前一個任務(wù)執(zhí)行完畢再調(diào)度新的任務(wù)去執(zhí)行耻讽。所以,在同步執(zhí)行任務(wù)的情況下帕棉,串行隊列和并發(fā)隊列的運行結(jié)果是一致的针肥。

3.異步執(zhí)行串行隊列任務(wù)

    //創(chuàng)建一個串行隊列
    dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);

    //異步執(zhí)行串行隊列任務(wù)
    for (int i = 0; i < 10; i ++) {
        dispatch_async(serialQueue, ^{
            NSLog(@"%d %@",i,[NSThread currentThread]);
        });
    }

運行結(jié)果:

可以看到,這種情況下香伴,任務(wù)是順序執(zhí)行的祖驱,但是它是在子線程執(zhí)行的。這是因為瞒窒,異步執(zhí)行可以開啟新線程捺僻,但是由于是串行隊列,所以任務(wù)只能一個一個順序執(zhí)行。

4.異步執(zhí)行并發(fā)隊列任務(wù)

    //創(chuàng)建一個并發(fā)隊列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

    //異步執(zhí)行并發(fā)隊列任務(wù)
    for (int i = 0; i < 10; i ++) {
        dispatch_async(concurrentQueue, ^{
            NSLog(@"%d %@",i,[NSThread currentThread]);
        });
    }

運行結(jié)果:

可以看到匕坯,這種情況下束昵,執(zhí)行任務(wù)的順序不固定,并且會開啟多條線程同時執(zhí)行葛峻,所以這種時候執(zhí)行任務(wù)的效率最高锹雏。

在實際的開發(fā)中,我們更多運用到的還是異步執(zhí)行术奖,畢竟我們運用多線程技術(shù)是為了在另一條線程上執(zhí)行任務(wù)礁遵,至于選擇串行隊列還是并發(fā)隊列就要根據(jù)實際情況來判斷了:

  1. 如果隊列里的任務(wù)必須按照順序執(zhí)行,那就選擇串行隊列采记。
  2. 如果隊列里的任務(wù)沒有執(zhí)行順序的需求佣耐,那最好選擇并發(fā)隊列,因為并發(fā)隊列的執(zhí)行效率更高唧龄。

系統(tǒng)也為我們提供了兩種隊列兼砖,分別是:全局隊列dispatch_get_global_queue(long identifier, unsigned long flags)主隊列dispatch_get_main_queue()既棺。
全局隊列本質(zhì)上是一個并發(fā)隊列讽挟,可以通過前面的測試來證明,獲取時需要傳遞參數(shù)丸冕,第一個參數(shù)是服務(wù)質(zhì)量的選擇(以前叫優(yōu)先級)耽梅,第二個是保留參數(shù),暫時只需要傳0就可以了:

    //全局隊列1.優(yōu)先級或服務(wù)質(zhì)量胖烛,2.保留參數(shù)褐墅,目前傳0
    /*
     *  優(yōu)先級和服務(wù)質(zhì)量的對應(yīng)關(guān)系:
     *  - DISPATCH_QUEUE_PRIORITY_HIGH:         QOS_CLASS_USER_INITIATED
     *  - DISPATCH_QUEUE_PRIORITY_DEFAULT:      QOS_CLASS_DEFAULT
     *  - DISPATCH_QUEUE_PRIORITY_LOW:          QOS_CLASS_UTILITY
     *  - DISPATCH_QUEUE_PRIORITY_BACKGROUND:   QOS_CLASS_BACKGROUND
     */
    //默認(rèn)優(yōu)先級的全局隊列
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

這里值得注意的是,一般情況下最好不要隨意選擇優(yōu)先級洪己,默認(rèn)就夠用了妥凳。優(yōu)先級本質(zhì)上是一個概率的問題,優(yōu)先級越高答捕,CPU調(diào)度的概率越高逝钥,并不具備確定性,如果出現(xiàn)問題拱镐,很難查找原因艘款。當(dāng)然,如果你很了解這些沃琅,并且就是為了性能和資源的考慮而做了優(yōu)先級的選擇哗咆,那么你可以無視這些。

主隊列不需要參數(shù)可以直接獲取,不過主隊列并不會開啟新線程,主隊列上的所有任務(wù)都只會在主線程上執(zhí)行,所以我們在平時的編程中稽荧,往往是在子線程中處理耗時操作年碘,然后在主線程更新UI澈歉。在實際開發(fā)中,我們經(jīng)常會遇到一種場景屿衅,就是在界面上顯示一張網(wǎng)絡(luò)圖片埃难,要顯示圖片肯定得先下載,而下載是一個耗時操作涤久,如果在主線程下載涡尘,那就會使界面卡住不能進(jìn)行其它操作,所以我們一般都會在子線程下載响迂,下載好以后再去主線程更新界面:

    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_queue_t mainQueue = dispatch_get_main_queue();

    dispatch_async(globalQueue, ^{
        NSLog(@"在子線程執(zhí)行耗時操作考抄!");
        dispatch_async(mainQueue, ^{
            NSLog(@"在主線程更新UI");
        });
    });

以上的使用可以滿足一些簡單業(yè)務(wù)的需求,但是實際開發(fā)中有很多復(fù)雜業(yè)務(wù)栓拜,比如說在用戶登錄的時候需要同步多種信息座泳,而這些信息從不同的接口獲取惠昔,只有所有信息全部同步結(jié)束才可以正常操作幕与,同步各種信息應(yīng)該各自在子線程進(jìn)行,我們可以異步執(zhí)行并發(fā)隊列中的任務(wù)來做這些耗時操作镇防,但是我們怎么知道所有任務(wù)都執(zhí)行完了呢啦鸣?

GCD Group
GCD為我們提供了另一個東西,叫做Group(調(diào)度組)来氧。調(diào)度組是用來協(xié)調(diào)一個或多個任務(wù)提交到隊列異步觸發(fā)的诫给。 應(yīng)用程序可以使用調(diào)度組等待所有調(diào)度組中的所有任務(wù)的完成。

所有異步隊列執(zhí)行完畢后得到一個通知啦扬。

調(diào)度組的使用并不復(fù)雜中狂,它有兩種用法:

    //創(chuàng)建一個調(diào)度組
    dispatch_group_t group = dispatch_group_create();
    //獲取全局隊列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //為隊列添加任務(wù),并且和給定的調(diào)度組關(guān)聯(lián)
    dispatch_group_async(group, queue, ^{
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"同步信息1");
    });
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"同步信息2");
    });
    
    dispatch_group_async(group, queue, ^{
        [NSThread sleepForTimeInterval:.5];
        NSLog(@"同步信息3");
    });

    //所有任務(wù)執(zhí)行完畢通知
    dispatch_group_notify(group, queue, ^{
        NSLog(@"全部都完了");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"更新UI");
        });
    });

或者

    //創(chuàng)建一個調(diào)度組
    dispatch_group_t group = dispatch_group_create();
    //獲取全局隊列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //手動添加一個任務(wù)到該調(diào)度組
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"同步信息1");
        //該任務(wù)執(zhí)行完畢從調(diào)度組移除
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"同步信息2");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:.5];
        NSLog(@"同步信息3");
        dispatch_group_leave(group);
    });

    //等待所有任務(wù)執(zhí)行完畢 參數(shù):1.對應(yīng)的調(diào)度組 2.超時時間
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    //所有任務(wù)執(zhí)行完畢才會來這里
    dispatch_async(queue, ^{
        NSLog(@"全部都完了");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"更新UI");
        });
    });

運行結(jié)果:

兩種用法的運行結(jié)果是相同的扑毡,第二種更加靈活一些胃榕,但是代碼量也相應(yīng)多一些,使用調(diào)度組我們可以更好的對任務(wù)進(jìn)行控制瞄摊,并且在特定的場景滿足我們的需求勋又。靈活使用調(diào)度組,可以讓我們對線程同步控制更加得心應(yīng)手换帜。

GCD還有一個很重要的功能楔壤,就是一次執(zhí)行。用這個代碼塊包含的代碼只會執(zhí)行一次惯驼,在實際開發(fā)中經(jīng)常使用蹲嚣,單例模式一般都會用GCD來做递瑰,因為它效率高:

    for (int i = 0; i < 10; i++) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            NSLog(@"你猜我會執(zhí)行幾次?");
        });
    }

運行以后只會打印一次端铛。

  • NSOperation

NSOperation是對GCD面向?qū)ο蟮姆庋b泣矛。它擁有GCD的高效,也擁有面向?qū)ο蟮木幊趟枷牒滩稀:虶CD類似您朽,它也是把任務(wù)放在隊列里去執(zhí)行,不過它比GCD少了一些概念换淆,但是它也有了一些GCD沒有的功能哗总,接下來我們就從最開始了解NSOperation
Operation翻譯過來是操作的意思倍试,其實跟GCD的任務(wù)是一樣的讯屈。因為它本身就是GCD的封裝,所以在理解上也差不多县习。我們順著GCD的用法來使用NSOperation涮母。
NSOperation本身是個抽象類,我們要使用它就得使用它的子類躁愿。系統(tǒng)給我們提供了兩個叛本,分別是:NSInvocationOperationNSBlockOperation。一種是通過selector的形式添加操作彤钟,一種是以block的形式添加操作来候,我個人更喜歡NSBlockOperation,用起來更方便些逸雹。
分別用這兩種方式創(chuàng)建兩個操作(其實就是GCD的任務(wù)):

    //NSBlockOperation
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"blockOperation");
    }];
    //NSInvocationOperation
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationMethod) object:nil];

- (void)invocationMethod{
    NSLog(@"invocationOperation%@",[NSThread currentThread]);
}

根據(jù)GCD的操作流程营搅,這時候就需要創(chuàng)建隊列了。NSOperation的隊列也有一個類NSOperationQueue梆砸,它的創(chuàng)建也很簡單:

    NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];

然后把前面創(chuàng)建的操作添加到隊列中:

    [operationQueue addOperation:blockOperation];
    [operationQueue addOperation:invocationOperation];

這個地方也不需要指定它的執(zhí)行方式转质,直接把操作添加到隊列中就會自動異步執(zhí)行:

NSOperationQueue本身也有通過block添加操作的方法,不需要我們專門去創(chuàng)建帖世,這樣就進(jìn)一步簡化了代碼:

    NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
    
    [operationQueue addOperationWithBlock:^{
        NSLog(@"1%@",[NSThread currentThread]);
    }];
    [operationQueue addOperationWithBlock:^{
        NSLog(@"2%@",[NSThread currentThread]);
    }];

直接運行:

NSOperationQueue并沒有全局隊列休蟹,但是我們可以自己根據(jù)需求創(chuàng)建全局隊列。NSOperationQueue也有獲取主隊列的類方法[NSOperationQueue mainQueue];狮暑,用起來也很簡單方便鸡挠,跟GCD中的主隊列一樣。

一個子線程處理耗時操作搬男,然后刷新UI的代碼拣展,使用NSOperationQueue的方式就變成了這樣:

    NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];

    [operationQueue addOperationWithBlock:^{
        NSLog(@"子線程處理耗時操作%@", [NSThread currentThread]);
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            NSLog(@"主線程更新UI%@", [NSThread currentThread]);
        }];
    }];

運行結(jié)果:


怎么樣,是不是用起來很簡單缔逛。

在線程同步上备埃,NSOperation 沒有group姓惑,但是有操作依賴,一樣可以實現(xiàn)同樣的效果按脚。它的依賴于毙,是操作的方法,所以如果要使用依賴辅搬,我們就得自己創(chuàng)建操作唯沮,然后操作之間設(shè)置好依賴關(guān)系,再把它們丟到隊列里堪遂,比如說前面GCD中的那個同步信息的例子:

    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"同步信息1");
    }];
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"同步信息2");
    }];
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:.5];
        NSLog(@"同步信息3");
    }];
    NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"更新UI");
    }];
    
    [op4 addDependency:op1];
    [op4 addDependency:op2];
    [op4 addDependency:op3];

    NSOperationQueue *queue = [NSOperationQueue new];
    
    [queue addOperations:@[op1,op2,op3,op4] waitUntilFinished:NO];

這里介蛉,操作4依賴了操作1、2溶褪、3币旧,所以它得等到其它3個操作完成才能開始執(zhí)行,運行結(jié)果:

這里要注意猿妈,操作之間一定不能形成循環(huán)依賴吹菱,循環(huán)依賴的任務(wù)沒辦法執(zhí)行,因為都得再對方執(zhí)行完畢之后才滿足自己的執(zhí)行條件彭则。

這里在給隊列添加操作的時候鳍刷,用了新的方法[queue addOperations:@[op1,op2,op3,op4] waitUntilFinished:NO];,這個方法可以一次性添加多個操作贰剥,但是后面有一個參數(shù)倾剿,看名字我們就可以知道筷频,如果傳YES蚌成,它會一直等所有任務(wù)都執(zhí)行完畢才會繼續(xù)執(zhí)行下面的任務(wù),有點類似于GCD里面group的那個wait凛捏,但是它會卡主當(dāng)前線程担忧,所以不能在主線程中使用,我們也可以在子線程里面執(zhí)行這一段代碼坯癣,稍加改造瓶盛,達(dá)到同樣的效果:

    NSOperationQueue *queue = [NSOperationQueue new];
    
    [queue addOperationWithBlock:^{
        NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
            [NSThread sleepForTimeInterval:1.0];
            NSLog(@"同步信息1");
        }];
        NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"同步信息2");
        }];
        NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
            [NSThread sleepForTimeInterval:.5];
            NSLog(@"同步信息3");
        }];
        
        [queue addOperations:@[op1,op2,op3] waitUntilFinished:YES];
        
        NSLog(@"更新UI");
    }];

運行結(jié)果是一樣的,這里就不貼圖了示罗。

不知道你有沒有注意到惩猫,NSOperationQueue本身并沒有并發(fā)隊列和串行隊列的選項,它默認(rèn)是并發(fā)隊列蚜点,但是轧房,它有一個maxConcurrentOperationCount屬性(代表了最大并發(fā)數(shù),也就是最多能夠開幾條線程執(zhí)行操作)绍绘,如果最大并發(fā)數(shù)量為1奶镶,它就變成了類似串行隊列的模樣迟赃。

NSOperationQueue還可以使用suspended屬性來控制隊列里操作的暫停和繼續(xù)。使用cancelAllOperations方法來取消隊列里的所有操作厂镇。這些簡單的屬性和方法就不專門演示了纤壁。

  • 總結(jié)

以上就是OC中的多線程,在實際開發(fā)中捺信,我們幾乎不會使用pthread酌媒,很少會使用NSThread,不過NSThread的一些類方法會經(jīng)常使用迄靠,比如獲取當(dāng)前線程馍佑,睡眠當(dāng)前線程等。GCDNSOperation使用起來都效率更高梨水,并且操作簡單拭荤,是我們更好的選擇。它倆之間的選擇一般沒有明確的分界線疫诽,可以根據(jù)實際需求來選擇舅世,不過一般中小型項目多使用GCD,大型項目多使用NSOperation奇徒,可能是因為GCD底層雏亚,更一些,而NSOperation規(guī)范摩钙,同時也了些罢低。

與本文相關(guān)的代碼點擊前往

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末胖笛,一起剝皮案震驚了整個濱河市网持,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌长踊,老刑警劉巖功舀,帶你破解...
    沈念sama閱讀 218,640評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異身弊,居然都是意外死亡辟汰,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評論 3 395
  • 文/潘曉璐 我一進(jìn)店門阱佛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帖汞,“玉大人,你說我怎么就攤上這事凑术◆嬲海” “怎么了?”我有些...
    開封第一講書人閱讀 165,011評論 0 355
  • 文/不壞的土叔 我叫張陵麦萤,是天一觀的道長鹿鳖。 經(jīng)常有香客問我扁眯,道長,這世上最難降的妖魔是什么翅帜? 我笑而不...
    開封第一講書人閱讀 58,755評論 1 294
  • 正文 為了忘掉前任姻檀,我火速辦了婚禮,結(jié)果婚禮上涝滴,老公的妹妹穿的比我還像新娘绣版。我一直安慰自己,他們只是感情好歼疮,可當(dāng)我...
    茶點故事閱讀 67,774評論 6 392
  • 文/花漫 我一把揭開白布杂抽。 她就那樣靜靜地躺著,像睡著了一般韩脏。 火紅的嫁衣襯著肌膚如雪缩麸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,610評論 1 305
  • 那天赡矢,我揣著相機與錄音杭朱,去河邊找鬼。 笑死吹散,一個胖子當(dāng)著我的面吹牛弧械,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播空民,決...
    沈念sama閱讀 40,352評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼刃唐,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了界轩?” 一聲冷哼從身側(cè)響起画饥,我...
    開封第一講書人閱讀 39,257評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎耸棒,沒想到半個月后荒澡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體报辱,經(jīng)...
    沈念sama閱讀 45,717評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡与殃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,894評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了碍现。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片幅疼。...
    茶點故事閱讀 40,021評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖昼接,靈堂內(nèi)的尸體忽然破棺而出爽篷,到底是詐尸還是另有隱情,我是刑警寧澤慢睡,帶...
    沈念sama閱讀 35,735評論 5 346
  • 正文 年R本政府宣布逐工,位于F島的核電站铡溪,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏泪喊。R本人自食惡果不足惜棕硫,卻給世界環(huán)境...
    茶點故事閱讀 41,354評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望袒啼。 院中可真熱鬧哈扮,春花似錦、人聲如沸蚓再。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽摘仅。三九已至靶庙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間娃属,已是汗流浹背惶洲。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留膳犹,地道東北人恬吕。 一個月前我還...
    沈念sama閱讀 48,224評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像须床,于是被迫代替她去往敵國和親铐料。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,974評論 2 355

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