iOS多線程實現3-GCD

原文連接:http://www.cnblogs.com/mddblog/p/4767559.html  html

  敲下gcd三個字母,搜狗第一條顯示竟然是「滾牀單」 ^_^ios

1、介紹

  GCD,英文全稱是Grand Central Dispatch(功能強悍的中央調度器),基於C語言編寫的一套多線程開發機制,所以使用時會以函數形式出現,且大部分函數以dispatch開頭,雖然是C語言的但相對於蘋果其它多線程實現方式,抽象層次更高,使用起來也更加方便。git

  它是蘋果爲應對多核的並行運算提出的解決方案,它會自動利用多核進行併發處理和運算,且線程由系統自動管理(調度、運行),無需程序員參與,使用起來很是方便。程序員

2、任務和隊列

  GCD有兩個核心:任務和隊列。github

  任務:要執行的操做或方法函數,隊列:存聽任務的集合,而咱們要作的就是將任務添加到隊列而後執行,GCD會自動將隊列中的任務按先進先出的方式取出並交給對應線程執行。注意任務的取出是按照先進先出的方式,這也是隊列的特性,可是取出後的執行順序則不必定,下面會詳細討論。緩存

  1 任務網絡

  任務是一個比較抽象的概念,能夠簡單的認爲是一個操做、一個函數、一個方法等等,在實際的開發中大可能是以block(block使用詳見)的形式,使用起來也更加靈活。多線程

  2 隊列queue併發

  • 有兩種隊列:串行隊列和並行隊列。串行隊列:同步執行,在當前線程執行;並行隊列:可由多個線程異步執行,但任務的取出仍是FIFO的。

    隊列建立,根據函數第二個參數來建立串行或並行隊列。框架

// 參數1 隊列名稱
// 參數2 隊列類型 DISPATCH_QUEUE_SERIAL/NULL串行隊列,DISPATCH_QUEUE_CONCURRENT表明並行隊列
// 下面代碼爲建立一個串行隊列,也是實際開發中用的最多的
dispatch_queue_t serialQ = dispatch_queue_create("隊列名", NULL);
  • 另外系統提供了兩種隊列:全局隊列和主隊列。

    全局隊列屬於並行隊列,只不過已由系統建立的沒有名字,且在全局可見(可用)。獲取全局隊列:

/* 取得全局隊列
 第一個參數:線程優先級,設爲默認便可,我的習慣寫0,等同於默認
 第二個參數:標記參數,目前沒有用,通常傳入0
 */
serialQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    主隊列屬於串行隊列,也由系統建立,只不過運行在主線程(UI線程)。獲取主隊列:

// 獲取主隊列
serialQ = dispatch_get_main_queue();
  • 關於內存

    queue屬於一個對象,也是佔用內存的,也會使用引用計數,當向queue添加一個任務時就會將這個queue retain一下,引用計數+1,直到全部任務都完成內存纔會釋放。(咱們在聲明一個queue屬性時要用strong)。

  3 執行方式-2種

  同步執行和異步執行。

    同步執行:不會開啓新的線程,在當前線程執行。

    異步執行:gcd管理的線程池中有空閒線程就會從隊列中取出任務執行,會開啓線程。

  下面爲實現同步和異步的函數,函數功能爲:將任務添加到隊列並執行。

/* 同步執行
 第一個參數:執行任務的隊列:串行、並行、全局、主隊列
 第二個參數:block任務
 */
void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
// 異步執行
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

注意:默認狀況下,新線程都沒有開啓runloop,因此當block任務完成後,線程都會自動被回收,假設咱們想在新開的線程中使用NSTimer,就必須開啓runloop,可使用[[NSRunLoop currentRunLoop] run]開啓當前線程,這是就要本身管理線程的回收等工做。

  • 另外還有兩個方法,實際開發中用的並非太多
dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);

dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

  加了一個barrier,意義在於:隊列以前的block處理完成以後纔開始處理隊列中barrier的block,且barrier的block必須處理完以後,才能處理其它的block。

  根據這個特性咱們能夠實現123456一共6個block,可讓特定幾個併發執行完成以後,再併發執行剩下的block。好比123先併發,以後456再併發執行。具體代碼以下(將barrier放在123與456之間便可):

- (void)barrierTest {
    // 1 建立併發隊列
    dispatch_queue_t BCqueue = dispatch_queue_create("BarrierConcurrent", DISPATCH_QUEUE_CONCURRENT);
    
    // 2.1 添加任務123
    dispatch_async(BCqueue, ^{
        NSLog(@"task1,%@", [NSThread currentThread]);
    });
    dispatch_async(BCqueue, ^{
        sleep(3);
        NSLog(@"task2,%@", [NSThread currentThread]);
    });
    dispatch_async(BCqueue, ^{
        sleep(1);
        NSLog(@"task3,%@", [NSThread currentThread]);
    });
    // 2.2 添加barrier
    dispatch_barrier_async(BCqueue, ^{
        NSLog(@"barrier");
    });
    // 2.3 添加任務456
    dispatch_async(BCqueue, ^{
        sleep(1);
        NSLog(@"task4,%@", [NSThread currentThread]);
    });
    dispatch_async(BCqueue, ^{
        NSLog(@"task5,%@", [NSThread currentThread]);
    });
    dispatch_async(BCqueue, ^{
        NSLog(@"task6,%@", [NSThread currentThread]);
    });
}

  輸出結果,爲了顯示效果,代碼有延時操做:

22:33:56.822 GCDTest[32339:7753989] task1,<NSThread: 0x600000274780>{number = 3, name = (null)}
22:33:57.827 GCDTest[32339:7753986] task3,<NSThread: 0x600000271a00>{number = 5, name = (null)}
22:33:59.826 GCDTest[32339:7754004] task2,<NSThread: 0x600000275100>{number = 4, name = (null)}
22:33:59.826 GCDTest[32339:7754004] barrier
22:33:59.827 GCDTest[32339:7753986] task5,<NSThread: 0x600000271a00>{number = 5, name = (null)}
22:33:59.827 GCDTest[32339:7753987] task6,<NSThread: 0x608000279300>{number = 6, name = (null)}
22:34:00.828 GCDTest[32339:7754004] task4,<NSThread: 0x600000275100>{number = 4, name = (null)}

 

3、幾種類型

  很明顯兩種執行方式,兩種隊列。那麼就有4種狀況:串行隊列同步執行、串行隊列異步執行、並行隊列同步執行、並行隊列異步執行。哪種會開啓新的線程?開幾條?是否併發?記憶起來比較繞,可是隻要抓住基本的就能夠,爲了方便理解,現分析以下:

  1)串行隊列,同步執行-----串行隊列意味着順序執行,同步執行意味着不開啓線程(在當前線程執行)

  2)串行隊列,異步執行-----串行隊列意味着任務順序執行,異步執行說明要開線程, (若是開多個線程的話,不能保證串行隊列順序執行,因此只開一個線程)

  3)並行隊列,異步執行-----並行隊列意味着執行順序不肯定,異步執行意味着會開啓線程,而並行隊列又容許不按順序執行,因此係統爲了提升性能會開啓多個線程,來隊列取任務(隊列中任務取出仍然是順序取出的,只是線程執行無序)。

  4)並行隊列,同步執行-----同步執行意味着不開線程,則確定是順序執行

  5)死鎖-----程序執行不出來(死鎖) ;

4、死鎖舉例

  • 主隊列死鎖:

    這種死鎖最多見,問題也最嚴重,會形成主線程卡住。緣由:主隊列,若是主線程正在執行代碼,就不調度任務;同步執行:一直執行第一個任務直到結束。二者互相等待形成死鎖,示例以下:

- (void)mainThreadDeadLockTest {
    NSLog(@"begin");
    dispatch_sync(dispatch_get_main_queue(), ^{
        // 發生死鎖下面的代碼不會執行
        NSLog(@"middle");
    });
    // 發生死鎖下面的代碼不會執行,固然函數也不會返回,後果也最爲嚴重
    NSLog(@"end");
}
  • 在其它線程死鎖,這種不會影響主線程:

    緣由:serialQueue爲串行隊列,當代碼執行到block1時正常,執行到dispatch_sync時,dispatch_sync等待block2執行完畢纔會返回,而serialQueue是串行隊列,它正在執行block1,只有等block1執行完畢後纔會去執行block2,相互等待形成死鎖

- (void)deadLockTest {
    // 其它線程的死鎖
    dispatch_queue_t serialQueue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(serialQueue, ^{
        // 串行隊列block1
        NSLog(@"begin");
        dispatch_sync(serialQueue, ^{
            // 串行隊列block2 發生死鎖,下面的代碼不會執行
            NSLog(@"middle");
        });
        // 不會打印
        NSLog(@"end");
    });
    // 函數會返回,不影響主線程
    NSLog(@"return");
}

5、經常使用舉例

  1 線程間通信

  好比,爲了提升用戶體驗,咱們通常在其餘線程(非主線程)下載圖片或其它網絡資源,下載完成後咱們要更新UI,而UI更新必須在主線程執行,因此咱們常常會使用:

// 同步執行,會阻塞指導下面block中的代碼執行完畢
dispatch_sync(dispatch_get_main_queue(), ^{
    // 主線程,UI更新
});
// 異步執行
dispatch_async(dispatch_get_main_queue(), ^{
    // 主線程,UI更新
});

  2 信號量的使用

