OC-多線程的理解和使用

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

  • 什么是線程

線程(英語:thread)是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以并發多個線程,每條線程并行執行不同的任務。

  • 什么是進程

進程(英語:process),是計算機中已運行程序的實體。進程為曾經是分時系統的基本運作單位。在面向進程設計的系統(如早期的UNIX,Linux 2.4及更早的版本)中,進程是程序的基本執行實體;在面向線程設計的系統(如當代多數操作系統、Linux 2.6及更新的版本)中,進程本身不是基本運行單位,而是線程的容器。程序本身只是指令、數據及其組織形式的描述,進程才是程序(那些指令和數據)的真正運行實例。若干進程有可能與同一個程序相關系,且每個進程皆可以同步(循序)或異步(平行)的方式獨立運行。現代計算機系統可在同一段時間內以進程的形式將多個程序加載到內存中,并借由時間共享(或稱時分復用),以在一個處理器上表現出同時(平行性)運行的感覺。同樣的,使用多線程技術(多線程即每一個線程都代表一個進程內的一個獨立執行上下文)的操作系統或計算機架構,同樣程序的平行線程,可在多CPU主機或網絡上真正同時運行(在不同的CPU上)。

  • 什么是程序

程序(英語:procedure),指特定的一系列動作、行動或操作,而這些活動、動作或操作必須以相同方式運行,借此在相同環境下恒常得出相同的結果(例如緊急應變程序)。粗略而言,程序可以指一序列的活動、作業、步驟、決斷、計算和工序,當它們保證依照嚴格規定的順序發生時即產生所述的后果、產品或局面。一個程序通常引致一個改變。現在小孩也可以寫程序。

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

今天,我們談談在OC中,多線程的幾種方案。其中pthread是純C語言的一套線程管理方案,由于在iOS開發中基本上不會使用,我們這里不做討論。我們重點討論以下幾種方案:

  • NSThread

NSThread有兩種創建方法,一種是類方法,一種是實例方法,類方法創建的時候需要確定好執行的任務,沒有任何返回,它會自己創建一個新線程去執行指定任務,而用實例方法則需要我們手動開啟這個線程。創建的時候我們既可以使用SEL也可以使用Block,為了簡化代碼,我們都使用Block來創建。

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

運行結果:



我們可以看到這兩個線程是完全異步的。運行幾次,結果也不相同。

值得注意的是,一個NSThread線程的啟動,有三種方式,我們前面已經看到了兩種,一種是類方法直接創建并啟動,另一種是[thread start];方法,還有一種是[thread main];,但是蘋果并不建議我們直接使用main方法,它可以在子類化的時候重寫并實現你的線程主體,而不用調用super,任何時候,啟動線程都應該用start方法。

前面我們簡單得用NSThread的方式創建了兩個線程,并且讓它們各自執行了自己的任務。但是NSThread可不止這么幾個方法,它還有很多比較有用的方法:

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

接下來,我們用經典的賣票問題來模擬并解決NSThread中的線程同步問題。有兩個售票員,同時開始賣票,一共有20張票,模擬該場景:

@property (nonatomic, assign) NSInteger tickets;

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

    //創建兩個線程來充當兩個售票員
    [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);
        }
    }];

運行結果:


通過打印結果,可以看出,一共賣出去了26張票,超出了我們的票的總數,這是同一張票被多次出售的結果。為了避免這種情況,通常的做法就是上鎖,當某一條線程對數據進行操作時,先給數據上鎖,別的線程阻塞,等到這條線程操作結束,在開鎖,別的線程再進去,上鎖,操作,開鎖……這樣就保證了數據的安全性。

我們可以使用@synchronized (object) {}來進行上鎖,括號里的參數可以填任意對象,但是要注意的是,必須填寫線程共有的變量才能實現上鎖,局部變量是無效的,原因是,如果用局部變量,就會創建多個鎖,這些鎖之間并無關聯,所以與不上鎖沒有區別:

