1. 程式人生 > >GCD原始碼吐血分析(1)——GCD Queue

GCD原始碼吐血分析(1)——GCD Queue

看了快半個月的GCD原始碼,只能說太變態了。
先來吐槽一下:一個函式,呼叫棧都是十幾層…… 為了效率,程式碼使用了純C語言,但是為了模擬面向物件中的繼承,虛擬函式等,定義了一層層的巨集定義,看一個struct的定義要繞過來繞過去…… 網上的資料極少,有的那幾篇,還都是用舊版本的GCD在說事兒,而新版的GCD原始碼複雜度及晦澀度,在原有的基礎上,又升級了一個檔次……

說實話,到現在也不敢說是看懂了或是看對了,所以這篇部落格只是個人的一個不負責任的總結而已。天哪,讓我從GCD的泥潭中出來吧……

基本的資料結構

GCD的類都是struct定義的。但並不像runtime一樣,直接使用:

來繼承定義。而是將所有的父類的資料成員,都平鋪重複的寫在一個個的struct中,這樣做的好處應該是為了提高效率,避免引入繼承機制帶來的程式碼執行上的延遲?但是,為了減少程式碼量和易讀性,蘋果還是很“貼心”的做了許多巨集定義,在閱讀原始碼時,你不得不將這些巨集,替換為它真實的定義。

GCD中的資料結構是如下組織的:

這裡寫圖片描述

GCD中類都繼承自統一的基類dispatch_object_t, 再下一層,則是表示系統物件的基類_os_object_s

root 基類dispatch_object_t的定義如下:

typedef union {
    struct _os_object_s *_os_obj;
    struct
dispatch_object_s *_do; struct dispatch_continuation_s *_dc; struct dispatch_queue_s *_dq; struct dispatch_queue_attr_s *_dqa; struct dispatch_group_s *_dg; struct dispatch_source_s *_ds; struct dispatch_mach_s *_dm; struct dispatch_mach_msg_s *_dmsg; struct dispatch_source_attr_s *_dsa; struct
dispatch_semaphore_s *_dsema; struct dispatch_data_s *_ddata; struct dispatch_io_s *_dchannel; struct dispatch_operation_s *_doperation; struct dispatch_disk_s *_ddisk; } dispatch_object_t DISPATCH_TRANSPARENT_UNION;

可見它是一個聯合,可以表示union中任意的型別。起到的作用和基類指標相似,所有的子型別都可以用dispatch_object_t統一表示。

dispatch_queue_s : GCD中的queue。

dispatch_continuation_s : 我們向queue提交的任務,無論block還是function形式,最終都會被封裝為dispatch_continuation_s。

注意,這只是邏輯上的繼承關係,在實際的原始碼中,是看不到繼承的符號的。而是通過巨集的方式,將父類中的屬性,在子類中再寫一次。如繼承了_os_object_sdispatch_object_s的定義:

struct dispatch_object_s {
    _DISPATCH_OBJECT_HEADER(object);
};

