你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 深入理解dispatch_queue

深入理解dispatch_queue

編輯:IOS開發基礎

303.png

文本由CocoaChina譯者candeladiao(GitHub)翻譯

作者:Mike Ash(Blog GitHub)

原文:Friday Q&A 2015-09-04: Let's Build dispatch_queue


Grand Central Dispatch是蘋果過去幾年創造出來的非常強大的API,在Let's Build系列的最新一期中,我們將探究dispatch_queue基礎功能的重新實現。該主題是Rob Rixr提議的。

概述

dispatch queue是一個工作隊列,其背後是一個全局的線程池。特別是,提交到隊列的任務會在後台線程異步執行。所有線程共享同一個後台線程池,這使得系統更有效率。

這也是我將要模仿的API的精髓部分。GCD還提供了很多精心設計的功能,為了簡單起見,本文將把它們都略過。比如線程池的線程數量會根據待完成的任務數和系統CPU的使用率動態作調整。如果你已經有一堆任務占滿了CPU,然後再扔給它另一個任務,GCD不會再創建另外的工作線程,因為CPU已經被100%占用,再執行別的任務只會更低效。這裡我會寫死線程數而不做模擬動態調整。同時我還會忽略並發隊列的目標隊列和調度屏障功能。

我們目標是聚焦於dispatch queue的真髓:能串行、能並行、能同步、能異步以及共享同一個線程池。

編碼

和以往一樣,今天文章的代碼可以在GitHub上找到:https://github.com/mikeash/MADispatchQueue

如果你想讀的過程中自己探索,以上是所有代碼。

接口

GCD是基於C語言的 API。雖然最新的系統版本中GCD對象已經轉成了Objective-C對象,但API仍保持純C接口(加了block擴展)。這對實現底層接口是好事,GCD提供了出色而簡單的接口,但對我個人而言,我更喜歡用Objective-C來實現。

Objective-C類名稱為MADispatchQueue,包含四個調用方法:

1.獲取全局共享隊列的方法。GCD有多個不同優先級的全局隊列,出於簡單考慮,我們在實現中保留一個。

2.串行和並行隊列的初始化函數。

3.異步分發調用

4.同步分發調用

接口聲明:

   @interface MADispatchQueue : NSObject
    
    + (MADispatchQueue *)globalQueue;
    
    - (id)initSerial: (BOOL)serial;
    
    - (void)dispatchAsync: (dispatch_block_t)block;
    - (void)dispatchSync: (dispatch_block_t)block;
    
    @end

接下來的目標就是實現這些方法的功能。

線程池接口

隊列後面的線程池接口更簡單。它將真正執行提交的任務。隊列負責在合適的時間把已入隊的任務提交給它。

線程池只做一件事:投遞任務並運行。對應地,一個接口只有一個方法:

@interface MAThreadPool : NSObject
    - (void)addBlock: (dispatch_block_t)block;
@end

由於這是核心部分,我們先實現它。

線程池實現

首先看實例變量。線程池能被多個內部線程或外部線程訪問,因此需要線程安全。而在可能的情況下,GCD會使用原子操作,而我這裡以一種以前比較流行的方式-加鎖。我需要知道鎖處於等待和鎖相關的信號,而不僅僅強制其互斥,因此我使用NSCondition而不是NSLock。如果你不熟悉,NSCondition 本質上還是鎖,只是添加了一個條件變量:

NSCondition *_lock;

想要知道什麼時候增加工作線程,我要知道線程池裡的線程數,有多少線程正被占用以及所能擁有的最大線程數:

    NSUInteger _threadCount;
    NSUInteger _activeThreadCount;
    NSUInteger _threadCountLimit;

最後,得有一個NSMutableArray類型的block列表模擬一個隊列,從隊列後端添加新block,從隊列前端刪除:

NSMutableArray *_blocks;

初始化函數很簡單。初始化鎖和block數組,隨便設置一個最大線程數比如128:

    - (id)init {
        if((self = [super init])) {
            _lock = [[NSCondition alloc] init];
            _blocks = [[NSMutableArray alloc] init];
            _threadCountLimit = 128;
        }
        return self;
    }