//創建兩個線程來充當兩個售票員
    [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);
            }
        }
    }];

運行結果:

因為在鎖內進行數據操作時,其它線程都會阻塞在外面,這個時候,其實線程不是并發執行的,所以我們不難想到,鎖內執行的任務越少,那么這段代碼執行的效率就越高。在此基礎上,我們可以對前面的加鎖進行一個小修改:

    //創建兩個線程來充當兩個售票員
    [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);
            }
        }
    }];

運行結果:

注意看一下時間,修改以后,我們的賣票效率提升了一倍,之前那種方式要20秒才能賣完,現在只需要10秒。

當然也可以用NSLock來進行上鎖,使用NSLock需要創建一個NSLock實例,然后調用lockunlock方法來進行加鎖和解鎖的操作:

@property (nonatomic, strong) NSLock *lock;

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

    //創建兩個線程來充當兩個售票員
    [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];
        }
    }];

運行結果跟上面是一樣的。

不知道你還記得不記得atomic,這個就修飾了屬性的原子性,如果直接把屬性修飾改為atomic,會不會就不需要我們加鎖了呢?我試過,不行!這是因為atomic只會對該屬性的GetterSetter方法上鎖,而我們很顯然是在別的方法里面對數據進行操作,所以并沒什么卵用。同時也因為atomic太耗性能,所以在實際開發中,我們一般都不使用它來修飾變量。

  • GCD

Grand Central Dispatch (GCD)是什么?
GCD中文翻譯過來是宏偉的中樞調度,是一種基于C語言的并發編程技術。它是蘋果為多核的并行運算提出的解決方案,會自動調度系統資源,所以它的效率很高。
GCD并不直接操作線程,而是操作隊列和任務。我們只需要把任務添加到隊列里,然后指定任務執行的方式,GCD就會自動調度線程執行任務。

GCD的任務都是以Block形式存在的。

隊列有兩種:串行隊列/并發隊列

  • 串行隊列只能等一個任務執行完畢才可以繼續調度下一個任務
    /*  創建一個串行隊列
     *  參數:1.名字2.類型,DISPATCH_QUEUE_SERIAL(串行隊列) DISPATCH_QUEUE_CONCURRENT(并發隊列)
     */
    dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
  • 并發隊列可以同時調度多個任務
    /*  創建一個并發隊列
     *  參數:1.名字2.類型,DISPATCH_QUEUE_SERIAL(串行隊列) DISPATCH_QUEUE_CONCURRENT(并發隊列)
     */
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

執行任務也有兩種方式:同步執行/異步執行

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

隊列和任務都有兩種,排列組合以后就有四種情況,在不同的情況下,執行的結果可能會有差異,如果不清楚原理比較容易混淆。這里有一個簡單的方法去分析執行情況:隊列的類型決定了能不能同時執行多個任務(串行隊列一次只能執行一個任務,并發隊列一次可以執行多個任務),執行的方式決定了會不會開啟新線程(同步執行不會開啟新線程,異步執行可以開啟新線程)。

了解以上的基礎,我們就可以利用GCD進行編程了,我們把創建好的隊列兩個隊列分別放到兩種執行方式中,把這四種情況都演示一遍,任務都是在block中打印0-9:

1.同步執行串行隊列任務

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

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

運行結果:

我們可以看到,同步執行的方式并沒有開啟新線程,打印結果也是順序的。

2.同步執行并發隊列任務

    //創建一個并發隊列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

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

運行結果:

可以看到同步執行的情況下,無論是串行隊列還是并發隊列,結果并沒有區別,這是因為在同步執行的情況下并不會開啟新的線程,所有任務都只能在一條線程上執行,而同一條線程上的任務只能串行執行,所以即使并發隊列擁有同時調度多個任務的能力,但是在一條線程的情況下,也只能等前一個任務執行完畢再調度新的任務去執行。所以,在同步執行任務的情況下,串行隊列和并發隊列的運行結果是一致的。

