iOS多執行緒的那些事兒

碼碼碼碼上有錢發表於2018-02-14

要想掌握多執行緒,就有必要了解一些相關的基本概念,比方說什麼是執行緒?要了解執行緒肯定會牽涉到程式,那什麼又是程式?明白這些之後,還要明白多執行緒是怎麼回事?為什麼要用多執行緒?瞬時有點懵逼,一堆的概念。但要了解多執行緒,並且要會用它,這些玩意還是跳不過。既然事情比繁瑣,那就拆分其化繁為簡,一個一個地去了解。

  • 程式
    程式是計算機中的程式關於某資料集合上的一次執行活動,是系統進行資源分配和排程的基本單位,是作業系統結構的基礎。

上面是百科的概念,翻譯成人話就是:程式是系統正在執行的應用程式,一個應用程式即一程式,程式與程式間是相互獨立的,且執行在其專用且受保護的記憶體空間中。

  • 執行緒

    執行緒是程式中一個單一的順序控制流程。程式內有一個相對獨立的、可排程的執行單元,是系統獨立排程和分派CPU的基本單位指令執行時的程式的排程單位。

正常的理解是程式要想執行任務,得有執行緒。這樣一來,程式中得至少有一個執行緒,這執行緒即為主執行緒。

執行緒的任務可以多個,但同一時間執行緒只能執行一個任務,即多工的執行是按順序的,執行完了一個任務再接著執行下一個任務。

關於程式與執行緒間的區別,知乎上有個大牛解釋地非常到位,傳送程式與執行緒的區別

  • 多執行緒

    多執行緒(multithreading),是指從軟體或者硬體上實現多個執行緒併發執行的技術。具有多執行緒能力的計算機因有硬體支援而能夠在同一時間執行多於一個執行緒,進而提升整體處理效能。

    而事實上呢,CPU在同一時間只能處理一個執行緒,不能多個執行緒同時處理。那多執行緒一說從何而來?那是因為CPU在切換排程不同執行緒比較快,時間很短。所以才會給我們的感覺就是多個任務同時在併發進行著的假象。

    合理地利用多執行緒,可以提高應用程式的執行效率,及提高CPU/記憶體的利用率。但凡事都有個度,過度使用多執行緒也會導致佔用過高的記憶體(預設情況下,主執行緒佔用1M,子執行緒佔用512K記憶體空間)及增加CPU的開銷,從而降低應用程式的效能。

接下來是多執行緒的主要內容,iOS開發中多執行緒的常使用方式有三種,分別是:

  • NSThread

    每個NSThread的物件對應著一個執行緒,輕量級別,比較簡單。但需要程式設計師手動管理執行緒的所有活動,如執行緒的生命週期、睡眠、同步。且管理多個執行緒比較困難,所以NSThread開發上相對比較少使用

  • GCD

    apple官方推薦的高效多執行緒程式設計解決方案,基於C。使用block定義任務,使用起來靈活方便。執行緒生命週期自動管理,編碼時可更注重邏輯實現。

  • NSOperation/NSOPerationQueue

    NSOperation基於GCD實現的一套Objective-C的API,是物件導向的執行緒技術,執行緒的生命週期自動管理,程式設計師可更加註重邏輯;較於GCD更容易實現限制最大併發量,操作之間的依賴關係。

綜合上述三種多執行緒實現的方式,NSThread由於在處理多個執行緒上,存在困難,較少使用。NSOperation、GCD執行緒生命週期自動管理,程式設計師只需要注重業務邏輯即可。GCD基於C實現,效能最為高效。NSOperation基於GCD封裝的一套API,使用更加物件導向,更為簡單,且在控制任務最大併發、處理依賴關係上更為方便。

NSThread

NSThread建立的方式有三種,分別如下:

//thread建立方式1
- (void)threadCreateMethod1 { 
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(threadImageDownload:) object:opImageUrl];
    [thread1 start];
}

//thread建立方式2
- (void)threadCreateMethod2 {
    [NSThread detachNewThreadSelector:@selector(threadImageDownload:) toTarget:self withObject:opImageUrl2];
}

//thread建立方式3
- (void)threadCreateMethod3 {
    [self performSelectorInBackground:@selector(threadImageDownload:) withObject:opImageUrl3];
}

#pragma mark - downloadMethod

- (void)threadImageDownload:(NSString *)imageUrl {
    //如果當前執行緒被取消,則return
    if ([[NSThread currentThread] isCancelled]) {
        return;
    }
    //
    NSURL *requestUrl = [NSURL URLWithString:imageUrl];
    NSData *imageData = [NSData dataWithContentsOfURL:requestUrl];
    if (imageData) {
        [self performSelectorOnMainThread:@selector(refreshUiImage:) withObject:imageData waitUntilDone:YES];
    }
}

- (void)refreshUiImage:(NSData *)imageData {
    UIImage *downloadImage = [UIImage imageWithData:imageData];
    self.contentImageView.image = downloadImage;
}

複製程式碼