工作線程運行了一個簡單的無限循環。只要block數組為空,它將一直等待。一旦有block加入,它將被從數組中取出並執行。同時將活動線程數加1,完成後活動線程數減1:

- (void)workerThreadLoop: (id)ignore {

首先要獲取鎖。注意需要在循環開始前獲得。至於原因,等寫到循環結束時你就會明白。

        [_lock lock];

無限循環開始:

        while(1) {

如果隊列為空,等待鎖:

            while([_blocks count] == 0) {
                [_lock wait];
            }

注意:這裡是內循環結束而非if判斷。原因是由於虛假喚醒。簡單說來就是wait 在沒有信號通知的情況下也有可能返回,目前為此,條件檢測的正確方式是當wait 返回時重新進行條件檢測。

一旦有隊列中有block,取出:

            dispatch_block_t block = [_blocks firstObject];
            [_blocks removeObjectAtIndex: 0];

活動線程計數加,表示有新線程正在處理任務:

            _activeThreadCount++;

現在執行block,我們先得釋放鎖,不然代碼並發執行時會出現死鎖:

            [_lock unlock];

安全釋放鎖後,執行block

            block();

block執行完畢,活動線程計數減1。該操作必須在鎖內做,以避免競態條件,最後是循環結束:

            [_lock lock];
            _activeThreadCount--;
        }
    }

現在你該明白為什麼需要在進入循環前獲得鎖了。循環的最後是在鎖內減少活動線程計數。循環開始檢測block隊列。通過在循環外第一次獲得鎖,後續循環迭代能夠使用一個鎖來完成,而不是鎖,解鎖,然後再立即上鎖。

下面是 addBlock:

- (void)addBlock: (dispatch_block_t)block {

這裡唯一需要做的是獲得鎖:

        [_lock lock];

添加一個新的block到block隊列:

        [_blocks addObject: block];

如果有一個空閒的工作線程去執行這個block的話,這裡什麼都不需要做。如果沒有足夠的工作線程去處理等待的block,而工作線程數也沒超限,則我們需要創建一個新線程:

        NSUInteger idleThreads = _threadCount - _activeThreadCount;
        if([_blocks count] > idleThreads && _threadCount < _threadCountLimit) {
            [NSThread detachNewThreadSelector: @selector(workerThreadLoop:)
                                     toTarget: self
                                   withObject: nil];
            _threadCount++;
        }


一切准備就緒。由於空閒線程都在休眠,喚醒它:

        [_lock signal];

最後釋放鎖:

        [_lock unlock];
    }

線程池能在達到預設的最大線程數前創建工作線程,以處理對應的block。現在以此為基礎實現隊列。

隊列實現

和線程池一樣,隊列使用鎖保護其內容。和線程池不同的是,它不需要等待鎖,也不需要信號觸發,僅僅是簡單互斥即可,因此采用 NSLock:

    NSLock *_lock;

和線程池一樣,它把 pending block存在NSMutableArray裡。

NSMutableArray *_pendingBlocks;

標識是串行還是並行隊列:

BOOL _serial;

如果是串行隊列,還需要標識當前是否有線程正在運行:

BOOL _serialRunning;

並行隊列裡有無線程都一樣處理,所以無需關注。

全局隊列是一個全局變量,共享線程池也一樣。它們都在+initialize裡創建:

    static MADispatchQueue *gGlobalQueue;
    static MAThreadPool *gThreadPool;
    + (void)initialize {
        if(self == [MADispatchQueue class]) {
            gGlobalQueue = [[MADispatchQueue alloc] initSerial: NO];
            gThreadPool = [[MAThreadPool alloc] init];
        }
    }

由於+initialize裡已經初始化了,+globalQueue 只需返回該變量。

    + (MADispatchQueue *)globalQueue {
        return gGlobalQueue;
    }

這裡所做的事情和dispatch_once是一樣的,但是實現GCD API的時候使用GCD API有點自欺欺人,即使代碼不一樣。

初始化一個隊列:初始化lock 和pending Blocks,設置_serial變量:

    - (id)initSerial: (BOOL)serial {
        if ((self = [super init])) {
            _lock = [[NSLock alloc] init];
            _pendingBlocks = [[NSMutableArray alloc] init];
            _serial = serial;
        }
        return self;
    }

