The source code of this paper is libdispatch-1173.40.5, which mainly analyzes the specific implementation principle of commonly used DISPATCH API. The dispatch_object_s data structure is used for subsequent analysis
structdispatch_object_s {conststructdispatch_object_vtable_s *do_vtable.intvolatiledo_ref_cnt// Reference countintvolatiledo_xref_cnt, // External reference count, the object memory is freed only when both are 0structdispatch_object_s *volatiledo_next;// Next dostructdispatch_queue_s *do_targetq;// Target queuevoid *do_ctxt; / / contextvoid *do_finalizer; // Call the function when it is destroyedunsignedint do_suspend_cnt; //suspend suspends the count
};
Copy the code
Where do_vtable contains the object type and function pointer;
dispatch_object_t
“Dispatch_object_t” is a union of “dispatch_object_t” for all data structures in the union.
typedefunion {
struct _os_object_s* _os_obj;structdispatch_object_s* _do;/ / the object structurestructdispatch_continuation_s* _dc;// The dispatch_aync block is encapsulated into this data structurestructdispatch_queue_s* _dq;/ / the queuestructdispatch_queue_attr_s* _dqa;// Queue attributesstructdispatch_group_s* _dg;// Group operationstructdispatch_source_s* _ds;/ / the source structurestructdispatch_mach_s* _dm;structdispatch_mach_msg_s* _dmsg;structdispatch_timer_aggregate_s* _dta;structdispatch_source_attr_s* _dsa;/ / the source attributestructdispatch_semaphore_s* _dsema;/ / semaphorestructdispatch_data_s* _ddata;structdispatch_io_s* _dchannel;structdispatch_operation_s* _doperation;structdispatch_disk_s* _ddisk;
} dispatch_object_t __attribute__((__transparent_union__));
Copy the code
dispatch_continuation_s
structdispatch_continuation_s {structdispatch_object_s *volatiledo_next;// Next taskdispatch_function_t dc_func; // The method of executionvoid *dc_ctxt; // Method contextvoid *dc_data; // Related datavoid *dc_other // Other information
}
Copy the code
Dispatch_continuation_s is the structure of the task in dispatch_continuation_s. Incoming blocks are queued as this structure object.
dispatch_queue_s
structdispatch_queue_s {structdispatch_queue_s *do_targetq;// Target queue, which eventually points to a system's default queuestructdispatch_object_s *volatiledq_items_head;// Queue headerstructdispatch_object_s *volatiledq_items_tail;// The end of the queueunsignedlong dq_serialnum; // Queue numberconstchar *dq_label; / / the queue namedispatch_priority_t dq_priority; / / prioritydispatch_priority_tvolatile dq_override; // Whether it is overwrittenuint16_t dq_width; // The number of tasks that can be executed concurrentlydispatch_queue_t dq_specific_q; // Special queueuint32_t dq_side_suspend_cnt; // The number of paused tasksconststructqueue_vtable_s *do_vtable {// Some function Pointers to the queueunsignedlongconst do_type; // Queue types, such as DISPATCH_QUEUE_CONCURRENT_TYPE, DISPATCH_QUEUE_SERIAL_TYPE, DISPATCH_QUEUE_GLOBAL_ROOT_TYPE...constchar *const do_kind; // Queue type, for example: "serial-queue", "concurrent-queue", "global-queue", "main-queue", "runloop-queue"" Mgr-queue "...void (*const do_dispose)(/*params*/); // Destroy the queuevoid (*const do_suspend)(/*params*/); // Pause the queuevoid (*const do_resume)(/*params*/); // Restore the queuevoid (*const do_invoke)(/*params*/); // Start processing the queuevoid (*const do_wakeup)(/*params*/); // Wake up the queuevoid (*const do_set_targetq)(/*params*/); // Set target queue
};
}
Copy the code
Dispatch_queue_s is the structure of the queue. In its DO_vtable, there are many Pointers to functions corresponding to the queue operation methods. There should be some macros to call these methods in the queue. For example, the do_dispose method should have a macro dx_Dispose:
#define dx_dispose(queue) &(queue)->do_vtable->_os_obj_vtable->do_dispose(queue)Copy the code
Understand the relationship between queues and threads
dispatch_once
The source code is as follows:
voiddispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func){
dispatch_once_gate_t l = (dispatch_once_gate_t)val;
#if! DISPATCH_ONCE_INLINE_FASTPATH || DISPATCH_ONCE_USE_QUIESCENT_COUNTER// atomically get l->dgo_onceuintptr_t v = os_atomic_load(&l->dgo_once, acquire);
// Check if the above value is DLOCK_ONCE_DONE(most likely yes, indicating that func has been assigned), if yes, return directlyif (likely(v == DLOCK_ONCE_DONE)) {
return;
}
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER// Different decision formsif (likely(DISPATCH_ONCE_IS_GEN(v))) {
return _dispatch_once_mark_done_if_quiesced(l, v);
}
#endif#endif// Atomicity determines whether an assignment has been madeif (_dispatch_once_gate_tryenter(l)) {
return _dispatch_once_callout(l, ctxt, func);
}
// The thread blocks waiting for dispatch_function_t func to completereturn _dispatch_once_wait(l);
}
staticinlinebool
_dispatch_once_gate_tryenter(dispatch_once_gate_t l)
{
// if l->dgo_once equals DLOCK_ONCE_UNLOCKED,// If so, assign the thread IDreturn os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED,
(uintptr_t)_dispatch_lock_value_for_self(), relaxed);
}
staticvoid
_dispatch_once_callout(dispatch_once_gate_t l, void *ctxt,
dispatch_function_t func)
{
// Call func in block
_dispatch_client_callout(ctxt, func);
// Broadcast to wake up all waiting threads
_dispatch_once_gate_broadcast(l);
}
Copy the code
The general process is as follows (copied from others’ drawings) :
dispatch_queue_t
dispatch_queue_create(constchar *label, dispatch_queue_attr_t attr)
{
return _dispatch_lane_create_with_target(label, attr,
DISPATCH_TARGET_QUEUE_DEFAULT, true);
}
//_dispatch_lane_create_with_targetstaticdispatch_queue_t
_dispatch_lane_create_with_target(constchar *label, dispatch_queue_attr_t dqa,
dispatch_queue_t tq, bool legacy)
{
//Step 1: Normalize arguments (qos, overcommit, tq)dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);// Returns an empty attribute object for serial queues (DQA is NULL), and a default initialization value for parallel queuesOvercommit whether new threads need to be created. Tq target queue is DISPATCH_TARGET_QUEUE_DEFAULT by defaultif(! tq) { tq = _dispatch_root_queues[2 * (qos - 1) + overcommit];// If the destination queue is not specified, it is obtained from the root queue array
}
Initialize the queue Initialize the queue Initialize the queue// Apply for memory spacedispatch_lane_t dq = _dispatch_object_alloc(vtable,
sizeof(struct dispatch_lane_s));
// Initialize the serial number from 17, the rest is as follows:// skip zero// 1-main_q // main queue// 2 -mgr_q // Manage the queue// 3 -mgr_root_q // The target queue of the management queue/ / 4,5,6,7,8,9,10,11,12,13,14,15 - global the queues, global queue has set up a file in the root queue array initialization, respectively according to different qos specified priority queue// 17 - workloop_fallback_q// we use 'xadd' on Intel, so the initial value == next assigned
dq->dq_serialnum = os_atomic_inc_orig(&_dispatch_queue_serial_numbers, relaxed);
// Number of concurrent queues (1 for serial queues and DISPATCH_QUEUE_WIDTH_MAX for parallel queues)
dq->dq_width = dqai.dqai_concurrent ? DISPATCH_QUEUE_WIDTH_MAX : 1;
dq->dq_label = label;// Specify a name
dq->dq_priority = _dispatch_priority_make((dispatch_qos_t)dqai.dqai_qos,
dqai.dqai_relpri);// Specify the priority
dq->do_targetq = tq;// Specify the destination queuereturn dq._dq;
}
// Allocated global queue, specifying queues of different qos classesstructdispatch_queue_global_s _dispatch_root_queues[] = {// Initialize the property. _DISPATCH_ROOT_QUEUE_ENTRY(MAINTENANCE,0,
.dq_label = "com.apple.root.maintenance-qos",
.dq_serialnum = 4,
),
_DISPATCH_ROOT_QUEUE_ENTRY(MAINTENANCE, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.maintenance-qos.overcommit",
.dq_serialnum = 5,
),
_DISPATCH_ROOT_QUEUE_ENTRY(BACKGROUND, 0,
.dq_label = "com.apple.root.background-qos",
.dq_serialnum = 6,
),
_DISPATCH_ROOT_QUEUE_ENTRY(BACKGROUND, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.background-qos.overcommit",
.dq_serialnum = 7,
),
_DISPATCH_ROOT_QUEUE_ENTRY(UTILITY, 0,
.dq_label = "com.apple.root.utility-qos",
.dq_serialnum = 8,
),
_DISPATCH_ROOT_QUEUE_ENTRY(UTILITY, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.utility-qos.overcommit",
.dq_serialnum = 9,
),
_DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT, DISPATCH_PRIORITY_FLAG_FALLBACK,
.dq_label = "com.apple.root.default-qos",
.dq_serialnum = 10,
),
_DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT,
DISPATCH_PRIORITY_FLAG_FALLBACK | DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.default-qos.overcommit",
.dq_serialnum = 11,
),
_DISPATCH_ROOT_QUEUE_ENTRY(USER_INITIATED, 0,
.dq_label = "com.apple.root.user-initiated-qos",
.dq_serialnum = 12,
),
_DISPATCH_ROOT_QUEUE_ENTRY(USER_INITIATED, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.user-initiated-qos.overcommit",
.dq_serialnum = 13,
),
_DISPATCH_ROOT_QUEUE_ENTRY(USER_INTERACTIVE, 0,
.dq_label = "com.apple.root.user-interactive-qos",
.dq_serialnum = 14,
),
_DISPATCH_ROOT_QUEUE_ENTRY(USER_INTERACTIVE, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.user-interactive-qos.overcommit",
.dq_serialnum = 15,),};Copy the code
Dispatch_queue_create creates a queue and initializes its parameters, including its name, priority, number of concurrent requests, sequence number, and destination queue.
The main queue is _dispatch_main_q queue, whose queue name is com.apple.main-thread, width is 1, serial number is 1.
async
voiddispatch_async(dispatch_queue_t dq, dispatch_block_t work){
// Allocate dispatch_CONTINUATION memorydispatch_continuation_t dc = _dispatch_continuation_alloc();
uintptr_t dc_flags = DC_FLAG_CONSUME;
dispatch_qos_t qos;
// Initialize the primary copy(work) to avoid the block being destroyed before execution;/ / specify the dc - > dc_ctxt = work;//dc->func=_dispatch_call_block_and_release; Used to release a block after it completes execution
qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
//_dispatch_continuation_async core functions are dX_push function Pointers saved in do_vtable
_dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
}
staticinlinevoid
_dispatch_continuation_async(dispatch_queue_class_t dqu,
dispatch_continuation_t dc, dispatch_qos_t qos, uintptr_t dc_flags)
{
#if DISPATCH_INTROSPECTIONif(! (dc_flags & DC_FLAG_NO_INTROSPECTION)) { _dispatch_trace_item_push(dqu, dc); }#else
(void)dc_flags;
#endifreturn dx_push(dqu._dq, dc, qos);// The core is the dq_push pointer stored in do_vtable
}
Copy the code
The pointer to the dx_push function is specified when init.c is initialized as follows:
//workloop class instance vtable
_dispatch_workloop_push
/ / queue_serial queue_runloop, source and channel, the Mach serial/runloop/source/channel/Mach
_dispatch_lane_push
//queue_concurrent concurrent queue
_dispatch_lane_concurrent_push
//queue_global,queue_pthread_root Global queue and root thread
_dispatch_root_queue_push
//queue_mgr manages queues
_dispatch_mgr_queue_push
/ / queue_main the home team
_dispatch_main_queue_push
Copy the code
The following analyzes common primary and global queues:
void _dispatch_main_queue_push(dispatch_queue_main_t dq, dispatch_object_t dou,
dispatch_qos_t qos)
{
/ / pseudo code
_disaptch_queue_push();//push to main queue
dx_wakeup()// Call the dq_wakeup function in do_vtable to wakeup the queue and execute
}
Copy the code
Call stack for MAC (ios has some differences) :
dx_wakeup
_dispatch_main_queue_wakeup
_dispatch_runloop_queue_wakeup
_dispatch_runloop_queue_poke
_dispatch_runloop_queue_handle_init// Dispatch_once_f dispatch_once_f dispatch_once_f dispatch_once_f dispatch_once_f dispatch_once_f dispatch_once_f dispatch_once_f
mach_port_construct// Build a thread-Mach port
_dispatch_runloop_queue_set_handle
dq->do_ctxt = (void(*)uintptr_t)handle
_dispatch_send_wakeup_runloop_thread// Wake up the runloop through the Mach port built aboveCopy the code
The main thread runloop call stack looks like this:
push mach port _dispatch_send_wakeup_runloop_thread mach port runloop runloop libdispatch.dylib _dispatch_main_queue_callback_4CF block
Global Queue call stack:
_dispatch_root_queue_push
_dispatch_root_queue_push_override
_dispatch_root_queue_push_inlineOs_mpsc_push_list Atomic push to the queue
_dispatch_root_queue_poke
_dispatch_root_queue_poke_slow
_pthread_workqueue_addthreads// Add the task to the work queueCopy the code
The work queue call thread stack looks like this:
global quque push root queue pop
Other queue types can be traced through the source code and call stack analysis, as long as you understand the working principle, the source code needs to deal with various situations of the queue logic is more complex;
dispatch_sync
_dispatch_sync_f
_dispatch_sync_f_inline
_dispatch_barrier_sync_f// Serial queue
_dispatch_queue_try_acquire_barrier_sync// Attempts to lock the queue. If the lock fails, the queue is being scheduled
_dispatch_sync_f_slow
_dispatch_sync_recurse// Nested calls with multiple target queues are recursive calls
_dispatch_lane_barrier_sync_invoke_and_complete// If none of the above occurs, the queue triggers the call immediately
_dispatch_sync_f_slow// Concurrent queue
_dispatch_sync_function_invoke
_dispatch_client_callout
__main_block_invoke
Copy the code
Execute tasks synchronously. If a task is being executed or the current queue is scheduled, the system locks the task (semaphore or KEvent) until the task is executed. Otherwise, the system directly executes the task to ensure synchronous execution.
The little knowledge
__builtin_expect
GCC introduced to optimize compiler instructions, avoid instruction jump, improve CPU efficiency;
By introducing pipeline into CPU, CPU efficiency can be improved. More simply, allowing the CPU to pre-fetch the next instruction provides CPU efficiency. As shown below:
It can be seen that CPU flow money can reduce the CPU waiting time for instructions, thus improving the EFFICIENCY of the CPU. If there is a jump instruction, the pre-fetched instruction is useless. When executing the current instruction, the CPU fetches the next instruction of the current instruction from memory. After executing the current instruction, the CPU discovers that instead of executing the next instruction, it is executing the instruction at offset. The CPU can only refetch instructions at offset from memory. Therefore, jump instructions reduce pipeline efficiency, and thus CPU efficiency. In summary, skip statements should be avoided when writing programs. So how do you avoid jump statements? The answer is **builtin_expect. This instruction was introduced by GCC to “allow the programmer to tell the compiler which branch is most likely to be executed.” The command is written as: **builtin_expect(EXP, N). The probability that e to the N is equal to N is high. The __builtin_expect directive is generally used to encapsulate it as likely and Unlikely macros, which are commonly used in kernel programming. These macros are written as follows:
#define likely(x) __builtin_expect(!! (x), 1) //x is likely to be true#define unlikely(x) __builtin_expect(!! (x), 0) //x is probably falseCopy the code
Just be clear:
ifLikely (value) // equivalent toif(value)
if(unlikely(value)) // Is equivalent toif(value)
Copy the code
However, statements following if are more likely to be executed with likely(), and statements following else are more likely to be executed with Unlikely (). In this way, the compiler can reduce the performance penalty of instruction jumps by following the most likely code right behind it during compilation.
Builtin_expect instructions
GCC __builtin_expect
os_atomic_cmpxchg
The p variable is equivalent to the PTR pointer of atomic_t type used to obtain the value of the current memory access restriction rule M, which is used to compare the old value E, and assign a new value V if it is equal.
staticinlineintatomic_cmpxchg(atomic_t *ptr, int old, intnew)Copy the code
Atomic_cmpxchg is used to compare the value of the Atmoic variable and replace it with the new value if it is equal to the value in memory. The PTR argument points to an atomic_t variable; The parameter old represents the value compared to memory; New represents the value of the substitution.
Atomic_cmpxchg source code analysis
Linux atomic_cmpxchg()/Atomic_read()/Atomic_set()/Atomic_add()/Atomic_sub()/atomi
std::memory_order
LLDB debugging
Use breakpoint matching and mirror matching to find function symbols, you can view all link library function symbols and break points, follow up the function call stack;
Breakpoint set -r -n _dispatch_ // Image lookup -r -n _dispatch_ libdispatch.dylib // Specify a matching function to find the libdispatch.dylib dynamic libraryCopy the code
LLDB supports Python script parsing, and you can import Python scripts for advanced debugging.