NSThread還有一些屬性:

//是否在執行中
thread1.isExecuting;
//是否為主執行緒
thread1.isMainThread;
//是否被取消
thread1.isCancelled;
//是否已經完成
thread1.isFinished;
//執行緒的堆疊大小,執行緒執行前堆疊大小為512K,執行緒完成後堆疊大小為0K 值得一提的是執行緒在執行完畢後,由於記憶體空間被釋放,不能再次啟動
thread1.stackSize;
//執行緒優先順序
thread1.threadPriority;
複製程式碼

例項方法

//執行緒開始,執行緒加入執行緒池等待CPU排程(並非真正開始執行,只是通常等待時間都非常短,看不出效果)
[thread1 start];
//標記執行緒被取消執行
[thread1 cancel]
複製程式碼

類方法

//是否為多執行緒
+ (BOOL)isMultiThreaded;
//執行緒阻塞方法
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
//執行緒退出,釋放記憶體空間,執行緒不可再啟動。(cancel方法,只是標誌執行緒取消執行,並非退出,exit為執行緒退出)
+ (void)exit;
//設定執行緒優先順序,值區間0~1,預設為0.5。這裡要注意的是,在多執行緒的執行中,某執行緒設定優先順序為1,並不是CPU會先排程完了該執行緒再去排程其它執行緒的意思,只是優先順序高的執行緒在排程頻率上會高於基於優先順序低的執行緒。
+ (BOOL)setThreadPriority:(double)p;
複製程式碼

GCD

Grand Central Dispatch (GCD) 是Apple開發的一個多核程式設計的較新的解決方法。它主要用於優化應用程式以支援多核處理器以及其他對稱多處理系統。它是一個線上程池模式的基礎上執行的並行任務。——百度百科

這是apple官方推薦基於C實現的一套高效的多執行緒解決方案,使用方便,優點多:

1、GCD可用於多核的並行運算

2、GCD會自動利用更多的CPU核心(比如雙核、四核)

3、GCD會自動管理執行緒的生命週期(建立執行緒、排程任務、銷燬執行緒)

4、程式設計師只需要告訴GCD想要執行什麼任務,不需要編寫任何執行緒管理程式碼。

只需要把要執行的任務扔進去即可,簡直6到不行啊。平時的開發中也經常使用,不僅因為它功能強大,簡單易用,且它是以block方式呈現出來的。程式碼集中度也高,更為直觀。說了這麼多,來看看它具體怎麼用吧。

老規則,先了解一些概念性的東西:任務、佇列

  • 任務:任務就是要執行的操作,簡單來說就是一段要處理某個業務的邏輯程式碼。在GCD中來看的話,任務就是block裡面的程式碼。執行任務的時候有兩方式:同步執行非同步執行,這兩者的區別就在於開闢新執行緒的能力。

    • 同步執行(sync):只能在當前執行緒中執行任務,不具備開啟新執行緒的能力
    • 非同步執行(async):可以在新的執行緒中執行任務,具備開啟新執行緒的能力,但在主執行緒執行非同步操作,是不會開闢新執行緒的。
  • 佇列:這裡的佇列指任務佇列,即用來存放及管理任務的佇列。採用FIFO(先進先出)的原則,即新任務總是被插入到佇列的末尾,而讀取任務的時候總是從佇列的頭部開始讀取。每讀取一個任務,則從佇列中釋放一個任務。在GCD中有兩種佇列:序列佇列併發佇列

    • 併發佇列:系統排程時,同時建立多個執行緒,執行多個任務,且併發功能只有在非同步(dispatch_async)函式下才有效;
    • 序列佇列(Serial Dispatch Queue):讓任務一個接著一個地執行(一個任務執行完畢後,再執行下一個任務)

GCD使用的步驟:第一步:建立佇列,第二步執行任務。

建立佇列

//label引數為佇列標識,用於debug除錯使用;attr為佇列型別,DISPATCH_QUEUE_SERIAL為序列佇列,DISPATCH_QUEUE_CONCURRENT為併發佇列
dispatch_queue_create(const char * _Nullable label, dispatch_queue_attr_t  _Nullable attr)

//序列佇列
dispatch_queue_t serialQueue = dispatch_queue_create("com.nevercopy.serial", DISPATCH_QUEUE_SERIAL);
//併發佇列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.nevercopy.concurrent", DISPATCH_QUEUE_CONCURRENT);
//特殊的序列佇列,主佇列
dispatch_queue_t mainQueue = dispatch_get_main_queue();

//identifier優先順序,flags未使用的保留值,為0(iOS8 開始使用 QOS(服務質量) 替代了原有的優先順序。獲取全域性併發佇列時,直接傳遞 0,可以實現 iOS 7 & iOS 8 later 的適配。)
dispatch_get_global_queue(long identifier, unsigned long flags); 
//全域性佇列,apple為方便開發者設立的全域性佇列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//iOS8以後可以直接 
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
複製程式碼

建立任務

