An overview of the
The Dispatch Source is a wrapper around the BSD kernel’s customary kQueue, which is a technique that performs processing on the application programmer side when various events occur in the XNU kernel. It has a very low CPU load and uses as little resources as possible. When an event occurs, the Dispatch Source performs the processing of the event in the specified Dispatch Queue.
Use paper
The most common use of dispatch_source is to implement timers as follows:
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); dispatch_source_set_timer(source, dispatch_time(DISPATCH_TIME_NOW, 0), 3 * NSEC_PER_SEC, 0); Dispatch_source_set_event_handler (source, ^{NSLog(@"timer responded ")); }); // Start timer dispatch_resume(source); The code of the Dispatch Source timer looks very simple, but in fact it is the most corrupt API in GCD, and if handled badly, it can easily cause Crash. Please refer to the summary at the end of this article for the knowledge points needing attention about the Dispatch Source timer.Copy the code
The principle of article
dispatch_source_create
The dispatch_source_create function creates a dispatch_source_t object.
dispatch_source_t dispatch_source_create(dispatch_source_type_t type, uintptr_t handle, unsigned long mask, Dispatch_queue_t q) {dispatch_alloc(DISPATCH_VTABLE(source), sizeof(struct dispatch_source_s)); // initialize ds _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 // Default is temporary, you need to manually call resume ds->do_suspend_cnt = DISPATCH_OBJECT_SUSPEND_INTERVAL; ds->do_targetq = &_dispatch_mgr_q; // Specified target queue dispatch_set_target_queue(ds, q); _dispatch_object_debug(ds, "%s", __func__); return ds; }Copy the code
dispatch_source_set_timer
Dispatch_source_set_timer actually calls _dispatch_source_set_timer. Look at the code:
static inline void _dispatch_source_set_timer(dispatch_source_t ds, dispatch_time_t start, uint64_t interval, Uint64_t leeway, bool source_sync) { ds->ds_is_timer) || slowpath(ds_timer(ds->ds_refs).flags & DISPATCH_TIMER_INTERVAL)) { DISPATCH_CLIENT_CRASH("Attempt to set timer on a non-timer source"); } // create dispatch_set_timer_params struct dispatch_set_timer_params; params = _dispatch_source_timer_params(ds, start, interval, leeway); _dispatch_source_timer_telemetry(ds, params->ident, ¶ms->values); dispatch_retain(ds); If (source_sync) {// Use source as a queue, execute dispatch_barrier_async_f, // return _dispatch_barrier_trysync_f((dispatch_queue_t)ds, params, _dispatch_source_set_timer2); } else { return _dispatch_source_set_timer2(params); }}Copy the code
_dispatch_source_set_timer actually calls the _dispatch_source_set_timer2 function:
static void _dispatch_source_set_timer2(void *context) { // Called on the source queue struct dispatch_set_timer_params *params = context; // Pause the queue to avoid triggering the timer during the modification process. dispatch_suspend(params->ds); // Execute _dispatch_source_set_timer3(params) dispatch_barrier_async_f(&_dispatch_mgr_q, params, _dispatch_source_set_timer3); }Copy the code
The logic of the _dispatch_source_set_timer2 function is to execute _dispatch_source_set_timer3(params) on the _dispatch_mgr_q queue, and the following logic follows:
static void _dispatch_source_set_timer3(void *context) { // Called on the _dispatch_mgr_q struct dispatch_set_timer_params *params = context; dispatch_source_t ds = params->ds; ds->ds_ident_hack = params->ident; ds_timer(ds->ds_refs) = params->values; ds->ds_pending_data = 0; (void)dispatch_atomic_or2o(ds, ds_atomic_flags, DSF_ARMED, release); // Resume the queue, corresponding to dispatch_suspend dispatch_resume(ds) in the _dispatch_source_set_timer2 function; // The timer Must happen after resume to avoid getting disarmed due to suspension _dispatch_timers_update(ds); dispatch_release(ds); if (params->values.flags & DISPATCH_TIMER_WALL_CLOCK) { _dispatch_mach_host_calendar_change_register(); } free(params); }Copy the code
When a block submitted to the _dispatch_mGR_Q queue is executed, the &_dispatch_mGR_q -> DO_invoke function is called, that is, the _dispatch_mGR_thread defined in the Vtable of &_dispatch_MGR_Q. The next step is the _dispatch_mgr_invoke function. Select from I/O multiplexing to implement the timer function:
r = select(FD_SETSIZE, &tmp_rfds, &tmp_wfds, NULL,
poll ? (struct timeval*)&timeout_immediately : NULL);
Copy the code
When the inner _dispatch_mGR_Q queue is woken up, the outer queue (the one originally specified by the user) is further woken up and the timer triggered block is executed on the specified queue.
dispatch_source_set_event_handler
void dispatch_source_set_event_handler(dispatch_source_t ds, Handler = _dispatch_block_t handler) {// Copy blocks to a queue. _dispatch_barrier_trysync_f((dispatch_queue_t)ds, handler, _dispatch_source_set_event_handler2); } static void _dispatch_source_set_event_handler2(void *context) { dispatch_source_t ds = (dispatch_source_t)_dispatch_queue_get_current(); dispatch_assert(dx_type(ds) == DISPATCH_SOURCE_KEVENT_TYPE); dispatch_source_refs_t dr = ds->ds_refs; if (ds->ds_handler_is_block && dr->ds_handler_ctxt) { Block_release(dr->ds_handler_ctxt); Dr ->ds_handler_func = context? _dispatch_Block_invoke(context) : NULL; dr->ds_handler_ctxt = context; ds->ds_handler_is_block = true; }Copy the code
dispatch_source_set_cancel_handler
The dispatch_source_set_cancel_handler function is similar to that of dispatch_source_set_event_handler. The code is as follows:
void dispatch_source_set_cancel_handler(dispatch_source_t ds, Handler = _dispatch_block_t handler) {// Copy blocks to a queue. _dispatch_barrier_trysync_f((dispatch_queue_t)ds, handler, _dispatch_source_set_cancel_handler2); } static void _dispatch_source_set_cancel_handler2(void *context) { dispatch_source_t ds = (dispatch_source_t)_dispatch_queue_get_current(); dispatch_assert(dx_type(ds) == DISPATCH_SOURCE_KEVENT_TYPE); dispatch_source_refs_t dr = ds->ds_refs; if (ds->ds_cancel_is_block && dr->ds_cancel_handler) { Block_release(dr->ds_cancel_handler); } // save event cancellation information Dr ->ds_cancel_handler = context; ds->ds_cancel_is_block = true; }Copy the code
dispatch_resume/dispatch_suspend
// restore void dispatch_resume(dispatch_object_t dou) {DISPATCH_OBJECT_TFB(_dispatch_objc_resume, dou); // Global objects cannot be suspended or resumed. if (slowpath(dou._do->do_ref_cnt == DISPATCH_OBJECT_GLOBAL_REFCNT) || slowpath(dx_type(dou._do) == DISPATCH_QUEUE_ROOT_TYPE)) { return; } // Reduce the atomicity of do_suspend_cnt by two, Unsigned int suspend_cnt = dispatch_atomic_sub_orig2O (dou._do, do_suspend_cnt, DISPATCH_OBJECT_SUSPEND_INTERVAL, relaxed); if (fastpath(suspend_cnt > DISPATCH_OBJECT_SUSPEND_INTERVAL)) { return _dispatch_release(dou._do); } if (fastpath(suspend_cnt == DISPATCH_OBJECT_SUSPEND_INTERVAL)) { _dispatch_wakeup(dou._do); return _dispatch_release(dou._do); } DISPATCH_CLIENT_CRASH("Over-resume of an object"); } // suspend void dispatch_suspend(dispatch_object_t dou) {DISPATCH_OBJECT_TFB(_dispatch_objc_suspend, dou); if (slowpath(dou._do->do_ref_cnt == DISPATCH_OBJECT_GLOBAL_REFCNT) || slowpath(dx_type(dou._do) == DISPATCH_QUEUE_ROOT_TYPE)) { return; } // add two (void) dispatch_atomic_add2O (dou._do, do_suspend_cnt, DISPATCH_OBJECT_SUSPEND_INTERVAL, relaxed); _dispatch_retain(dou._do); }Copy the code
If the queue is paused, do_SUSpend_cnt is greater than or equal to 2. By default, both the global queue and the main queue are started. In the dispatch_source_create method, do_suspend_cnt is initially DISPATCH_OBJECT_SUSPEND_INTERVAL, which is suspended by default. You need to manually invoke resume to enable it. The code is defined as follows:
#define DISPATCH_OBJECT_SUSPEND_LOCK 1u #define DISPATCH_OBJECT_SUSPEND_INTERVAL 2u #define DISPATCH_OBJECT_SUSPENDED(x) \ ((x)-> do_suspend_cnT >= DISPATCH_OBJECT_SUSPEND_INTERVAL) dispatch_after dispatch_after The dispatch_after_f function calls dispatch_after_f internally as follows: void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void *ctxt, dispatch_function_t func) { uint64_t delta, leeway; dispatch_source_t ds; // DISPATCH_TIME_FOREVER if (when == DISPATCH_TIME_FOREVER) {#if DISPATCH_DEBUG DISPATCH_CLIENT_CRASH( "dispatch_after_f() called with 'when' == infinity"); #endif return; } delta = _dispatch_timeout(when); if (delta == 0) { return dispatch_async_f(queue, ctxt, func); } leeway = delta / 10; // <rdar://problem/13447496> if (leeway < NSEC_PER_MSEC) leeway = NSEC_PER_MSEC; if (leeway > 60 * NSEC_PER_SEC) leeway = 60 * NSEC_PER_SEC; // This function can and should be optimized to not use a dispatch_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); dispatch_assert(ds); dispatch_continuation_t dc = _dispatch_continuation_alloc(); dc->do_vtable = (void *)(DISPATCH_OBJ_ASYNC_BIT | DISPATCH_OBJ_BARRIER_BIT); dc->dc_func = func; dc->dc_ctxt = ctxt; dc->dc_data = ds; // Store dispatch_continuation_t in the dispatch_set_context(ds, dc); // Set timer and start dispatch_source_set_event_handler_f(ds, _dispatch_after_timer_callback); dispatch_source_set_timer(ds, when, DISPATCH_TIME_FOREVER, leeway); dispatch_resume(ds); } after timer, the _dispatch_after_timer_callback function is called, where the block in the context is fetched and executed: void _dispatch_after_timer_callback(void *ctxt) { dispatch_continuation_t dc = ctxt, dc1; dispatch_source_t ds = dc->dc_data; dc1 = _dispatch_continuation_free_cacheonly(dc); // Execute the block of the task and execute _dispatch_client_callout(dc->dc_ctxt, dc->dc_func); // Clear data dispatch_source_cancel(ds); dispatch_release(ds); if (slowpath(dc1)) { _dispatch_continuation_free_to_cache_limit(dc1); }}Copy the code
Summarizes the article
Dispatch Source is most used to implement the timer. After the Source is created, it is suspended by default. You need to manually call dispatch_resume to start the timer. Dispatch_after simply encapsulates the call to the Dispatch source timer and then executes the defined block in the callback function.
Note that the Dispatch Source timer may cause a crash:
-
Circular reference: Because the dispatch_source_set_event_handler callback is a block that is copied when added to the source list and is strongly referenced by the source, if the block holds self and self holds the source, It causes circular references. The correct way to cancel the timer is to use weak+strong or call dispatch_source_cancel in advance.
-
There is a balance between dispatch_resume and dispatch_suspend calls. If dispatch_resume is called repeatedly, it crashes because it disestablishes the if branch of the dispatch_resume code. DISPATCH_CLIENT_CRASH(“Over-resume of an object”) was executed causing the crash.
-
Source in the suspend state, setting source = nil or recreating source will cause crash. The correct way is to restore the resume with dispatch_source_cancel(source).