從NSTimer的失效性談起(二):關於GCD Timer和libdispatch

1、GCD Timer的建立和安放

儘管GCD Timer並不依賴於NSRunLoop,但是有沒有可能在某種狀況下,GCD Timer也失效了?就比如一開始咱們也不知道NSTimer相應着一個runloop的某種mode。編程

先來看看GCD Timer的用法:api

dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, aQueue);

dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, ti * NSEC_PER_SEC, ti * 0.1 * NSEC_PER_SEC);

dispatch_source_set_event_handler(timer, ^{
    //...
});

dispatch_resume(timer);

考慮到NSTimer做爲timerSource被放到一個runloop的某種mode所相應的集合中,那麼咱們天然而然會聯想GCD Timer做爲dispatch_source_t被放到哪裏呢?數組

參考libdispatch的源代碼dispatch_source_create這個API爲一個dispatch_source_t類型的結構體ds作了分配內存和初始化操做。而後將其返回。bash

摘取當中代碼片斷來看:markdown

ds = _dispatch_alloc(DISPATCH_VTABLE(source),
            sizeof(struct dispatch_source_s));
    // Initialize as a queue first, then override some settings below.
    _dispatch_queue_init((dispatch_queue_t)ds);
    ds->dq_label = "source";

    ds->do_ref_cnt++; // the reference the manager queue holds
    ds->do_ref_cnt++; // since source is created suspended
    ds->do_suspend_cnt = DISPATCH_OBJECT_SUSPEND_INTERVAL;
    // The initial target queue is the manager queue, in order to get
    // the source installed. <rdar://problem/8928171>
    ds->do_targetq = &_dispatch_mgr_q;

從以上代碼片斷中可以獲得幾個信息:app

  1. 在命名方面,dispatch_source_t變量命名爲ds,從而可以判斷dq_label成員應該是屬於dispatch_queue_t的,而do_ref_cnt應該相應着dispatch_object_t這麼一個類型,ref_cnt引用計數則顯然是用來管理「對象」的生命週期;
  2. 考慮到出現了dispatch_object_t這麼一個類型,咱們可以天然而然地猜測dispatch_系列的結構體應該都「繼承自」dispatch_object_t。儘管C語言中沒有面向對象編程中的繼承這個概念,但僅僅要將dispatch_object_t結構體放在內存佈局的開始處(做爲「基類」)。則實現了繼承的概念。另一個樣例是Python的C實現,詳細可以參考Python源代碼剖析一書;
  3. 從最後三行的凝視來看,默認初始化do_targetq爲_dispatch_mgr_q。這是爲了保證source被安裝,因此可以初步獲得一個dispatch_source_t的安放信息。需要注意的是_dispatch_mgr_q在GCD中是個很是重要的角色,從命名也可以看出基本是做爲單例管理隊列來進行調度分發的;
  4. 進一步證實了即使 dispatch_source_create這個API不傳入queue參數。timer也可以有效工做,因爲這個參數僅僅是用來代表回調在哪裏運行,假設沒有傳入,回調則交於root queue來分發;固然,假設有傳入queue參數,則會將該參數做爲targetq。

2、libdispatch的基本結構關係

上面提到了「基類」的概念,這裏先看下「基類」的佈局:dom

#define DISPATCH_STRUCT_HEADER(x) \
    _OS_OBJECT_HEADER( \
    const struct dispatch_##x##_vtable_s *do_vtable, \
    do_ref_cnt, \
    do_xref_cnt); \
    struct dispatch_##x##_s *volatile do_next; \
    struct dispatch_queue_s *do_targetq; \
    void *do_ctxt; \
    void *do_finalizer; \
    unsigned int volatile do_suspend_cnt;

struct dispatch_object_s {
    DISPATCH_STRUCT_HEADER(object);
};

從命名上來看,dispatch_系列的結構體都應該有這麼一個Header部分。
也就是說在libdispatch中,很是多結構體都繼承自上述基類:異步

struct dispatch_queue_s {
    DISPATCH_STRUCT_HEADER(queue);
        DISPATCH_QUEUE_HEADER;
    //...省略部分代碼
};

struct dispatch_semaphore_s {
    DISPATCH_STRUCT_HEADER(semaphore);
        //...省略部分代碼
}

struct dispatch_source_s {
    DISPATCH_STRUCT_HEADER(source);
    //...省略部分代碼
};