3.異步執行串行隊列任務

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

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

運行結果:

可以看到,這種情況下,任務是順序執行的,但是它是在子線程執行的。這是因為,異步執行可以開啟新線程,但是由于是串行隊列,所以任務只能一個一個順序執行。

4.異步執行并發隊列任務

    //創建一個并發隊列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

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

運行結果:

可以看到,這種情況下,執行任務的順序不固定,并且會開啟多條線程同時執行,所以這種時候執行任務的效率最高。

在實際的開發中,我們更多運用到的還是異步執行,畢竟我們運用多線程技術是為了在另一條線程上執行任務,至于選擇串行隊列還是并發隊列就要根據實際情況來判斷了:

  1. 如果隊列里的任務必須按照順序執行,那就選擇串行隊列。
  2. 如果隊列里的任務沒有執行順序的需求,那最好選擇并發隊列,因為并發隊列的執行效率更高。

系統也為我們提供了兩種隊列,分別是:全局隊列dispatch_get_global_queue(long identifier, unsigned long flags)主隊列dispatch_get_main_queue()
全局隊列本質上是一個并發隊列,可以通過前面的測試來證明,獲取時需要傳遞參數,第一個參數是服務質量的選擇(以前叫優先級),第二個是保留參數,暫時只需要傳0就可以了:

    //全局隊列1.優先級或服務質量,2.保留參數,目前傳0
    /*
     *  優先級和服務質量的對應關系:
     *  - 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
     */
    //默認優先級的全局隊列
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

這里值得注意的是,一般情況下最好不要隨意選擇優先級,默認就夠用了。優先級本質上是一個概率的問題,優先級越高,CPU調度的概率越高,并不具備確定性,如果出現問題,很難查找原因。當然,如果你很了解這些,并且就是為了性能和資源的考慮而做了優先級的選擇,那么你可以無視這些。

主隊列不需要參數可以直接獲取,不過主隊列并不會開啟新線程,主隊列上的所有任務都只會在主線程上執行,所以我們在平時的編程中,往往是在子線程中處理耗時操作,然后在主線程更新UI。在實際開發中,我們經常會遇到一種場景,就是在界面上顯示一張網絡圖片,要顯示圖片肯定得先下載,而下載是一個耗時操作,如果在主線程下載,那就會使界面卡住不能進行其它操作,所以我們一般都會在子線程下載,下載好以后再去主線程更新界面:

    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(@"在子線程執行耗時操作!");
        dispatch_async(mainQueue, ^{
            NSLog(@"在主線程更新UI");
        });
    });

以上的使用可以滿足一些簡單業務的需求,但是實際開發中有很多復雜業務,比如說在用戶登錄的時候需要同步多種信息,而這些信息從不同的接口獲取,只有所有信息全部同步結束才可以正常操作,同步各種信息應該各自在子線程進行,我們可以異步執行并發隊列中的任務來做這些耗時操作,但是我們怎么知道所有任務都執行完了呢?

GCD Group
GCD為我們提供了另一個東西,叫做Group(調度組)。調度組是用來協調一個或多個任務提交到隊列異步觸發的。 應用程序可以使用調度組等待所有調度組中的所有任務的完成。

所有異步隊列執行完畢后得到一個通知。

調度組的使用并不復雜,它有兩種用法:

    //創建一個調度組
    dispatch_group_t group = dispatch_group_create();
    //獲取全局隊列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //為隊列添加任務,并且和給定的調度組關聯
    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");
    });

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

或者

    //創建一個調度組
    dispatch_group_t group = dispatch_group_create();
    //獲取全局隊列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //手動添加一個任務到該調度組
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"同步信息1");
        //該任務執行完畢從調度組移除
        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);
    });

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

運行結果:

兩種用法的運行結果是相同的,第二種更加靈活一些,但是代碼量也相應多一些,使用調度組我們可以更好的對任務進行控制,并且在特定的場景滿足我們的需求。靈活使用調度組,可以讓我們對線程同步控制更加得心應手。

GCD還有一個很重要的功能,就是一次執行。用這個代碼塊包含的代碼只會執行一次,在實際開發中經常使用,單例模式一般都會用GCD來做,因為它效率高:

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

運行以后只會打印一次。

  • NSOperation

NSOperation是對GCD面向對象的封裝。它擁有GCD的高效,也擁有面向對象的編程思想。和GCD類似,它也是把任務放在隊列里去執行,不過它比GCD少了一些概念,但是它也有了一些GCD沒有的功能,接下來我們就從最開始了解NSOperation
Operation翻譯過來是操作的意思,其實跟GCD的任務是一樣的。因為它本身就是GCD的封裝,所以在理解上也差不多。我們順著GCD的用法來使用NSOperation
NSOperation本身是個抽象類,我們要使用它就得使用它的子類。系統給我們提供了兩個,分別是:NSInvocationOperationNSBlockOperation。一種是通過selector的形式添加操作,一種是以block的形式添加操作,我個人更喜歡NSBlockOperation,用起來更方便些。
分別用這兩種方式創建兩個操作(其實就是GCD的任務):

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

根據GCD的操作流程,這時候就需要創建隊列了。NSOperation的隊列也有一個類NSOperationQueue,它的創建也很簡單:

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

然后把前面創建的操作添加到隊列中:

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

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

NSOperationQueue本身也有通過block添加操作的方法,不需要我們專門去創建,這樣就進一步簡化了代碼:

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

直接運行:

NSOperationQueue并沒有全局隊列,但是我們可以自己根據需求創建全局隊列。NSOperationQueue也有獲取主隊列的類方法[NSOperationQueue mainQueue];,用起來也很簡單方便,跟GCD中的主隊列一樣。

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

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

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

運行結果:


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

在線程同步上,NSOperation 沒有group,但是有操作依賴,一樣可以實現同樣的效果。它的依賴,是操作的方法,所以如果要使用依賴,我們就得自己創建操作,然后操作之間設置好依賴關系,再把它們丟到隊列里,比如說前面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個操作完成才能開始執行,運行結果:

這里要注意,操作之間一定不能形成循環依賴,循環依賴的任務沒辦法執行,因為都得再對方執行完畢之后才滿足自己的執行條件。

這里在給隊列添加操作的時候,用了新的方法[queue addOperations:@[op1,op2,op3,op4] waitUntilFinished:NO];,這個方法可以一次性添加多個操作,但是后面有一個參數,看名字我們就可以知道,如果傳YES,它會一直等所有任務都執行完畢才會繼續執行下面的任務,有點類似于GCD里面group的那個wait,但是它會卡主當前線程,所以不能在主線程中使用,我們也可以在子線程里面執行這一段代碼,稍加改造,達到同樣的效果:

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

運行結果是一樣的,這里就不貼圖了。

不知道你有沒有注意到,NSOperationQueue本身并沒有并發隊列和串行隊列的選項,它默認是并發隊列,但是,它有一個maxConcurrentOperationCount屬性(代表了最大并發數,也就是最多能夠開幾條線程執行操作),如果最大并發數量為1,它就變成了類似串行隊列的模樣。

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

  • 總結

以上就是OC中的多線程,在實際開發中,我們幾乎不會使用pthread,很少會使用NSThread,不過NSThread的一些類方法會經常使用,比如獲取當前線程,睡眠當前線程等。GCDNSOperation使用起來都效率更高,并且操作簡單,是我們更好的選擇。它倆之間的選擇一般沒有明確的分界線,可以根據實際需求來選擇,不過一般中小型項目多使用GCD,大型項目多使用NSOperation,可能是因為GCD底層,更一些,而NSOperation規范,同時也了些。

與本文相關的代碼點擊前往

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

推薦閱讀更多精彩內容