#define _DISPATCH_OBJECT_HEADER(x) \
    struct _os_object_s _as_os_obj[0]; \
    OS_OBJECT_STRUCT_HEADER(dispatch_##x); \  // 這個巨集,可以理解為dispatch_object_s繼承自_os_object_s
    struct dispatch_##x##_s *volatile do_next; \
    struct dispatch_queue_s *do_targetq; \
    void *do_ctxt; \
    void *do_finalizer

簡單的理解了GCD中基本類的關係,我們就來看一下各個類中的定義。

_os_object_s

typedef struct _os_object_s {
    _OS_OBJECT_HEADER(
    const _os_object_vtable_s *os_obj_isa,
    os_obj_ref_cnt,
    os_obj_xref_cnt);
} _os_object_s;

#define _OS_OBJECT_HEADER(isa, ref_cnt, xref_cnt) \
        isa; /* must be pointer-sized */ \
        int volatile ref_cnt; \
        int volatile xref_cnt

把巨集展開,得到直觀的定義:

typedef struct _os_object_s {
    const _os_object_vtable_s *os_obj_isa; // 這個也是個巨集定義,展開後似乎是可以被子類重寫的和引用計數相關的兩個函式指標
    int volatile os_obj_ref_cnt;     // 引用計數,這是內部gcd內部使用的計數器
    int volatile os_obj_xref_cnt;    // 外部引用計數,這是gcd外部使用的計數器,兩者都為0的時候才能dispose
} _os_object_s;

dispatch_object_s

struct dispatch_object_s {
    _DISPATCH_OBJECT_HEADER(object);
};

#define _DISPATCH_OBJECT_HEADER(x) \
    struct _os_object_s _as_os_obj[0]; \
    OS_OBJECT_STRUCT_HEADER(dispatch_##x); \
    struct dispatch_##x##_s *volatile do_next; \
    struct dispatch_queue_s *do_targetq; \
    void *do_ctxt; \
    void *do_finalizer

巨集展開:

struct dispatch_object_s {
    struct _os_object_s _as_os_obj[0];
    OS_OBJECT_STRUCT_HEADER(dispatch_object); // 繼承自_os_object_s的部分,和引用計數相關
    struct dispatch_object_s *volatile do_next;  // 連結串列的 next
    struct dispatch_queue_s *do_targetq;  // 目標佇列,指定這個object在哪個queue中執行
    void *do_ctxt;    // 上下文,我們要傳遞的引數
    void *do_finalizer;  // 解構函式
};

dispatch_queue_s

struct dispatch_queue_s {
    _DISPATCH_QUEUE_HEADER(queue);
} DISPATCH_ATOMIC64_ALIGN;

#define _DISPATCH_QUEUE_HEADER(x) \
    struct os_mpsc_queue_s _as_oq[0]; \
    DISPATCH_OBJECT_HEADER(x); \
    _OS_MPSC_QUEUE_FIELDS(dq, dq_state); \
    uint32_t dq_side_suspend_cnt; \
    dispatch_unfair_lock_s dq_sidelock; \
    union { \
        dispatch_queue_t dq_specific_q; \
        struct dispatch_source_refs_s *ds_refs; \
        struct dispatch_timer_source_refs_s *ds_timer_refs; \
        struct dispatch_mach_recv_refs_s *dm_recv_refs; \
    }; \
    DISPATCH_UNION_LE(uint32_t volatile dq_atomic_flags, \
        const uint16_t dq_width, \
        const uint16_t __dq_opaque \
    ); \

巨集展開:

struct dispatch_queue_s {
    struct os_mpsc_queue_s _as_oq[0];
    DISPATCH_OBJECT_HEADER(x);
    _OS_MPSC_QUEUE_FIELDS(dq, dq_state); // 這裡又是一個巨集,需要再展開
    uint32_t dq_side_suspend_cnt;
    dispatch_unfair_lock_s dq_sidelock;
    union { 
        dispatch_queue_t dq_specific_q; 
        struct dispatch_source_refs_s *ds_refs; 
        struct dispatch_timer_source_refs_s *ds_timer_refs; 
        struct dispatch_mach_recv_refs_s *dm_recv_refs; 
    }; 
    DISPATCH_UNION_LE(uint32_t volatile dq_atomic_flags,
        const uint16_t dq_width, 
        const uint16_t __dq_opaque 
    ); 
}

#define _OS_MPSC_QUEUE_FIELDS(ns, __state_field__) \
    struct dispatch_object_s *volatile ns##_items_head; \
    DISPATCH_UNION_LE(uint64_t volatile __state_field__, \
            dispatch_lock __state_field__##_lock, \
            uint32_t __state_field__##_bits \
    ) DISPATCH_ATOMIC64_ALIGN; \
    /* LP64 global queue cacheline boundary */ \
    unsigned long ns##_serialnum; \
    const char *ns##_label; \
    struct dispatch_object_s *volatile ns##_items_tail; \
    dispatch_priority_t ns##_priority; \
    int volatile ns##_sref_cnt

展開_OS_MPSC_QUEUE_FIELDS 後:

struct dispatch_queue_s {
    struct os_mpsc_queue_s _as_oq[0];
    // 展開_OS_MPSC_QUEUE_FIELDS 
    struct dispatch_object_s *volatile queue_items_head; // queue首元素
    DISPATCH_UNION_LE(
            uint64_t volatile dq_state,    // queue的狀態
            dispatch_lock dq_state_lock, 
            uint32_t dq_state_bits
    ) DISPATCH_ATOMIC64_ALIGN;
    unsigned long queue_serialnum; // queue的編號
    const char *queue_label; // queue的名稱
    struct dispatch_object_s *volatile queue_items_tail;  // queue 尾元素
    dispatch_priority_t queue_priority; // queue優先順序
    int volatile queue_sref_cnt

    uint32_t dq_side_suspend_cnt;
    dispatch_unfair_lock_s dq_sidelock;
    union { // 這些是提交的queue上的元素佇列嗎????????
        dispatch_queue_t dq_specific_q; 
        struct dispatch_source_refs_s *ds_refs; 
        struct dispatch_timer_source_refs_s *ds_timer_refs; 
        struct dispatch_mach_recv_refs_s *dm_recv_refs; 
    }; 
    DISPATCH_UNION_LE(uint32_t volatile dq_atomic_flags,
        const uint16_t dq_width,    // queue的併發數(dq_width==1表示是序列佇列)
        const uint16_t __dq_opaque 
    ); 
}

dispatch_continuation_s

typedef struct dispatch_continuation_s {
    struct dispatch_object_s _as_do[0];
    DISPATCH_CONTINUATION_HEADER(continuation);
} *dispatch_continuation_t;

#define DISPATCH_CONTINUATION_HEADER(x) \
    union { \
        const void *do_vtable; \
        uintptr_t dc_flags; \
    }; \
    union { \
        pthread_priority_t dc_priority; \
        int dc_cache_cnt; \
        uintptr_t dc_pad; \
    }; \
    struct dispatch_##x##_s *volatile do_next; \
    struct voucher_s *dc_voucher; \
    dispatch_function_t dc_func; \
    void *dc_ctxt; \
    void *dc_data; \
    void *dc_other

巨集展開後

typedef struct dispatch_continuation_s {
    struct dispatch_object_s _as_do[0];
    union {
        const void *do_vtable;
        uintptr_t dc_flags;
    };
    union { 
        pthread_priority_t dc_priority; 
        int dc_cache_cnt; 
        uintptr_t dc_pad; 
    };
    struct dispatch_continuation_s *volatile do_next;
    struct voucher_s *dc_voucher;
    dispatch_function_t dc_func;
    void *dc_ctxt;
    void *dc_data;
    void *dc_other

} *dispatch_continuation_t;

dispatch_queue_attr_s

struct dispatch_queue_attr_s {
    OS_OBJECT_STRUCT_HEADER(dispatch_queue_attr);
    dispatch_priority_requested_t dqa_qos_and_relpri;  // queue優先順序
    uint16_t dqa_overcommit:2;                       // 是否可以overcommit
    uint16_t dqa_autorelease_frequency:2;
    uint16_t dqa_concurrent:1;                      // 是否是併發佇列
    uint16_t dqa_inactive:1;                        // 是否啟用
};

root queues

我們要使用GCD,需要把任務提交到佇列。而我們可以用如下三種方法之一獲取要使用的佇列:

dispatch_queue_t
dispatch_queue_create(const char *_Nullable label,
        dispatch_queue_attr_t _Nullable attr)

dispatch_queue_t dispatch_get_main_queue(void)

dispatch_queue_t
dispatch_get_global_queue(long identifier, unsigned long flags)

第一種方法是建立一個queue,後兩種方法是獲取系統自定義的queue。

但其實背後的實現是,無論是自己建立還是獲取系統定義的queue,只會在GCD啟動時建立的root queue陣列中,取得一個queue而已。

使用者層面的queue和root queue之間的關係如下圖所示:
TODO

root queue一共有12個,分別有不同的優先順序和序列號。

讓我們一起看看它們的實現,首先,是dispatch_queue_create

dispatch_queue_create

dispatch_queue_t
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
{
    return _dispatch_queue_create_with_target(label, attr,
            DISPATCH_TARGET_QUEUE_DEFAULT, true);
}

可以看到,當用戶要建立一個queue的時候,需要指定target queue,即建立的queue最終是在哪個queue上執行的,這稱之為target queue。對於使用者建立的queue來說,這個target queue會取root queue之一。

這裡的DISPATCH_TARGET_QUEUE_DEFAULT是一個巨集定義:

#define DISPATCH_TARGET_QUEUE_DEFAULT NULL
static dispatch_queue_t
_dispatch_queue_create_with_target(const char *label, dispatch_queue_attr_t dqa,
        dispatch_queue_t tq, bool legacy)
{
    if (!slowpath(dqa)) { // 如果是序列佇列
        dqa = _dispatch_get_default_queue_attr(); // 序列佇列的attr 取預設attr
    } else if (dqa->do_vtable != DISPATCH_VTABLE(queue_attr)) { // 並行佇列的 attr->do_vtable 應該等於 DISPATCH_VTABLE(queue_attr)
        DISPATCH_CLIENT_CRASH(dqa->do_vtable, "Invalid queue attribute");
    }

    //
    // Step 1: Normalize arguments (qos, overcommit, tq)
    //

    // dispatch_qos_t qos是優先順序?
    dispatch_qos_t qos = _dispatch_priority_qos(dqa->dqa_qos_and_relpri);
    // 是否overcommit(即queue建立的執行緒數是否允許超過實際的CPU個數)
    _dispatch_queue_attr_overcommit_t overcommit = dqa->dqa_overcommit;
    if (overcommit != _dispatch_queue_attr_overcommit_unspecified && tq) { //
        if (tq->do_targetq) { // overcommit 的queue 必須是全域性的
            DISPATCH_CLIENT_CRASH(tq, "Cannot specify both overcommit and "
                    "a non-global target queue");
        }
    }

    // 下面這些程式碼,因為使用者建立的queue的tq一定為NULL,因此,只要關注tq == NULL的分支即可,我們刪除了其餘分支
    if (!tq) { // 自己建立的queue,tq都是null
        tq = _dispatch_get_root_queue( // 在root queue裡面去取一個合適的queue當做target queue
                qos == DISPATCH_QOS_UNSPECIFIED ? DISPATCH_QOS_DEFAULT : qos, // 無論是使用者建立的序列還是並行佇列,其qos都沒有指定,因此,qos這裡都取DISPATCH_QOS_DEFAULT
                overcommit == _dispatch_queue_attr_overcommit_enabled);
        if (slowpath(!tq)) { // 如果根據create queue是傳入的屬性無法獲取到對應的tq,crash
            DISPATCH_CLIENT_CRASH(qos, "Invalid queue attribute");
        }
    }

    //
    // Step 2: Initialize the queue
    //
    const void *vtable;
    dispatch_queue_flags_t dqf = 0;
    // 根據不同的queue型別,設定vtable。vtable實現了SERIAL queue 和 CONCURRENT queue的行為差異。
    if (dqa->dqa_concurrent) { // 併發
        vtable = DISPATCH_VTABLE(queue_concurrent);
    } else {  // 序列
        vtable = DISPATCH_VTABLE(queue_serial);
    }

    // 建立一個與tq對應的dq,用來給使用者返回
    dispatch_queue_t dq = _dispatch_object_alloc(vtable,
            sizeof(struct dispatch_queue_s) - DISPATCH_QUEUE_CACHELINE_PAD);
    // 初始化dq,可以看到dqa->dqa_concurrent,對於併發佇列,其queue width是DISPATCH_QUEUE_WIDTH_MAX,而序列佇列其width是1
    _dispatch_queue_init(dq, dqf, dqa->dqa_concurrent ? 
            DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
            (dqa->dqa_inactive ? DISPATCH_QUEUE_INACTIVE : 0));

    dq->dq_label = label; // 設定dq的名字
#if HAVE_PTHREAD_WORKQUEUE_QOS
    dq->dq_priority = dqa->dqa_qos_and_relpri;
    if (overcommit == _dispatch_queue_attr_overcommit_enabled) {
        dq->dq_priority |= DISPATCH_PRIORITY_FLAG_OVERCOMMIT;
    }
#endif
    _dispatch_retain(tq);
    if (qos == QOS_CLASS_UNSPECIFIED) { // 如果沒有指定queue的優先順序,則預設繼承target queue的優先順序
        // legacy way of inherithing the QoS from the target
        _dispatch_queue_priority_inherit_from_target(dq, tq);
    }
    if (!dqa->dqa_inactive) {
        _dispatch_queue_inherit_wlh_from_target(dq, tq);
    }
    dq->do_targetq = tq; // 這一步,很關鍵!!! 將root queue設定為dq的target queue,root queue和新建立的queue聯合在了一起
    _dispatch_object_debug(dq, "%s", __func__);
    // 將新建立的dq,新增到GCD內部管理的叫做_dispatch_introspection的queue列表中。這是GCD內部維護的一個queue列表,具體作用不太清楚。
    return _dispatch_introspection_queue_create(dq);
}

去除多餘的判斷,其實邏輯也很簡單,當我們呼叫dispatch_queue_create,GCD內部會呼叫_dispatch_queue_create_with_target, 它首先會根據我們建立的queue的屬性:DISPATCH_QUEUE_SERIALDISPATCH_QUEUE_CONCURRENT,到root queue陣列中取出一個對應的queue作為target queue。然後,會新建一個dispatch_queue_t物件,並設定其target queue,返回給使用者。同時,在GCD內部,新建的queue還會被加入introspection queue列表中。

這裡的關鍵是根據queue屬性獲取對應的target root queue:

tq = _dispatch_get_root_queue( // 在root queue裡面去取一個合適的queue當做target queue
                qos == DISPATCH_QOS_UNSPECIFIED ? DISPATCH_QOS_DEFAULT : qos, // 序列 用DISPATCH_QOS_DEFAULT, 並行 用自己的qos

_dispatch_get_root_queue的實現如下:

static inline dispatch_queue_t
_dispatch_get_root_queue(dispatch_qos_t qos, bool overcommit)
{
    return &_dispatch_root_queues[2 * (qos - 1) + overcommit]; // 根據qos和 是否overcommit,取得root queues陣列中對應的queue(一共有12個root queue)
}

qos==DISPATCH_QOS_DEFAULT,值是4。根據公式,2*(4-1) + 1 = 7, 所有建立的序列佇列應該取root queue陣列的第8個queue,而由於並行佇列的overcommit是false(0),並行佇列回去root queue的第7個元素:

// 使用者建立的並行佇列,預設會使用這個target queue
    _DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT, DISPATCH_PRIORITY_FLAG_DEFAULTQUEUE,
        .dq_label = "com.apple.root.default-qos",
        .dq_serialnum = 10,
    ),
    // 使用者建立的序列佇列,會使這個target queue, main queue 也會取這個值,但是會將 .dq_label = "com.apple.main-thread",
                                                                        // .dq_atomic_flags = DQF_THREAD_BOUND | DQF_CANNOT_TRYSYNC | DQF_WIDTH(1),
                                                                        // .dq_serialnum = 1,
    _DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT,
            DISPATCH_PRIORITY_FLAG_DEFAULTQUEUE | DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
        .dq_label = "com.apple.root.default-qos.overcommit",
        .dq_serialnum = 11,
    ),

OK,現在我們已經知道使用者自建立的queue預設都會附加到root queue上。那麼對於dispatch_get_main_queue, dispatch_get_global_queue是否也有類似的邏輯呢?我們先來看dispatch_get_global_queue

dispatch_get_global_queue

我們通常會用如下程式碼,來獲取一個全域性的併發佇列,並指定其優先順序。

dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

其實現如下:

dispatch_queue_t
dispatch_get_global_queue(long priority, unsigned long flags)
{
    if (flags & ~(unsigned long)DISPATCH_QUEUE_OVERCOMMIT) {
        return DISPATCH_BAD_INPUT;
    }
    dispatch_qos_t qos = _dispatch_qos_from_queue_priority(priority); // 將使用者使用的優先順序轉換為root queue的優先順序
    if (qos == DISPATCH_QOS_UNSPECIFIED) {
        return DISPATCH_BAD_INPUT;
    }
    return _dispatch_get_root_queue(qos, flags & DISPATCH_QUEUE_OVERCOMMIT); // 由於flags是保留值,均取0,因此global queue都是no overcommit的
}

邏輯很簡單,首先將外部傳入的queue優先順序轉換為GCD內部的優先順序dispatch_qos_t qos。 然後,在呼叫_dispatch_get_root_queue 獲取root queue中對應的queue。

我們來看一下_dispatch_qos_from_queue_priority的實現:

static inline dispatch_qos_t
_dispatch_qos_from_queue_priority(long priority)
{
    switch (priority) {
    case DISPATCH_QUEUE_PRIORITY_BACKGROUND:      return DISPATCH_QOS_BACKGROUND;
    case DISPATCH_QUEUE_PRIORITY_NON_INTERACTIVE: return DISPATCH_QOS_UTILITY;
    case DISPATCH_QUEUE_PRIORITY_LOW:             return DISPATCH_QOS_UTILITY;
    case DISPATCH_QUEUE_PRIORITY_DEFAULT:         return DISPATCH_QOS_DEFAULT;
    case DISPATCH_QUEUE_PRIORITY_HIGH:            return DISPATCH_QOS_USER_INITIATED;
    default: return _dispatch_qos_from_qos_class((qos_class_t)priority);
    }
}

dispatch_get_main_queue

/*!
 * @function dispatch_get_main_queue
 *
 * @abstract
 * Returns the default queue that is bound to the main thread.
 *
 * @discussion
 * In order to invoke blocks submitted to the main queue, the application must
 * call dispatch_main(), NSApplicationMain(), or use a CFRunLoop on the main
 * thread.
 *
 * @result
 * Returns the main queue. This queue is created automatically on behalf of
 * the main thread before main() is called.
 */
dispatch_queue_t
dispatch_get_main_queue(void)
{
    return DISPATCH_GLOBAL_OBJECT(dispatch_queue_t, _dispatch_main_q);
}

根據註釋,可以看到,main queue是由系統在main()方法呼叫前建立的。專門繫結到main thread上。而且,為了觸發提交到main queue上的block,和其他queue不一樣,main queue上的任務是依賴於main runloop觸發的

_dispatch_main_q

struct dispatch_queue_s _dispatch_main_q = {
    DISPATCH_GLOBAL_OBJECT_HEADER(queue_main),
#if !DISPATCH_USE_RESOLVERS
    .do_targetq = &_dispatch_root_queues[
            DISPATCH_ROOT_QUEUE_IDX_DEFAULT_QOS_OVERCOMMIT],  // 同樣也是取root queue中的queue作為target queue
#endif
    .dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(1) |
            DISPATCH_QUEUE_ROLE_BASE_ANON,
    .dq_label = "com.apple.main-thread",
    .dq_atomic_flags = DQF_THREAD_BOUND | DQF_CANNOT_TRYSYNC | DQF_WIDTH(1),
    .dq_serialnum = 1,
};

總結

從上面原始碼可以看出,GCD用到的queue,無論是自己建立的,或是獲取系統的main queue還是global queue,其最終都是落腳於GCD root queue中。 我們可以在程式碼中的任意位置建立queue,但最終GCD管理的,也不過這12個root queue,這種思路有點類似於命令模式,即分散建立,集中管理