//...省略其餘繼承演示樣例

3、再看dispatch_source_t

當中,dispatch_source_t做爲咱們眼下的重點討論對象,作一下延伸:async

struct dispatch_source_s {
    DISPATCH_STRUCT_HEADER(source);
    DISPATCH_QUEUE_HEADER;
    DISPATCH_SOURCE_HEADER(source);
    unsigned long ds_ident_hack;
    unsigned long ds_data;
    unsigned long ds_pending_data;
};

除了開頭的DISPATCH_STRUCT_HEADER,緊接着的是DISPATCH_QUEUE_HEADER,接下來纔是DISPATCH_SOURCE_HEADERide

也就是說,除了基類信息,一個dispatch_source_t還包括着queue的信息。而在DISPATCH_SOURCE_HEADER中,第一個成員例如如下:

#define DISPATCH_SOURCE_HEADER(refs) \
    dispatch_kevent_t ds_dkev; \
        //...省略部分代碼

struct dispatch_kevent_s {
    TAILQ_ENTRY(dispatch_kevent_s) dk_list;
    TAILQ_HEAD(, dispatch_source_refs_s) dk_sources;
    struct kevent64_s dk_kevent;
};

typedef struct dispatch_kevent_s *dispatch_kevent_t;

這個成員在dispatch_source_create方法中也會被初始化,以備用來興許事件監聽。

4、Timer類型dispatch_source_t的處理

以上討論的基本是通用的dispatch_source_t相關處理。接下來討論一個GCD Timer的真正處理流程,主要是dispatch_source_set_timer這個API:

void
dispatch_source_set_timer(dispatch_source_t source,
    dispatch_time_t start,
    uint64_t interval,
    uint64_t leeway);

在這種方法中,會將定時器的相關信息封裝在一個dispatch_set_timer_params結構體中做爲上下文參數params,交由_dispatch_mgr_q來異步調用_dispatch_source_set_timer2方法:

// 不一樣版本號不同。這裏取了比較easy理解的版本號作演示樣例
dispatch_barrier_async_f(&_dispatch_mgr_q, params, _dispatch_source_set_timer2);

這種方法也是做爲GCD API暴露給開發人員的,在這種方法中作了進一步封裝:

// ...省略部分代碼
    dispatch_continuation_t dc = fastpath(_dispatch_continuation_alloc_cacheonly());

    dc->do_vtable = (void *)(DISPATCH_OBJ_ASYNC_BIT | DISPATCH_OBJ_BARRIER_BIT);
    dc->dc_func = func;
    dc->dc_ctxt = context;

    _dispatch_queue_push(dq, dc);

這裏將相關參數信息以及接下來要調用的方法名封裝做爲一個dispatch_continuation_t結構體。可以理解爲一個隊列任務塊,而後push到隊列中——這裏的隊列是_dispatch_mgr_q

到這裏咱們可以更清晰地瞭解到GCD內部是怎樣對咱們調用的API進行封裝、進隊,而後進一步分發運行。

5、熟悉又陌生的com.apple.libdispatch-manager

做爲iOS開發,咱們對com.apple.libdispatch-manager這個字符串應該很是熟悉,比方在crash日誌中看過,也會在斷點調試時遇到——它基本都是緊隨在主線程以後。

這個字符串所相應的隊列就是上文提到的_dispatch_mgr_q

static const struct dispatch_queue_vtable_s _dispatch_queue_mgr_vtable = {
    .do_type = DISPATCH_QUEUE_MGR_TYPE,
    .do_kind = "mgr-queue",
    .do_invoke = _dispatch_mgr_invoke,
    .do_debug = dispatch_queue_debug,
    .do_probe = _dispatch_mgr_wakeup,
};

// 6618342 Contact the team that owns the Instrument DTrace probe before renaming this symbol
struct dispatch_queue_s _dispatch_mgr_q = {
    .do_vtable = &_dispatch_queue_mgr_vtable,
    .do_ref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT,
    .do_xref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT,
    .do_suspend_cnt = DISPATCH_OBJECT_SUSPEND_LOCK,
    .do_targetq = &_dispatch_root_queues[DISPATCH_ROOT_QUEUE_COUNT - 1],

    .dq_label = "com.apple.libdispatch-manager",
    .dq_width = 1,
    .dq_serialnum = 2,
};

咱們發現,就連_dispatch_mgr_q都有它相應的do_targetq,從命名上來看,可以初步判斷_dispatch_mgr_q要作的事情終於都會丟到它的targetq上來完畢。

實際上,在libdispatch中,僅僅要有targetq,都會一層一層地往上扔。直到盡頭。那麼盡頭在哪裏呢?這裏引用Concurrent Programming: APIs and Challenges裏的一張圖:

盡頭在GCD的線程池。

6、GCD的盡頭:root queue和線程池

回過頭來看_dispatch_mgr_qdo_targetq,是_dispatch_root_queues中的最後一個元素。而root queue數組中按優先級升序排列:

// 老版本號libdispatch的代碼,新版本號不一樣
static struct dispatch_queue_s _dispatch_root_queues[] = {
    {
        .do_vtable = &_dispatch_queue_root_vtable,
        .do_ref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT,
        .do_xref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT,
        .do_suspend_cnt = DISPATCH_OBJECT_SUSPEND_LOCK,
        .do_ctxt = &_dispatch_root_queue_contexts[0],

        .dq_label = "com.apple.root.low-priority",
        .dq_running = 2,
        .dq_width = UINT32_MAX,
        .dq_serialnum = 4,
    },
    {
        // ... 省略部分代碼
        .dq_label = "com.apple.root.low-overcommit-priority",
    },
    {
        // ... 省略部分代碼
        .dq_label = "com.apple.root.default-priority",
    },
    {
        // ... 省略部分代碼
        .dq_label = "com.apple.root.default-overcommit-priority",
    },
    {
        // ... 省略部分代碼
        .dq_label = "com.apple.root.high-priority",
    },
    {
                // ... 省略部分代碼
        .dq_label = "com.apple.root.high-overcommit-priority",
    },
};

可以看到,在老版本號的libdispatch中,_dispatch_mgr_q是取最高優先級的root queue來做爲do_targetq的。而在新版本號中,則是有專門爲其服務的root queue:

static struct dispatch_queue_s _dispatch_mgr_root_queue = {
    .do_vtable = DISPATCH_VTABLE(queue_root),
    .do_ref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT,
    .do_xref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT,
    .do_suspend_cnt = DISPATCH_OBJECT_SUSPEND_LOCK,
    .do_ctxt = &_dispatch_mgr_root_queue_context,
    .dq_label = "com.apple.root.libdispatch-manager",
    .dq_running = 2,
    .dq_width = DISPATCH_QUEUE_WIDTH_MAX,
    .dq_serialnum = 3,
};

static struct dispatch_queue_s _dispatch_mgr_root_queue = {
    .do_vtable = DISPATCH_VTABLE(queue_root),
    .do_ref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT,
    .do_xref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT,
    .do_suspend_cnt = DISPATCH_OBJECT_SUSPEND_LOCK,
    .do_ctxt = &_dispatch_mgr_root_queue_context,
    .dq_label = "com.apple.root.libdispatch-manager",
    .dq_running = 2,
    .dq_width = DISPATCH_QUEUE_WIDTH_MAX,
    .dq_serialnum = 3,
};

只是不論是老版本號仍是新版本號。_dispatch_mgr_qdo_targetq——最好仍是稱做_dispatch_mgr_root_queue——的VTABLE中,終於指向的方法都是_dispatch_queue_wakeup_global

// 老版本號
.do_probe = _dispatch_queue_wakeup_global,

// 新版本號
unsigned long
_dispatch_root_queue_probe(dispatch_queue_t dq)
{
    _dispatch_queue_wakeup_global(dq);
    return false;
}

也就是說,當任務一層一層終於丟到root queue上,觸發的是_dispatch_queue_wakeup_global這種方法。在這種方法中。則是線程池的相關維護,比方調用pthread_create建立線程來運行_dispatch_worker_thread方法。

到眼下爲止,咱們跳過了一些過程討論到了GCD的線程池,接下來咱們會先回過頭來看怎樣一步步走到線程的建立和運行的,再討論線程建立後要運行些什麼。

7、從任務安排到分發

咱們在第四部分討論到了_dispatch_queue_push(dq, dc);,將定時器相關信息以及下一步要調用的方法封裝成dispatch_continuation_t結構放到隊列_dispatch_mgr_q中。

那麼,_dispatch_mgr_q是作什麼的呢?可以先簡單直接地看看它一般在作什麼:

mgr

可以看到,它一般都是沒事幹等事來。先來看看它怎麼處於等事幹的狀態,也就是它怎麼被建立出來並初始化完畢的。

咱們從上圖調用棧可以看到線程入口是_dispatch_mgr_thread,它是做爲_dispatch_mgr_q的.do_invoke的:

DISPATCH_VTABLE_SUBCLASS_INSTANCE(queue_mgr, queue,
    .do_type = DISPATCH_QUEUE_MGR_TYPE,
    .do_kind = "mgr-queue",
    .do_invoke = _dispatch_mgr_thread,
    .do_probe = _dispatch_mgr_queue_probe,
    .do_debug = dispatch_queue_debug,
);

何時會觸發.do_invoke調用呢?在整個libdispatch中,僅僅有在元素出隊的時候纔會觸發:

static inline void
_dispatch_continuation_pop(dispatch_object_t dou)
{
    dispatch_continuation_t dc = dou._dc, dc1;
    dispatch_group_t dg;

    _dispatch_trace_continuation_pop(_dispatch_queue_get_current(), dou);
    if (DISPATCH_OBJ_IS_VTABLE(dou._do)) {
        return dx_invoke(dou._do);
    }

那就是說_dispatch_mgr_q從root queue出隊時會進入等事幹的狀態,那麼它是何時進隊的?當咱們要push任務塊進入隊列時。會喚醒該隊列並調用其.do_probe成員,而_dispatch_mgr_q相應的.do_probe_dispatch_mgr_wakeup

unsigned long
_dispatch_mgr_wakeup(dispatch_queue_t dq DISPATCH_UNUSED)
{
    if (_dispatch_queue_get_current() == &_dispatch_mgr_q) {
        return false;
    }

    static const struct kevent64_s kev = {
        .ident = 1,
        .filter = EVFILT_USER,
        .fflags = NOTE_TRIGGER,
    };

#if DISPATCH_DEBUG && DISPATCH_MGR_QUEUE_DEBUG
    _dispatch_debug("waking up the dispatch manager queue: %p", dq);
#endif

    _dispatch_kq_update(&kev);

    return false;
}

_dispatch_kq_update裏面會作一次性的初始化:dispatch_once_f(&pred, NULL, _dispatch_kq_init);,當中有運行到:

_dispatch_queue_push(_dispatch_mgr_q.do_targetq, &_dispatch_mgr_q);

也就是將_dispatch_mgr_q進隊並wakeup它的targetq。因爲它的targetq是root queue。因此就會調用到_dispatch_queue_wakeup_global,就到了咱們在第六部分講的GCD盡頭,建立或從線程池中獲取一個線程來運行_dispatch_worker_thread

static void *
_dispatch_worker_thread(void *context)
{
    dispatch_queue_t dq = context;
    // ... 省略部分代碼

    const int64_t timeout = 5ull * NSEC_PER_SEC;
    do {
        _dispatch_root_queue_drain(dq);
    } while (dispatch_semaphore_wait(&pqc->dpq_thread_mediator,
            dispatch_time(0, timeout)) == 0);

    // ... 省略部分代碼
    return NULL;
}

在drain一個queue的過程,就是儘量地將隊列裏面的任務塊一個個出隊,出隊時就會觸發出隊元素的.do_invoke,相應於_dispatch_mgr_q就是_dispatch_mgr_thread

void
_dispatch_mgr_thread(dispatch_queue_t dq DISPATCH_UNUSED)
{
    _dispatch_mgr_init();
    // never returns, so burn bridges behind us & clear stack 2k ahead
    _dispatch_clear_stack(2048);
    _dispatch_mgr_invoke();
}

static void
_dispatch_mgr_invoke(void)
{
    static const struct timespec timeout_immediately = { 0, 0 };
    struct kevent64_s kev;
    bool poll;
    int r;

    for (;;) {
        _dispatch_mgr_queue_drain();
        poll = _dispatch_mgr_timers();
        if (slowpath(_dispatch_select_workaround)) {
            poll = _dispatch_mgr_select(poll);
            if (!poll) continue;
        }
        poll = poll || _dispatch_queue_class_probe(&_dispatch_mgr_q);
        r = kevent64(_dispatch_kq, _dispatch_kevent_enable,
                _dispatch_kevent_enable ? 1 : 0, &kev, 1, 0,
                poll ? &timeout_immediately : NULL);
        _dispatch_kevent_enable = NULL;
        if (slowpath(r == -1)) {
            int err = errno;
            switch (err) {
            case EINTR:
                break;
            case EBADF:
                DISPATCH_CLIENT_CRASH("Do not close random Unix descriptors");
                break;
            default:
                (void)dispatch_assume_zero(err);
                break;
            }
        } else if (r) {
            _dispatch_kevent_drain(&kev);
        }
    }
}

一旦進入_dispatch_mgr_invoke。這個線程就進入了等事幹的狀態。

8、GCD Timer到期時的任務分發

上面講了_dispatch_mgr_q的初始化和工做過程。現在回過頭來繼續看GCD Timer的處理過程。

和第七部分開頭同樣:咱們在第四部分討論到了_dispatch_queue_push(dq, dc);,將定時器相關信息以及下一步要調用的方法封裝成dispatch_continuation_t結構放到隊列_dispatch_mgr_q中。

這時候咱們push了任務塊進入_dispatch_mgr_q,就會wakeup to drain,將任務塊pop出來:

static inline void
_dispatch_continuation_pop(dispatch_object_t dou)
{
    dispatch_continuation_t dc = dou._dc, dc1;
    dispatch_group_t dg;

    _dispatch_trace_continuation_pop(_dispatch_queue_get_current(), dou);
    if (DISPATCH_OBJ_IS_VTABLE(dou._do)) {
        return dx_invoke(dou._do);
    }

    // ... 省略部分代碼
    _dispatch_client_callout(dc->dc_ctxt, dc->dc_func);
    // ... 省略部分代碼
}

回頭看下咱們以前進隊時封裝的信息:

dispatch_continuation_t dc = _dispatch_continuation_alloc_from_heap();

    dc->do_vtable = (void *)(DISPATCH_OBJ_ASYNC_BIT | DISPATCH_OBJ_BARRIER_BIT);
    dc->dc_func = func;
    dc->dc_ctxt = ctxt;

而在pop過程當中的判斷條件是if (DISPATCH_OBJ_IS_VTABLE(dou._do)),相關代碼例如如下:

#define DISPATCH_OBJ_ASYNC_BIT 0x1
#define DISPATCH_OBJ_BARRIER_BIT 0x2
#define DISPATCH_OBJ_GROUP_BIT 0x4
// vtables are pointers far away from the low page in memory
#define DISPATCH_OBJ_IS_VTABLE(x) ((unsigned long)(x)->do_vtable > 127ul)

條件不知足。因此咱們運行了方法調用,一步步先進入了_dispatch_source_set_timer2再進入_dispatch_source_set_timer3,而後更新timer鏈表:

// Updates the ordered list of timers based on next fire date for changes to ds.
// Should only be called from the context of _dispatch_mgr_q.
static void
_dispatch_timers_update(dispatch_source_t ds)

這裏值得一提的是,假設定時器採用的是wall clock。那麼會作下額外的處理:

if (params->values.flags & DISPATCH_TIMER_WALL_CLOCK) { _dispatch_mach_host_calendar_change_register(); }

當定時器到期時就會運行_dispatch_wakeup(ds),而後一路push & wakeup直到root queue。一般咱們建立的queue所相應的targetq是default優先級的root queue,因此終於仍是走到了_dispatch_queue_wakeup_global來分配線程運行drain queue的pop動做:

_2016_03_15_7_54_21

終於回調出去。

9、GCD Timer的失效性

討論了那麼多。那麼GCD Timer是否是也有可能在某種狀況下失效呢?

關於定時器的有效工做,有兩個關鍵環節,一個是mgr queue。還有一個是root queue。

可以看到mgr queue僅僅是負責事件監聽和分發,可以理解是很是輕量級的、不該該也不一樣意存在失效的;而root queue則負責從線程池分配線程運行任務。線程池的大小眼下來看是255,並且有高低優先級之分。

咱們建立的GCD Timer的優先級是繼承自它的targetq的,而咱們正常建立的queue所相應的root queue優先級是default。因此說假設存在大量高優先級的任務派發。或者255個線程都卡住了。那麼GCD Timer是會被影響到的。

相關文章
相關標籤/搜索