從NSTimer的失效性談起(二):關於GCD Timer和libdispatch
一、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;
從以上代碼片段中能夠得到幾個信息:
- 在命名方面,
dispatch_source_t
變量命名為ds,從而能夠判斷dq_label成員應該是屬於dispatch_queue_t
的,而do_ref_cnt應該相應著dispatch_object_t
這麽一個類型,ref_cnt引用計數則顯然是用來管理“對象”的生命周期; - 考慮到出現了
dispatch_object_t
這麽一個類型,我們能夠自然而然地猜想dispatch_系列的結構體應該都“繼承自”dispatch_object_t。盡管C語言中沒有面向對象編程中的繼承這個概念,但僅僅要將dispatch_object_t結構體放在內存布局的開始處(作為“基類”)。則實現了繼承的概念。另外一個樣例是Python的C實現,詳細能夠參考Python源代碼剖析一書; - 從最後三行的凝視來看,默認初始化do_targetq為
_dispatch_mgr_q
。這是為了保證source被安裝,所以能夠初步得到一個dispatch_source_t
的安放信息。須要註意的是_dispatch_mgr_q
在GCD中是個非常重要的角色,從命名也能夠看出基本是作為單例管理隊列來進行調度分發的; - 進一步證明了即便
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_q
的do_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_q
的do_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