實現剩下的公有API前,我們需先實現一個底層方法用於給線程分發一個block,然後繼續調用自己去處理另一個block:

    - (void)dispatchOneBlock {

整個生命周期所做的是在線程池上運行block,分發代碼如下:

       [gThreadPool addBlock: ^{

然後取隊列中的第一個block,顯然這需要在鎖內完成,以避免出現問題:

            [_lock lock];
            dispatch_block_t block = [_pendingBlocks firstObject];
            [_pendingBlocks removeObjectAtIndex: 0];
            [_lock unlock];

取到了block又釋放了鎖,block接下來可以安全地在後台線程執行了:

            block();

如果是並行執行的話就不需要再做啥了。如果是串行執行,還需要以下操作:

            if(_serial) {

串行隊列裡將會積累別的block,但不能執行,直到先前的block完成。block完成後,dispatchOneBlock 接下來會看是否還有其他的block被添加到隊列裡面。若有,它調用自己去處理下一個block。若無,則把隊列的運行狀態置為NO:

                [_lock lock];
                if([_pendingBlocks count] > 0) {
                    [self dispatchOneBlock];
                } else {
                    _serialRunning = NO;
                }
                [_lock unlock];
            }
        }];
    }

用以上方法來實現dispatchAsync:就非常容易了。添加block到pending  block隊列,合適的時候設置狀態並調用dispatchOneBlock:

    - (void)dispatchAsync: (dispatch_block_t)block {
        [_lock lock];
        [_pendingBlocks addObject: block];

如果串行隊列空閒,設置隊列狀態為運行並調用dispatchOneBlock 進行處理。

        if(_serial && !_serialRunning) {
            _serialRunning = YES;
            [self dispatchOneBlock];

如果隊列是並行的,直接調用dispatchOneBlock。由於多個block能並行執行,所以這樣能保證即使有其他block正在運行,新的block也能立即執行。

        } else if (!_serial) {
            [self dispatchOneBlock];
        }

如果串行隊列已經在運行,則不需要另外做處理。因為block執行完成後對dispatchOneBlock 的調用最終會調用加入到隊列的block。接著釋放鎖:

        [_lock unlock];
    }

對於 dispatchSync: GCD的處理更巧妙,它是直接在調用線程上執行block,以防止其他block在隊列上執行(如果是串行隊列)。在此我們不用做如此聰明的處理,我們僅僅是對dispatchAsync:進行封裝,讓其一直等待直到block執行完成。

它使用局部NSCondition進行處理,另外使用一個done變量來指示block何時完成:

    - (void)dispatchSync: (dispatch_block_t)block {
        NSCondition *condition = [[NSCondition alloc] init];
        __block BOOL done = NO;

下面是異步分發block。block裡面調用傳入的block,然後設置done的值,給condition發信號

        [self dispatchAsync: ^{
            block();
            [condition lock];
            done = YES;
            [condition signal];
            [condition unlock];
        }];

在調用線程裡面,等待信號done ,然後返回

        [condition lock];
        while (!done) {
            [condition wait];
        }
        [condition unlock];
    }

到此。block的執行就結束了,這也是MADispatchQueue API的最後一點內容。

結論

全局線程池可以使用block隊列和智能產生的線程實現。使用一個共享全局線程池,就能構建一個能提供基本的串行/並行、同步/異步功能的dispatch queue。這樣就重建了一個簡單的GCD,雖然缺少了很多非常好的特性且更低效率。但這能讓我們瞥見其內部工作過程,揭示了它畢竟不是那麼神秘(除dispatch_once比較神秘外)

今天到此為止。下次再帶給大家有趣的東西,Friday Q&A內容取決於讀者的想法,因此如果你有什麼東西想在下次或以後了解的,請聯系我。
(譯者注:作者此前已經將網站上Friday Q&A系列文章整理成了一本書,開發者可在iBooks和Kindle上查看,另外還有PDF和ePub格式供下載。點擊此處查看詳細信息。)

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved