iOS開發 多線程(一)-轉自MJ的GCD詳解

1、簡介網絡

在iOS全部實現多線程的方案中,GCD應該是最有魅力的,由於GCD自己是蘋果公司爲多核的並行運算提出的解決方案。GCD在工做時會自動利用更多的處理器核心,以充分利用更強大的機器。GCD是Grand Central Dispatch的簡稱,它是基於C語言的。若是使用GCD,徹底由系統管理線程,咱們不須要編寫線程代碼。只需定義想要執行的任務,而後添加到適當的調度隊列(dispatch queue)。GCD會負責建立線程和調度你的任務,系統直接提供線程管理數據結構


2、調度隊列(dispath queue)多線程

1.GCD的一個重要概念是隊列,它的核心理念:將長期運行的任務拆分紅多個工做單元,並將這些單元添加到dispath queue中,系統會爲咱們管理這些dispath queue,爲咱們在多個線程上執行工做單元,咱們不須要直接啓動和管理後臺線程。併發

2.系統提供了許多預約義的dispath queue,包括能夠保證始終在主線程上執行工做的dispath queue。也能夠建立本身的dispath queue,並且能夠建立任意多個。GCD的dispath queue嚴格遵循FIFO(先進先出)原則,添加到dispath queue的工做單元將始終按照加入dispath queue的順序啓動。app

3.dispatch queue按先進先出的順序,串行或併發地執行任務異步

1> serial dispatch queue一次只能執行一個任務, 當前任務完成纔開始出列並啓動下一個任務async

2> concurrent dispatch queue則儘量多地啓動任務併發執行函數


3、建立和管理dispatch queueoop

1.得到全局併發Dispatch Queue (concurrent dispatch queue)性能

1> 併發dispatch queue能夠同時並行地執行多個任務,不過併發queue仍然按先進先出的順序來啓動任務。併發queue會在以前的任務完成以前就出列下一個任務並開始執行。併發queue同時執行的任務數量會根據應用和系統動態變化,各類因素包括:可用核數量、其它進程正在執行的工做數量、其它串行dispatch queue中優先任務的數量等.

2> 系統給每一個應用提供三個併發dispatch queue,整個應用內全局共享,三個queue的區別是優先級。你不須要顯式地建立這些queue,使用dispatch_get_global_queue函數來獲取這三個queue:

// 獲取默認優先級的全局併發dispatch queue  
dispatch_queue_t  queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);


第一個參數用於指定優先級,分別使用DISPATCH_QUEUE_PRIORITY_HIGH和DISPATCH_QUEUE_PRIORITY_LOW兩個常來獲取高和低優先級的兩個queue;第二個參數目前未使用到,默認0便可

3> 雖然dispatch queue是引用計數的對象,但你不須要retain和release全局併發queue。由於這些queue對應用是全局的,retain和release調用會被忽略。你也不須要存儲這三個queue的引用,每次都直接調用dispatch_get_global_queue得到queue就好了。


2.建立串行Dispatch Queue (serial dispatch queue)

1> 應用的任務須要按特定順序執行時,就須要使用串行Dispatch Queue,串行queue每次只能執行一個任務。你可使用串行queue來替代鎖,保護共享資源 或可變的數據結構。和鎖不同的是,串行queue確保任務按可預測的順序執行。並且只要你異步地提交任務到串行queue,就永遠不會產生死鎖

2> 你必須顯式地建立和管理全部你使用的串行queue,應用能夠建立任意數量的串行queue,但不要爲了同時執行更多任務而建立更多的串行queue。若是你須要併發地執行大量任務,應該把任務提交到全局併發queue

3> 利用dispatch_queue_create函數建立串行queue,兩個參數分別是queue名和一組queue屬性

dispatch_queue_t queue;  
queue = dispatch_queue_create("cn.itcast.queue", NULL);

3.運行時得到公共Queue

GCD提供了函數讓應用訪問幾個公共dispatch queue:

1> 使用dispatch_get_current_queue函數做爲調試用途,或者測試當前queue的標識。在block對象中調用這個函數會返回block提交到的queue(這個時候queue應該正在執行中)。在block對象以外調用這個函數會返回應用的默認併發queue。
2> 使用dispatch_get_main_queue函數得到應用主線程關聯的串行dispatch queue
3> 使用dispatch_get_global_queue來得到共享的併發queue


4.Dispatch Queue的內存管理

1> Dispatch Queue和其它dispatch對象(還有dispatch source)都是引用計數的數據類型。當你建立一個串行dispatch queue時,初始引用計數爲 1,你可使用dispatch_retain和dispatch_release函數來增長和減小引用計數。當引用計數到達 0 時,系統會異步地銷燬這個queue

2> 對dispatch對象(如dispatch queue)retain和release 是很重要的,確保它們被使用時可以保留在內存中。和OC對象同樣,通用的規則是若是使用一個傳遞過來的queue,你應該在使用前retain,使用完以後release

3> 你不須要retain或release全局dispatch queue,包括全局併發dispatch queue和main dispatch queue

4> 即便你實現的是自動垃圾收集的應用,也須要retain和release建立的dispatch queue和其它dispatch對象。GCD 不支持垃圾收集模型來回收內存


4、添加任務到queue

要執行一個任務,你須要將它添加到一個適當的dispatch queue,你能夠單個或按組來添加,也能夠同步或異步地執行一個任務,也。一旦進入到queue,queue會負責儘快地執行你的任務。通常能夠用一個block來封裝任務內容。

1.添加單個任務到queue

1> 異步添加任務

你能夠異步或同步地添加一個任務到Queue,儘量地使用dispatch_async或dispatch_async_f函數異步地調度任務。由於添加任務到Queue中時,沒法肯定這些代碼何時可以執行。所以異步地添加block或函數,可讓你當即調度這些代碼的執行,而後調用線程能夠繼續去作其它事情。特別是應用主線程必定要異步地 dispatch 任務,這樣才能及時地響應用戶事件

2> 同步添加任務

少數時候你可能但願同步地調度任務,以免競爭條件或其它同步錯誤。 使用dispatch_sync和dispatch_sync_f函數同步地添加任務到Queue,這兩個函數會阻塞當前調用線程,直到相應任務完成執行。注意:絕對不要在任務中調用 dispatch_sync或dispatch_sync_f函數,並同步調度新任務到當前正在執行的 queue。對於串行queue這一點特別重要,由於這樣作確定會致使死鎖;而併發queue也應該避免這樣作。

3> 代碼演示

// 調用前,查看下當前線程  
NSLog(@"當前調用線程:%@", [NSThread currentThread]);  
  
// 建立一個串行queue  
dispatch_queue_t queue = dispatch_queue_create("cn.itcast.queue", NULL);  
  
dispatch_async(queue, ^{  
    NSLog(@"開啓了一個異步任務,當前線程:%@", [NSThread currentThread]);  
});  
  
dispatch_sync(queue, ^{  
    NSLog(@"開啓了一個同步任務,當前線程:%@", [NSThread currentThread]);  
});  
// 銷燬隊列  
dispatch_release(queue);
    打印信息:



  1. 2013-02-03 09:03:37.348 thread[6491:c07] 當前調用線程:<NSThread: 0x714fa80>{name = (null), num = 1}  

  2. 2013-02-03 09:03:37.349 thread[6491:1e03] 開啓了一個異步任務,當前線程:<NSThread: 0x74520a0>{name = (null), num = 3}  

  3. 2013-02-03 09:03:37.350 thread[6491:c07] 開啓了一個同步任務,當前線程:<NSThread: 0x714fa80>{name = (null), num = 1}  


2.併發地執行循環迭代

若是你使用循環執行固定次數的迭代, 併發dispatch queue可能會提升性能。

例以下面的for循環:

int i;  
int count = 10;  
for (i = 0; i < count; i++) {  
   printf("%d  ",i);  
}