也屬於線程間通信,下面的舉例是常常用到的場景。在網絡訪問中,NSURLSession類都是異步的(找了好久沒有找到同步的方法),而有時咱們但願可以像NSURLConnection同樣能夠同步訪問,即在網絡block調用完成以後作一些操做。那咱們可使用dispatch的信號量來解決:

/// 用於線程間通信,下面是等待一個網絡完成
- (void)dispatchSemaphore {
    NSString *urlString = [@"https://www.baidu.com" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    // 設置緩存策略爲每次都從網絡加載 超時時間30秒
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:30];
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        // 處理完成以後,發送信號量
        NSLog(@"正在處理...");
        dispatch_semaphore_signal(semaphore);
    }] resume];
    // 等待網絡處理完成
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"處理完成!");
}

在上面的舉例中dispatch_semaphore_signal的調用必須是在另外一個線程調用,由於當前線程已經dispatch_semaphore_wait阻塞。另外,dispatch_semaphore_wait最好不要在主線程調用

  3 其它經常使用

  全局隊列,實現併發:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 要執行的代碼
});

6、Dispatch Group調度組

  使用調度組,能夠輕鬆實如今一些任務完成後,作一些操做。好比具備順序性要求的生產者消費者等等。

  示例1  任務1完成以後執行任務2。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self groupTest];
}
- (void)groupTest {
    // 建立一個組
    dispatch_group_t group = dispatch_group_create();
    NSLog(@"開始執行");
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
            // 任務1
            // 等待1s一段時間在執行
            [NSThread sleepForTimeInterval:1];
            NSLog(@"task1 running in %@",[NSThread currentThread]);
        });
        dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
            // 任務2
            NSLog(@"task2 running in %@",[NSThread currentThread]);
        });
    });
}

  點擊屏幕後,打印以下,能夠看到任務1雖然等待了1s,任務2也不執行,只有任務1執行完畢才執行任務2.

2015-08-28 18:16:05.317 GCDTest[1468:229374] 開始執行
2015-08-28 18:16:06.323 GCDTest[1468:229457] task1 running in <NSThread: 0x7f8962f16900>{number = 2, name = (null)}
2015-08-28 18:16:06.323 GCDTest[1468:229456] task2 running in <NSThread: 0x7f8962c92750>{number = 3, name = (null)}

  示例2,其實示例1並不經常使用,真正用到的是監控多個任務完成以後,回到主線程更新UI,或者作其它事情。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self groupTest];
}
- (void)groupTest {
    // 建立一個組
    dispatch_group_t group = dispatch_group_create();
    NSLog(@"開始執行");
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
            // 關聯任務1
            NSLog(@"task1 running in %@",[NSThread currentThread]);
        });
        dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
            // 關聯任務2
            NSLog(@"task2 running in %@",[NSThread currentThread]);
        });
        dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
            // 關聯任務3
            NSLog(@"task3 running in %@",[NSThread currentThread]);
        });
        dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
            // 關聯任務4
            // 等待1秒
            [NSThread sleepForTimeInterval:1];
            NSLog(@"task4 running in %@",[NSThread currentThread]);
        });
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            // 回到主線程執行
            NSLog(@"mainTask running in %@",[NSThread currentThread]);
        });
    });
}

  點擊屏幕後,打印以下,能夠看到不管其它任務而後和執行,mainTask等待它們執行後才執行。

2015-08-28 18:24:14.312 GCDTest[1554:236273] 開始執行
2015-08-28 18:24:14.312 GCDTest[1554:236352] task3 running in <NSThread: 0x7fa8f1f0c9c0>{number = 4, name = (null)}
2015-08-28 18:24:14.312 GCDTest[1554:236354] task1 running in <NSThread: 0x7fa8f1d10750>{number = 2, name = (null)}
2015-08-28 18:24:14.312 GCDTest[1554:236351] task2 running in <NSThread: 0x7fa8f1c291a0>{number = 3, name = (null)}
2015-08-28 18:24:15.313 GCDTest[1554:236353] task4 running in <NSThread: 0x7fa8f1d0e7f0>{number = 5, name = (null)}
2015-08-28 18:24:15.313 GCDTest[1554:236273] mainTask running in <NSThread: 0x7fa8f1c13df0>{number = 1, name = main}

 

關於Dispatch對象內存管理問題

根據上面的代碼,能夠看出有關dispatch的對象並非OC對象,那麼,用不用像對待Core Foundation框架的對象同樣,使用retain/release來管理呢?答案是不用的!

若是是ARC環境,咱們無需管理,會像對待OC對象同樣自動內存管理。
若是是MRC環境,不是使用retain/release,而是使用dispatch_retain/dispatch_release來管理。

 

測試用例github下載:https://github.com/mddios/GCDTest

相關文章
相關標籤/搜索