GCD介紹(二): 多核心的性能

概念

爲了在單一進程中充分發揮多核的優勢,我們有必要使用多線程技術(我們沒必要去提多進程,這玩意兒和GCD沒關係)。在低層,GCD全局dispatch queue僅僅是工作線程池的抽象。這些隊列中的Block一旦可用,就會被dispatch到工作線程中。提交至用戶隊列的Block最終也會通過全局隊列進入相同的工作線程池(除非你的用戶隊列的目標是主線程,但是爲了提高運行速度,我們絕不會這麼幹)。

有兩種途徑來通過GCD“榨取”多核心繫統的性能:將單一任務或者一組相關任務併發至全局隊列中運算;將多個不相關的任務或者關聯不緊密的任務併發至用戶隊列中運算;

全局隊列

設想下面的循環:

    for(id obj in array)
        [self doSomethingIntensiveWith:obj];

假定 -doSomethingIntensiveWith: 是線程安全的且可以同時執行多個.一個array通常包含多個元素,這樣的話,我們可以很簡單地使用GCD來平行運算:

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    for(id obj in array)
        dispatch_async(queue, ^{
            [self doSomethingIntensiveWith:obj];
        });

如此簡單,我們已經在多核心上運行這段代碼了。 

當然這段代碼並不完美。有時候我們有一段代碼要像這樣操作一個數組,但是在操作完成後,我們還需要對操作結果進行其他操作:

    for(id obj in array)
        [self doSomethingIntensiveWith:obj];
    [self doSomethingWith:array];

這時候使用GCD的 dispatch_async 就悲劇了.我們還不能簡單地使用dispatch_sync來解決這個問題, 因爲這將導致每個迭代器阻塞,就完全破壞了平行計算。

解決這個問題的一種方法是使用dispatch group。一個dispatch group可以用來將多個block組成一組以監測這些Block全部完成或者等待全部完成時發出的消息。使用函數dispatch_group_create來創建,然後使用函數dispatch_group_async來將block提交至一個dispatch queue,同時將它們添加至一個組。所以我們現在可以重新編碼:

    dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    for(id obj in array)
        dispatch_group_async(group, queue, ^{
            [self doSomethingIntensiveWith:obj];
        });
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    dispatch_release(group);

    [self doSomethingWith:array];

如果這些工作可以異步執行,那麼我們可以更風騷一點,將函數-doSomethingWith:放在後臺執行。我們使用dispatch_group_async函數建立一個block在組完成後執行:

    dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    for(id obj in array)
        dispatch_group_async(group, queue, ^{
            [self doSomethingIntensiveWith:obj];
        });
    dispatch_group_notify(group, queue, ^{
        [self doSomethingWith:array];
    });
    dispatch_release(group);

不僅所有數組元素都會被平行操作,後續的操作也會異步執行,並且這些異步運算都會將程序的其他部分的負載考慮在內。注意如果-doSomethingWith:需要在主線程中執行,比如操作GUI,那麼我們只要將main queue而非全局隊列傳給dispatch_group_notify函數就行了。

 

對於同步執行,GCD提供了一個簡化方法叫做dispatch_apply。這個函數調用單一block多次,並平行運算,然後等待所有運算結束,就像我們想要的那樣:

dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply([array count], queue, ^(size_t index){
        [self doSomethingIntensiveWith:[array objectAtIndex:index]];
    });
    [self doSomethingWith:array];

這很棒,但是異步咋辦?dispatch_apply函數可是沒有異步版本的。但是我們使用的可是一個爲異步而生的API啊!所以我們只要用dispatch_async函數將所有代碼推到後臺就行了:

    dispatch_queue_t queue = dispatch_get_global_qeueue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        dispatch_apply([array count], queue, ^(size_t index){
            [self doSomethingIntensiveWith:[array objectAtIndex:index]];
        });
        [self doSomethingWith:array];
    });

簡單的要死!

 

這種方法的關鍵在於確定我們的代碼是在一次對不同的數據片段進行相似的操作。如果你確定你的任務是線程安全的(不在本篇討論範圍內)那麼你可以使用GCD來重寫你的循環了,更平行更風騷。

要看到性能提升,你還得進行一大堆工作。比之線程,GCD是輕量和低負載的,但是將block提交至queue還是很消耗資源的——block需要被拷貝和入隊,同時適當的工作線程需要被通知。不要將一張圖片的每個像素作爲一個block提交至隊列,GCD的優點就半途夭折了。如果你不確定,那麼請進行試驗。將程序平行計算化是一種優化措施,在修改代碼之前你必須再三思索,確定修改是有益的(還有確保你修改了正確的地方)。

Subsystem併發運算

前面的章節我們討論了在程序的單個subsystem中發揮多核心的優勢。下來我們要跨越多個子系統。

例如,設想一個程序要打開一個包含meta信息的文檔。文檔數據本身需要解析並轉換至模型對象來顯示,meta信息也需要解析和轉換。但是,文檔數據和meta信息不需要交互。我們可以爲文檔和meta各創建一個dispatch queue,然後併發執行。文檔和meta的解析代碼都會各自串行執行,從而不用考慮線程安全(只要沒有文檔和meta之間共享的數據),但是它們還是併發執行的。

一旦文檔打開了,程序需要響應用戶操作。例如,可能需要進行拼寫檢查、代碼高亮、字數統計、自動保存或者其他什麼。如果每個任務都被實現爲在不同的dispatch queue中執行,那麼這些任務會併發執行,並各自將其他任務的運算考慮在內(respect to each other),從而省去了多線程編程的麻煩。

使用dispatch source(下次我會講到),我們可以讓GCD將事件直接傳遞給用戶隊列。例如,程序中監視socket連接的代碼可以被置於它自己的dispatch queue中,這樣它會異步執行,並且執行時會將程序其他部分的運算考慮在內。另外,如果使用用戶隊列的話,這個模塊會串行執行,簡化程序。

結論

我們討論瞭如何使用GCD來提升程序性能以及發揮多核系統的優勢。儘管我們需要比較謹慎地編寫併發程序,GCD還是使得我們能更簡單地發揮系統的可用計算資源。

下一篇中,我們將討論dispatch source,也就是GCD的監視內部、外部事件的機制。

發佈了140 篇原創文章 · 獲贊 10 · 訪問量 24萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章