1> 若是每次迭代執行的任務與其它迭代獨立無關,並且循環迭代執行順序也可有可無的話,你能夠調用dispatch_apply或dispatch_apply_f函數來替換循環。這兩個函數爲每次循環迭代將指定的block或函數提交到queue。當dispatch到併發 queue時,就有可能同時執行多個循環迭代。用dispatch_apply或dispatch_apply_f時你能夠指定串行或併發 queue。併發queue容許同時執行多個循環迭代,而串行queue就沒太大必要使用了。

下面代碼使用dispatch_apply替換了for循環,你傳遞的block必須包含一個size_t類型的參數,用來標識當前循環迭代。第一次迭代這個參數值爲0,最後一次值爲count - 1


// 得到全局併發queue  
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  
size_t count = 10;  
dispatch_apply(count, queue, ^(size_t i) {  
    printf("%zd ", i);  
});  
// 銷燬隊列  
dispatch_release(queue);


打印信息:

  1. 1 2 0 3 4 5 6 7 8 9   

能夠看出,這些迭代是併發執行的

和普通for循環同樣,dispatch_apply和dispatch_apply_f函數也是在全部迭代完成以後纔會返回,所以這兩個函數會阻塞當前線程,主線程中調用這兩個函數必須當心,可能會阻止事件處理循環並沒有法響應用戶事件。因此若是循環代碼須要必定的時間執行,能夠考慮在另外一個線程中調用這兩個函數。若是你傳遞的參數是串行queue,並且正是執行當前代碼的queue,就會產生死鎖


3.在主線程中執行任務

1> GCD提供一個特殊的dispatch queue,能夠在應用的主線程中執行任務。只要應用主線程設置了run loop(由CFRunLoopRef類型或NSRunLoop對象管理),就會自動建立這個queue,而且最後會自動銷燬。非Cocoa應用若是不顯式地設置run loop, 就必須顯式地調用dispatch_main函數來顯式地激活這個dispatch queue,不然雖然你能夠添加任務到queue,但任務永遠不會被執行。

2> 調用dispatch_get_main_queue函數得到應用主線程的dispatch queue,添加到這個queue的任務由主線程串行化執行

3> 代碼實現,好比異步下載圖片後,回到主線程顯示圖片

// 異步下載圖片  
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
    NSURL *url = [NSURL URLWithString:@"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg"];  
    UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];  
      
    // 回到主線程顯示圖片  
    dispatch_async(dispatch_get_main_queue(), ^{  
        self.imageView.image = image;  
    });  
});


4.任務中使用Objective-C對象

GCD支持Cocoa內存管理機制,所以能夠在提交到queue的block中自由地使用Objective-C對象。每一個dispatch queue維護本身的autorelease pool確保釋放autorelease對象,可是queue不保證這些對象實際釋放的時間。若是應用消耗大量內存,而且建立大量autorelease對象,你須要建立本身的autorelease pool,用來及時地釋放再也不使用的對象。


5、暫停和繼續queue

咱們可使用dispatch_suspend函數暫停一個queue以阻止它執行block對象;使用dispatch_resume函數繼續dispatch queue。調用dispatch_suspend會增長queue的引用計數,調用dispatch_resume則減小queue的引用計數。當引用計數大於0時,queue就保持掛起狀態。所以你必須對應地調用suspend和resume函數。掛起和繼續是異步的,並且只在執行block之間(好比在執行一個新的block以前或以後)生效。掛起一個queue不會致使正在執行的block中止。


6、Dispatch Group的使用

假設有這樣一個需求:從網絡上下載兩張不一樣的圖片,而後顯示到不一樣的UIImageView上去,通常能夠這樣實現

/

/ 根據url獲取UIImage  
- (UIImage *)imageWithURLString:(NSString *)urlString {  
    NSURL *url = [NSURL URLWithString:urlString];  
    NSData *data = [NSData dataWithContentsOfURL:url];  
    return [UIImage imageWithData:data];  
}  
  
