1. 程式人生 > >從NSTimer的失效性談起(二):關於GCD Timer和libdispatch

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

not 證明 note sta 理解 得到 team 其他 vtable

一、GCD Timer的創建和安放

盡管GCD Timer並不依賴於NSRunLoop,可是有沒有可能在某種情況下,GCD Timer也失效了?就好比一開始我們也不知道NSTimer相應著一個runloop的某種mode。

先來看看GCD Timer的用法:

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做了分配內存和初始化操作。然後將其返回。

摘取當中代碼片段來看:

    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;

從以上代碼片段中能夠得到幾個信息:

  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。

二、libdispatch的基本結構關系

上面提到了“基類”的概念,這裏先看下“基類”的布局:

#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);
    //...省略部分代碼
};

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

三、再看dispatch_source_t

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

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_HEADER

也就是說,除了基類信息,一個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方法中也會被初始化,以備用來興許事件監聽。

四、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進行封裝、進隊,然後進一步分發運行。

五、熟悉又陌生的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的線程池。

六、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的線程池,接下來我們會先回過頭來看怎樣一步步走到線程的創建和運行的,再討論線程創建後要運行些什麽。

七、從任務安排到分發

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

那麽,_dispatch_mgr_q是做什麽的呢?能夠先簡單直接地看看它通常在做什麽:

技術分享

能夠看到,它通常都是沒事幹等事來。先來看看它怎麽處於等事幹的狀態,也就是它怎麽被創建出來並初始化完畢的。

我們從上圖調用棧能夠看到線程入口是_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。這個線程就進入了等事幹的狀態。

八、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動作:

技術分享

終於回調出去。

九、GCD Timer的失效性

討論了那麽多。那麽GCD Timer是不是也有可能在某種情況下失效呢?

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

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

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

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