上述講到任務的執行方式有兩種,分別是『同步執行』與『非同步執行』,那兩種不同的執行方式在不同佇列型別下的表現如何?先看序列佇列下的同步執行與非同步執行:

序列佇列下的任務同步執行

- (void)serialQueueSyncMethod {
    NSLog(@"_____serialQueueSyncMethod start_____");
    //NULL為序列佇列
    dispatch_queue_t serialQueue = dispatch_queue_create("com.nevercopy.serial", NULL);
    
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread1 is %@",[NSThread currentThread]);
        }
    });
    
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread2 is %@",[NSThread currentThread]);
        };
    });
    
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread3 is %@",[NSThread currentThread]);
        }
    });
    NSLog(@"_____serialQueueSyncMethod end_____");
}
複製程式碼

列印結果為:

2018-02-10 14:49:25.849676+0800 NCImageDownloader[2004:172084] _____serialQueueSyncMethod start_____
2018-02-10 14:49:25.849876+0800 NCImageDownloader[2004:172084] thread1 is <NSThread: 0x608000260c00>{number = 1, name = main}
2018-02-10 14:49:25.850013+0800 NCImageDownloader[2004:172084] thread1 is <NSThread: 0x608000260c00>{number = 1, name = main}
2018-02-10 14:49:25.850124+0800 NCImageDownloader[2004:172084] thread2 is <NSThread: 0x608000260c00>{number = 1, name = main}
2018-02-10 14:49:25.850209+0800 NCImageDownloader[2004:172084] thread2 is <NSThread: 0x608000260c00>{number = 1, name = main}
2018-02-10 14:49:25.850325+0800 NCImageDownloader[2004:172084] thread3 is <NSThread: 0x608000260c00>{number = 1, name = main}
2018-02-10 14:49:25.850455+0800 NCImageDownloader[2004:172084] thread3 is <NSThread: 0x608000260c00>{number = 1, name = main}
2018-02-10 14:49:25.850566+0800 NCImageDownloader[2004:172084] _____serialQueueSyncMethod end_____
複製程式碼

結果顯示序列佇列下同步執行任務,不會開啟新執行緒,任務一個完成後接下一個任務執行,序列執行方式。

序列佇列下的任務非同步執行

- (void)serialQueueAsyncMethod {
    NSLog(@"_____serialQueueAsyncMethod start_____");
    //NULL為序列佇列
    dispatch_queue_t serialQueue = dispatch_queue_create("com.nevercopy.serial", NULL);
    
    dispatch_async(serialQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread1 is %@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(serialQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread2 is %@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(serialQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread3 is %@",[NSThread currentThread]);
        }
    });
    NSLog(@"_____serialQueueAsyncMethod end_____");
}
複製程式碼

列印結果為:

2018-02-10 14:49:25.850664+0800 NCImageDownloader[2004:172084] _____serialQueueAsyncMethod start_____
2018-02-10 14:49:25.850758+0800 NCImageDownloader[2004:172084] _____serialQueueAsyncMethod end_____
2018-02-10 14:49:25.850837+0800 NCImageDownloader[2004:172141] thread1 is <NSThread: 0x604000463c40>{number = 4, name = (null)}
2018-02-10 14:49:25.850964+0800 NCImageDownloader[2004:172141] thread1 is <NSThread: 0x604000463c40>{number = 4, name = (null)}
2018-02-10 14:49:25.851080+0800 NCImageDownloader[2004:172141] thread2 is <NSThread: 0x604000463c40>{number = 4, name = (null)}
2018-02-10 14:49:25.851209+0800 NCImageDownloader[2004:172141] thread2 is <NSThread: 0x604000463c40>{number = 4, name = (null)}
2018-02-10 14:49:25.851318+0800 NCImageDownloader[2004:172141] thread3 is <NSThread: 0x604000463c40>{number = 4, name = (null)}
2018-02-10 14:49:25.851426+0800 NCImageDownloader[2004:172141] thread3 is <NSThread: 0x604000463c40>{number = 4, name = (null)}
複製程式碼

結果顯示序列佇列下非同步執行任務,會開啟新執行緒,任務一個完成後接下一個任務執行,序列執行方式。

併發佇列下的任務同步執行

- (void)concurrentQueueSyncMethod {
    NSLog(@"_____concurrentQueueSyncMethod start_____"); 
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.nevercopy.concurrentS", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_sync(concurrentQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"thread1 is %@",[NSThread currentThread]);
        }
    });
    
    dispatch_sync(concurrentQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread2 is %@",[NSThread currentThread]);
        }
    });
    
    
    dispatch_sync(concurrentQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread3 is %@",[NSThread currentThread]);
        }
    });
    NSLog(@"_____concurrentQueueSyncMethod end_____");
}
複製程式碼

列印結果為:

2018-02-10 15:02:50.870721+0800 NCImageDownloader[2065:180191] _____concurrentQueueSyncMethod start_____
2018-02-10 15:02:50.870914+0800 NCImageDownloader[2065:180191] thread1 is <NSThread: 0x60000006f700>{number = 1, name = main}
2018-02-10 15:02:50.871023+0800 NCImageDownloader[2065:180191] thread1 is <NSThread: 0x60000006f700>{number = 1, name = main}
2018-02-10 15:02:50.871121+0800 NCImageDownloader[2065:180191] thread2 is <NSThread: 0x60000006f700>{number = 1, name = main}
2018-02-10 15:02:50.871243+0800 NCImageDownloader[2065:180191] thread2 is <NSThread: 0x60000006f700>{number = 1, name = main}
2018-02-10 15:02:50.871329+0800 NCImageDownloader[2065:180191] thread3 is <NSThread: 0x60000006f700>{number = 1, name = main}
2018-02-10 15:02:50.871419+0800 NCImageDownloader[2065:180191] thread3 is <NSThread: 0x60000006f700>{number = 1, name = main}
2018-02-10 15:02:50.871512+0800 NCImageDownloader[2065:180191] _____concurrentQueueSyncMethod end_____
複製程式碼

結果顯示併發佇列下同步執行任務,不會開啟新執行緒,任務一個完成後接下一個任務執行,序列執行方式。

併發佇列下的任務非同步執行

- (void)concurrentQueueAsyncMethod {
    NSLog(@"_____concurrentQueueAsyncMethod start_____"); 
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.nevercopy.concurrentA", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"thread1 is %@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread2 is %@",[NSThread currentThread]);
        }
    });
    
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread3 is %@",[NSThread currentThread]);
        }
    });
    NSLog(@"_____concurrentQueueAsyncMethod end_____");
}
複製程式碼

列印結果為:

2018-02-10 15:05:39.025175+0800 NCImageDownloader[2080:181771] _____concurrentQueueAsyncMethod start_____
2018-02-10 15:05:39.025341+0800 NCImageDownloader[2080:181771] _____concurrentQueueAsyncMethod end_____
2018-02-10 15:05:39.025576+0800 NCImageDownloader[2080:182352] thread2 is <NSThread: 0x604000279c40>{number = 5, name = (null)}
2018-02-10 15:05:39.025583+0800 NCImageDownloader[2080:182364] thread3 is <NSThread: 0x60c0002722c0>{number = 6, name = (null)}
2018-02-10 15:05:39.025606+0800 NCImageDownloader[2080:181994] thread1 is <NSThread: 0x600000279ac0>{number = 4, name = (null)}
2018-02-10 15:05:39.025775+0800 NCImageDownloader[2080:182364] thread3 is <NSThread: 0x60c0002722c0>{number = 6, name = (null)}
2018-02-10 15:05:39.025780+0800 NCImageDownloader[2080:181994] thread1 is <NSThread: 0x600000279ac0>{number = 4, name = (null)}
2018-02-10 15:05:39.025783+0800 NCImageDownloader[2080:182352] thread2 is <NSThread: 0x604000279c40>{number = 5, name = (null)}
複製程式碼

結果顯示併發佇列下非同步執行任務,會開啟新執行緒,任務交替同時執行的,並行執行方式。

還有一特殊的序列佇列:主佇列

主佇列下的任務同步執行

- (void)mainQueueSyncMehtod {
    NSLog(@"_____mainQueueSyncMehtod start_____");
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    dispatch_sync(mainQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"thread1 is %@",[NSThread currentThread]);
        }
    });
    
    dispatch_sync(mainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread2 is %@",[NSThread currentThread]);
        }
    });
    
    
    dispatch_sync(mainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread3 is %@",[NSThread currentThread]);
        }
    });
    NSLog(@"_____mainQueueSyncMehtod end_____");
}
複製程式碼

執行崩潰:

iOS多執行緒的那些事兒

列印情況:

2018-02-10 15:25:10.070468+0800 NCImageDownloader[2143:192405] _____mainQueueSyncMehtod start_____
(lldb) 
複製程式碼

只列印了_____mainQueueSyncMehtod start_____就崩潰了。 為什麼,往主隊伍新增同步任務,會執行崩潰?原因啊,是因為往主佇列新增同步任務後,任務會立馬執行。但主執行緒現在正在呼叫mainQueueSyncMehtod方法,必須等mainQueueSyncMehtod方法執行完了之後,才會執行後面的任務。但mainQueueSyncMehtod必須等任務1執行完了之後,再執行後面的兩個任務才算執行完畢。所以現在的情況是mainQueueSyncMehtod等任務1執行完,任務1等mainQueueSyncMehtod方法執行完,互相等待,沒完沒了。

主佇列下的任務非同步執行

- (void)mainQueueAsyncMehtod {
    NSLog(@"_____mainQueueAsyncMehtod start_____");
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; i++) {
            NSLog(@"thread1 is %@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread2 is %@",[NSThread currentThread]);
        }
    });
    
    
    dispatch_async(mainQueue, ^{
        for (int i = 0; i < 2; ++i) {
            NSLog(@"thread3 is %@",[NSThread currentThread]);
        }
    });
    NSLog(@"_____mainQueueAsyncMehtod end_____");
}
複製程式碼

列印結果為:

2018-02-10 15:38:07.394448+0800 NCImageDownloader[2184:200006] _____mainQueueAsyncMehtod start_____
2018-02-10 15:38:07.394573+0800 NCImageDownloader[2184:200006] _____mainQueueAsyncMehtod end_____
2018-02-10 15:38:07.417503+0800 NCImageDownloader[2184:200006] thread1 is <NSThread: 0x600000260a40>{number = 1, name = main}
2018-02-10 15:38:07.417755+0800 NCImageDownloader[2184:200006] thread1 is <NSThread: 0x600000260a40>{number = 1, name = main}
2018-02-10 15:38:07.417865+0800 NCImageDownloader[2184:200006] thread2 is <NSThread: 0x600000260a40>{number = 1, name = main}
2018-02-10 15:38:07.418003+0800 NCImageDownloader[2184:200006] thread2 is <NSThread: 0x600000260a40>{number = 1, name = main}
2018-02-10 15:38:07.418122+0800 NCImageDownloader[2184:200006] thread3 is <NSThread: 0x600000260a40>{number = 1, name = main}
2018-02-10 15:38:07.418227+0800 NCImageDownloader[2184:200006] thread3 is <NSThread: 0x600000260a40>{number = 1, name = main}
複製程式碼

結果顯示主佇列下非同步執行任務,並不會開啟新執行緒,任務執行一個後再執行下一個任務,序列執行方式。

GCD的執行緒間的通訊

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    //耗時操作,如什麼下載,上傳,大量資料儲存、檔案寫入,讀取等
    // 迴歸主執行緒
    dispatch_async(dispatch_get_main_queue(), ^{
        //重新整理UI,同步實時狀態
    });
}); 
複製程式碼

看上去沒那麼難,對吧。

GCD一次性執行

最常用的單例,或者是有什麼只要執行一次程式碼段:

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    //write your code
});
複製程式碼

GCD的延時執行

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //2s後面執行這裡的程式碼
    });
複製程式碼

GCD的迴圈

dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index) {
        //迴圈10次
    });
複製程式碼

GCD柵欄方法

dispatch_barrier_sync 與 dispatch_barrier_Async 按字面來理解,這方法起到一堵障礙的作用。如果開發中有需要非同步執行兩組操作,第二組操作須在第一組操作完成之後再執行,那這堵牆就有作用了。

dispatch_barrier_sync

- (void)gcdBarrier
{
    dispatch_queue_t queue = dispatch_queue_create("gcdBarrier", NULL);
    
    dispatch_async(queue, ^{
        NSLog(@"----1-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----2-----%@", [NSThread currentThread]);
    });
    
    dispatch_barrier_sync(queue, ^{
        //讓它睡1s
        sleep(1);
        
        for (int i = 0; i < 2; ++i) {
            NSLog(@"----barrier-----%@", [NSThread currentThread]);
        }
    });
    NSLog(@"_______________________這是一條線");
    NSLog(@"_______________________這TM也是一條線");
    
    dispatch_async(queue, ^{
        NSLog(@"----3-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----4-----%@", [NSThread currentThread]);
    });
}
複製程式碼

列印結果為:

2018-02-10 16:18:06.531521+0800 NCImageDownloader[2406:228747] ----2-----<NSThread: 0x608000072c00>{number = 1, name = main}
2018-02-10 16:18:06.531534+0800 NCImageDownloader[2406:228808] ----1-----<NSThread: 0x60400026a580>{number = 4, name = (null)}
2018-02-10 16:18:06.531668+0800 NCImageDownloader[2406:228747] _______________________這是一條線
2018-02-10 16:18:06.531762+0800 NCImageDownloader[2406:228747] _______________________這TM也是一條線
2018-02-10 16:18:07.532328+0800 NCImageDownloader[2406:228808] ----barrier-----<NSThread: 0x60400026a580>{number = 4, name = (null)}
2018-02-10 16:18:07.532620+0800 NCImageDownloader[2406:228808] ----barrier-----<NSThread: 0x60400026a580>{number = 4, name = (null)}
2018-02-10 16:18:07.532858+0800 NCImageDownloader[2406:228808] ----3-----<NSThread: 0x60400026a580>{number = 4, name = (null)}
2018-02-10 16:18:07.532912+0800 NCImageDownloader[2406:228807] ----4-----<NSThread: 0x60000046db00>{number = 5, name = (null)}
複製程式碼

當把 dispatch_barrier_sync 換成 dispatch_barrier_async的時候,列印結果為:

2018-02-10 16:23:30.229972+0800 NCImageDownloader[2425:232456] ----2-----<NSThread: 0x604000062e40>{number = 1, name = main}
2018-02-10 16:23:30.229973+0800 NCImageDownloader[2425:232510] ----1-----<NSThread: 0x60c00026fbc0>{number = 4, name = (null)}
2018-02-10 16:23:30.230147+0800 NCImageDownloader[2425:232456] _______________________這是一條線
2018-02-10 16:23:30.230261+0800 NCImageDownloader[2425:232456] _______________________這TM也是一條線
2018-02-10 16:23:31.232162+0800 NCImageDownloader[2425:232510] ----barrier-----<NSThread: 0x60c00026fbc0>{number = 4, name = (null)}
2018-02-10 16:23:31.232432+0800 NCImageDownloader[2425:232510] ----barrier-----<NSThread: 0x60c00026fbc0>{number = 4, name = (null)}
2018-02-10 16:23:31.232687+0800 NCImageDownloader[2425:232509] ----4-----<NSThread: 0x6000000686c0>{number = 3, name = (null)}
2018-02-10 16:23:31.232689+0800 NCImageDownloader[2425:232510] ----3-----<NSThread: 0x60c00026fbc0>{number = 4, name = (null)}
複製程式碼

看那兩條線的位置變化,可以得出dispatch_barrier_sync同步會阻塞執行緒,dispatch_barrier_async則不會。

GCD佇列組

當需要非同步執行兩組操作任務後,迴歸主執行緒重新整理UI:

- (void)gcdGroup {
    dispatch_group_t gcdGroup = dispatch_group_create();
    
    dispatch_group_async(gcdGroup, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"第一組操作");
    });
    
    dispatch_group_async(gcdGroup, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"第二組操作");
    });
    //當佇列組操作執行完畢之後,發出通知回到主執行緒重新整理UI
    dispatch_group_notify(gcdGroup, dispatch_get_main_queue(), ^{
        //重新整理UI
    });
}
複製程式碼

像上面一條dispatch_barrier_sync 與 dispatch_barrier_async的例子也可以通過 dispatch_group_sync 及 dispatch_group_async,但實現方式不夠前者優雅,直觀。

NSOperation/NSOperationQueue

NSOperation是基於GCD封裝的物件導向使用的一套Objcet C的API,它既然基於GCD,顯然GCD有的優點NSOperation也一樣持有,同時NSOperation還有一些GCD不那麼容易實現的功能,如控制最大併發數。它比GCD更適合物件導向程式設計人員的使用習慣,簡單易用。

NSOperation它實際上是一個抽象類,不能直接實現,要實現它的功能就要使用它的子類NSIvocationOperation、NSBlockOperation及實現NSOperation自定義的子類。

NSOperation實際上是任務,而NSOperationQueue實際上就是佇列。對比於GCD,NSOperation就相當於GCD block裡面的執行任務,而NSOperationQueue則是dispatch_queue_t佇列。NSOperation一般的使用都要搭配NSOperationQueue佇列,單獨使用NSOperation則是『同步』執行在『當前』執行緒的任務,它是沒有開啟新執行緒的能力的。當NSOperation搭配NSOperationQueue的時候,就是把任務放到任務佇列中去,等待系統的排程,再由系統開闢執行緒去抽取佇列中的任務執行。

依上的描述,NSOperation/NSOperationQueue的使用步驟就是: 1、新建任務封於NSOperation; 2、新建NSOperationQueue佇列,並把NSOpertion新增到新建的NSOperationQueue佇列中; 3、系統自動排程NSOperationQueue佇列中的NSOperation任務,開闢新執行緒非同步執行。 PS:如果NSOperation單獨使用,則是在當前的執行緒上同步執行。

子類NSInvocationOperation與NSBlockOperation

NSOperation兩子類的應用程式碼:

//子類NSInvocationOperation
- (void)invocationOperationMethod {
    //新建佇列
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:img1];
    //建立佇列
    NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
    //將任務加入佇列等待系統排程
    [operationQueue addOperation:invocationOperation];
}

//子類NSBlockOperation
- (void)blockOperationMethod {
    //新建任務
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        [self downloadImage:img1];
    }];
    //建立佇列
    NSOperationQueue *blockOperationQueue = [[NSOperationQueue alloc] init];
    //將任務加入佇列等待系統排程
    [blockOperationQueue addOperation:blockOperation];
}

- (void)downloadImage:(NSString *)imageUrl{
    NSURL *imageRequestUrl = [NSURL URLWithString:imageUrl];
    NSData *imageData = [NSData dataWithContentsOfURL:imageRequestUrl];
    if (imageData) {
        NSInvocationOperation *mainOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(refresh:) object:imageData];
        [[NSOperationQueue mainQueue] addOperation:mainOperation];
    }
}

- (void)refresh:(NSData *)imageData {
    UIImage *image = [UIImage imageWithData:imageData];
    [self.imageView1 setImage:image];
} 
複製程式碼

看上去的確是挺簡單,也符合我們平時物件導向的編碼方式。

下面來看看上述關於NSOperation與NSOperationQueue搭配與否的區別,及它們之間開闢執行緒的能力。

NSOpertaion單獨使用:

-(void)operationWithoutOperationQueue {
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
    [invocationOperation start];
    
    NSLog(@"這是可能是一條分割線——————————————————————");
    
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        [self run];
    }];
    [blockOperation start];
}

- (void)run {
    NSLog(@"當前執行緒:%@",[NSThread currentThread]);
}
複製程式碼

列印結果:

2018-02-14 09:10:12.921660+0800 NCImageDownloader[6504:1921474] 當前執行緒:<NSThread: 0x60c00006c640>{number = 1, name = main}
2018-02-14 09:10:12.921847+0800 NCImageDownloader[6504:1921474] 這是可能是一條分割線——————————————————————
2018-02-14 09:10:12.922125+0800 NCImageDownloader[6504:1921474] 當前執行緒:<NSThread: 0x60c00006c640>{number = 1, name = main}
複製程式碼

在當前的執行緒下(也就是主執行緒)順序執行.

NSOpertaion搭配NSOperationQueue使用:

-(void)operationWithOperationQueue {
    NSOperationQueue *blockOperationQueue = [[NSOperationQueue alloc] init];
    //
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
    
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        [self run];
    }];
    //
    [blockOperationQueue addOperation:invocationOperation];
    [blockOperationQueue addOperation:blockOperation];
    NSLog(@"這是可能是一條分割線——————————————————————");
}

- (void)run {
    NSLog(@"當前執行緒:%@",[NSThread currentThread]);
}
複製程式碼

列印結果:

2018-02-14 09:14:12.989780+0800 NCImageDownloader[6526:1925078] 這是可能是一條分割線——————————————————————
2018-02-14 09:14:12.989920+0800 NCImageDownloader[6526:1925146] 當前執行緒:<NSThread: 0x60400026cb40>{number = 3, name = (null)}
2018-02-14 09:14:12.989921+0800 NCImageDownloader[6526:1925144] 當前執行緒:<NSThread: 0x608000276340>{number = 4, name = (null)}
複製程式碼

由結果可以看出來:兩任務分別由系統開闢兩條新的執行緒非同步執行。

此外,NSBlockOperation還有個更簡潔的用法:

NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
[operationQueue addOperationWithBlock:^{
    //要執行的任務
}];
複製程式碼

自定義Operation子類

自定義子類的實現:

@interface NCYOperation : NSOperation

@end

@implementation NCYOperation 

- (void)main {
    NSLog(@"當前執行緒:%@",[NSThread currentThread]);
} 
@end
複製程式碼

具體的使用:

NSOperationQueue *ncyOperationQueue = [[NSOperationQueue alloc] init];
NCYOperation *ncyOperation = [[NCYOperation alloc] init];
[ncyOperationQueue addOperation:ncyOperation];
複製程式碼

NSOperationQueue的一些東西

主佇列

NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
複製程式碼

其他佇列(上面的例子中其實已經使用到了)

NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
複製程式碼

最大併發

NSOperationQueue有個屬性叫maxConcurrentOperationCount譯為最大併發數,這個屬性尤為關鍵。經過上面的GCD瞭解什麼是併發,序列之後,那接下來就更容易懂得NSOperationQueue的maxConcurrentOperationCount屬性的使用了。先看段程式碼:

- (void)operationQueue {
    NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
    
    NSLog(@"maxConcurrentOperationCount : %ld",operationQueue.maxConcurrentOperationCount);
    
    [operationQueue addOperationWithBlock:^{
        NSLog(@"1______%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.5];
    }];
    [operationQueue addOperationWithBlock:^{
        NSLog(@"2______%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.5];
    }];
    [operationQueue addOperationWithBlock:^{
        NSLog(@"3______%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.5];
    }];
    [operationQueue addOperationWithBlock:^{
        NSLog(@"4______%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.5];
    }];
    [operationQueue addOperationWithBlock:^{
        NSLog(@"5______%@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:0.5];
    }];
}
複製程式碼

列印結果為:

2018-02-14 11:39:23.368948+0800 NCImageDownloader[9078:2021062] maxConcurrentOperationCount : -1
2018-02-14 11:39:23.369646+0800 NCImageDownloader[9078:2021116] 5______<NSThread: 0x60400007bfc0>{number = 3, name = (null)}
2018-02-14 11:39:23.369650+0800 NCImageDownloader[9078:2021117] 2______<NSThread: 0x600000273d80>{number = 6, name = (null)}
2018-02-14 11:39:23.369654+0800 NCImageDownloader[9078:2021118] 3______<NSThread: 0x60c000270bc0>{number = 5, name = (null)}
2018-02-14 11:39:23.369671+0800 NCImageDownloader[9078:2021115] 4______<NSThread: 0x60400026fc00>{number = 7, name = (null)}
2018-02-14 11:39:23.369671+0800 NCImageDownloader[9078:2021119] 1______<NSThread: 0x60c000271040>{number = 4, name = (null)}
複製程式碼

結果可以看出:maxConcurrentOperationCount預設的值為:-1,接下來看看列印的順序:5、2、3、4、1完全是亂來的。說明佇列預設的情況下是『併發佇列』。

其實NSOperationQueue是通過maxConcurrentOperationCount屬性來控制佇列的型別的:

maxConcurrentOperationCount -1,預設併發佇列
maxConcurrentOperationCount 1,為序列佇列
maxConcurrentOperationCount 大於1,併發佇列
複製程式碼

為了驗證,將上述程式碼的operationQueue.maxConcurrentOperationCount設定為1,再列印:

2018-02-14 11:57:18.465118+0800 NCImageDownloader[9357:2036692] 1______<NSThread: 0x6080004610c0>{number = 4, name = (null)}
2018-02-14 11:57:18.968538+0800 NCImageDownloader[9357:2036691] 2______<NSThread: 0x60c000261e80>{number = 5, name = (null)}
2018-02-14 11:57:19.470083+0800 NCImageDownloader[9357:2036691] 3______<NSThread: 0x60c000261e80>{number = 5, name = (null)}
2018-02-14 11:57:19.975773+0800 NCImageDownloader[9357:2036691] 4______<NSThread: 0x60c000261e80>{number = 5, name = (null)}
2018-02-14 11:57:20.480337+0800 NCImageDownloader[9357:2036691] 5______<NSThread: 0x60c000261e80>{number = 5, name = (null)}
複製程式碼

妥妥地按順序執行:1、2、3、4、5,再將其operationQueue.maxConcurrentOperationCount設定為2時,列印:

2018-02-14 11:58:45.773876+0800 NCImageDownloader[9392:2038259] 1______<NSThread: 0x60400026db00>{number = 5, name = (null)}
2018-02-14 11:58:45.773903+0800 NCImageDownloader[9392:2038256] 2______<NSThread: 0x60800026d440>{number = 4, name = (null)}
2018-02-14 11:58:46.276428+0800 NCImageDownloader[9392:2038260] 4______<NSThread: 0x604000463200>{number = 6, name = (null)}
2018-02-14 11:58:46.276428+0800 NCImageDownloader[9392:2038257] 3______<NSThread: 0x60400026ad80>{number = 3, name = (null)}
2018-02-14 11:58:46.781921+0800 NCImageDownloader[9392:2038257] 5______<NSThread: 0x60400026ad80>{number = 3, name = (null)}
複製程式碼

併發執行。

這裡要注意的是,maxConcurrentOperationCount值是有上限的,即使你設定了一個很變態的數,系統也會為之設定調整。

佇列掛起

- (void)suspendQueue:(NSOperationQueue *)operationQueue {
    //當佇列沒有了操作,直接return
    if (operationQueue.operationCount == 0) {
        return;
    }
    //只是佇列的掛起與恢復,不會影響正在執行的NSOperation
    operationQueue.suspended = !operationQueue.isSuspended;
    if (operationQueue.isSuspended) {
        NSLog(@"佇列掛起");
    }else {
        NSLog(@"佇列恢復執行");
    }
}
複製程式碼

佇列取消所有操作

- (void)cancelAllOperation:(NSOperationQueue *)operationQueue {
    //只能取消所有佇列的裡面的操作,正在執行的無法取消
    [operationQueue cancelAllOperations];
    //
    NSLog(@"取消佇列所有操作");
    //取消操作並不會影響佇列的掛起狀態,需要手動操作佇列操作
    //只要是取消了佇列的操作,我們就把佇列處於不掛起狀態,以便於後續佇列的開始)
    operationQueue.suspended = NO;
}
複製程式碼

如果要單個任務取消,可以使用NSOperation的cancel操作。

依賴

日常的開發中會遇到從伺服器下載zip包,解壓包的檔案,讀取檔案內容呈現到使用者介面上。此過程包括三組操作:下載-解壓-重新整理UI,示例:

- (void)dependency {
    //download
    NSBlockOperation *downloadOperation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"下載.zip包");
    }];
    //unzip
    NSBlockOperation *unzipOperation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"解壓.zip包");
    }];
    //refresh
    NSBlockOperation *refreshOperation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"重新整理UI");
    }];
    
    //建立佇列
    NSOperationQueue *handleZipOperationQueue = [[NSOperationQueue alloc] init];
    //解壓的操作依賴於下載操作的完成
    [unzipOperation addDependency:downloadOperation];
    //重新整理的操作依賴於解壓操作的完成
    [refreshOperation addDependency:unzipOperation];
    [handleZipOperationQueue addOperations:@[downloadOperation,unzipOperation] waitUntilFinished:YES];
    //
    [[NSOperationQueue mainQueue] addOperation:refreshOperation];
}
複製程式碼

列印結果:

2018-02-14 15:31:45.792174+0800 NCImageDownloader[12401:2188305] 下載.zip包
2018-02-14 15:31:45.792347+0800 NCImageDownloader[12401:2188308] 解壓.zip包
2018-02-14 15:31:45.816914+0800 NCImageDownloader[12401:2188237] 重新整理UI
複製程式碼

這樣就可以讓業務能正常執行,執行的順序有條不紊。

還有部分多執行緒安全部分的內容,抽時間再整理整理。

相關文章