- (void)downloadImages {  
    // 異步下載圖片  
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
        // 下載第一張圖片  
        NSString *url1 = @"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg";  
        UIImage *image1 = [self imageWithURLString:url1];  
          
        // 下載第二張圖片  
        NSString *url2 = @"http://hiphotos.baidu.com/lvpics/pic/item/3a86813d1fa41768bba16746.jpg";  
        UIImage *image2 = [self imageWithURLString:url2];  
          
        // 回到主線程顯示圖片  
        dispatch_async(dispatch_get_main_queue(), ^{  
            self.imageView1.image = image1;  
              
            self.imageView2.image = image2;  
        });  
    });  
}


雖然這種方案能夠解決問題,但其實兩張圖片的下載過程並不須要按順序執行,併發執行它們能夠提升執行速度。有個注意點就是必須等兩張圖片都下載完畢後才能回到主線程顯示圖片。Dispatch Group可以在這種狀況下幫咱們提高性能。下面先看看Dispatch Group的用處:

咱們可使用dispatch_group_async函數將多個任務關聯到一個Dispatch Group和相應的queue中,group會併發地同時執行這些任務。並且Dispatch Group能夠用來阻塞一個線程, 直到group關聯的全部的任務完成執行。有時候你必須等待任務完成的結果,而後才能繼續後面的處理。

下面用Dispatch Group優化上面的代碼:


// 根據url獲取UIImage  
- (UIImage *)imageWithURLString:(NSString *)urlString {  
    NSURL *url = [NSURL URLWithString:urlString];  
    NSData *data = [NSData dataWithContentsOfURL:url];  
    // 這裏並無自動釋放UIImage對象  
    return [[UIImage alloc] initWithData:data];  
}  
  
- (void)downloadImages {  
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);  
      
    // 異步下載圖片  
    dispatch_async(queue, ^{  
        // 建立一個組  
        dispatch_group_t group = dispatch_group_create();  
          
        __block UIImage *image1 = nil;  
        __block UIImage *image2 = nil;  
          
        // 關聯一個任務到group  
        dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
            // 下載第一張圖片  
            NSString *url1 = @"http://car0.autoimg.cn/upload/spec/9579/u_20120110174805627264.jpg";  
            image1 = [self imageWithURLString:url1];  
        });  
          
        // 關聯一個任務到group  
        dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{  
            // 下載第一張圖片  
            NSString *url2 = @"http://hiphotos.baidu.com/lvpics/pic/item/3a86813d1fa41768bba16746.jpg";  
            image2 = [self imageWithURLString:url2];  
        });  
          
        // 等待組中的任務執行完畢,回到主線程執行block回調  
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{  
            self.imageView1.image = image1;  
            self.imageView2.image = image2;  
              
            // 千萬不要在異步線程中自動釋放UIImage,由於當異步線程結束,異步線程的自動釋放池也會被銷燬,那麼UIImage也會被銷燬  
              
            // 在這裏釋放圖片資源  
            [image1 release];  
            [image2 release];  
        });  
          
        // 釋放group  
        dispatch_release(group);  
    });  
}

dispatch_group_notify函數用來指定一個額外的block,該block將在group中全部任務完成後執行


--------------------------------------

GCD中有三種隊列類型:

  1. The main queue: 與主線程功能相同。實際上,提交至main queue的任務會在主線程中執行。main queue能夠調用dispatch_get_main_queue()來得到。由於main queue是與主線程相關的,因此這是一個串行隊列。

  2. Global queues: 全局隊列是併發隊列,並由整個進程共享。進程中存在三個全局隊列:高、中(默認)、低三個優先級隊列。能夠調用dispatch_get_global_queue函數傳入優先級來訪問隊列。

  3. 用戶隊列: 用戶隊列 (GCD並不這樣稱呼這種隊列, 可是沒有一個特定的名字來形容這種隊列,因此咱們稱其爲用戶隊列) 是用函數 dispatch_queue_create 建立的隊列. 這些隊列是串行的。正由於如此,它們能夠用來完成同步機制, 有點像傳統線程中的mutex。

相關文章
相關標籤/搜索