This chapter explores singletons, fence functions, semaphores, scheduling groups, and event sources from the perspective of source code
1: dispatch_once is a singleton
Singletons are also used a lot in development, where we use GCD’s dispatch_once function
static dispatch_once_t token;
dispatch_once(&token, ^{
// code
});
Copy the code
The definition is as follows:
#define dispatch_once _dispatch_once
Copy the code
Find _dispatch_once in libdispatch
For different cases,dispatch_compiler_barrier() is for fences, just go to dispatch_once
Finally call dispatch_once_f
- Will be introduced to
val
Packaged inl
Through theos_atomic_load
Pull it from the bottom, associate it with a variablev
On. ifv
This is equal toDLOCK_ONCE_DONE
You’ve already dealt with it oncereturn
To return.
_dispatch_once_gate_tryenter
Atomic operations are performed in _dispatch_once_gate_tryenter, that is, lock handling, so it is thread-safe. If it hasn’t been done before, the atomic process will compare its state, unlock it, and it will eventually return a bool, and in multithreaded cases, only one can get the lock and return yes.
If yes is returned, _dispatch_once_callout is called to perform the singleton and broadcast externally.
Look at the _dispatch_once_gate_broadcast
Compares the token through atoms. If not done, set it to done. Locks in the _dispatch_once_gate_tryenter method are also handled.
_dispatch_once_mark_done
When marked done, the next entry returns directly.
If there are multiple threads and no lock is obtained, then _dispatch_once_WAIT is called. Here the spin lock is enabled and atomic processing is performed internally. In the loop process, If it is found that once_done has already been set by another thread, it will abort.
A picture to summarize:
2: dispatch_barrier_async
We sometimes need to perform two sets of operations asynchronously, and after the first set of operations is complete, the second set of operations can be performed. We need a fence to separate two asynchronously executed action groups, which can contain one or more tasks. This requires the dispatch_barrier_async method to form a fence between the two action groups.
2.1 Basic Usage
dispatch_barrier_async
- (void)demo2{ dispatch_queue_t concurrentQueue = dispatch_queue_create("cc", DISPATCH_QUEUE_CONCURRENT); */ dispatch_async(concurrentQueue, ^{sleep(1); NSLog(@"123"); }); dispatch_async(concurrentQueue, ^{ sleep(2); NSLog(@"456"); }); */ / dispatch_barrier_sync dispatch_barrier_async(concurrentQueue, ^{ NSLog(@"---------------------%@------------------------",[NSThread currentThread]); NSLog(@"789"); }); /* * async(concurrentQueue, ^{NSLog(@" load so many, take a breath!!") ); }); NSLog(@"********** get up and do it!!" ); }Copy the code
The print result is as follows:
We found thatdispatch_barrier_async
It doesn’t block the main thread, soAt the end
Print first. But it blocks the task threadconcurrentQueue
, so123
and456
Prior to the789
Print, print lastLoad that much. Take a breath
.
“Dispatch_barrier_sync” is the same as above. After replacing “dispatch_barrier_async” with “dispatch_barrier_sync”, LET me see the output.
The only difference is that the end is printed last, which means dispatch_barrier_sync blocks the current thread, the main thread, so be careful when using this.
-
Matters needing attention
- Fence functions and other tasks must be in the same queue.
- Only custom concurrent queues can be used, not global concurrent queues.
2.2 Principle Analysis
dispatch_barrier_sync
void
dispatch_barrier_sync(dispatch_queue_t dq, dispatch_block_t work)
{
uintptr_t dc_flags = DC_FLAG_BARRIER | DC_FLAG_BLOCK;
if (unlikely(_dispatch_block_has_private_data(work))) {
return _dispatch_sync_block_with_privdata(dq, work, dc_flags);
}
_dispatch_barrier_sync_f(dq, work, _dispatch_Block_invoke(work), dc_flags);
}
Copy the code
Trace the source to _dispatch_barrier_sync_f -> _dispatch_barrier_sync_f_inline
Where _dispatch_queue_try_acquire_barrier_SYNC suspends, _dispatch_queue_try_acquire_barrier_sync_and_suspend, The current state state will be added a layer of processing, temporarily abandoned.
What is ultimately returned is the underlying OS control processing.
In addition, _dispatch_SYNC_F_slow also involves deadlock handling.
Example code main thread step cause deadlock, view call stack involved_dispatch_sync_f_slow
and__DISPATCH_WAIT_FOR_QUEUE__
_dispatch_sync_f_slow
In this method, tasks are added to the queue. For example, when a synchronization task is added to the main thread, the synchronization task is added to the main queue.
__DISPATCH_WAIT_FOR_QUEUE__
It can be understood that task A is to be executed, but the system has arranged it to wait before, so is it executed or wait? There are contradictions, waiting for each other, causing deadlocks.
Let’s go back to _dispatch_barrier_sync_f_inline and scroll down.
Based on the stack information, _dispatch_sync_INVOke_and_COMPLEte_RECURse is called
Follow the call path _dispatch_sync_INVOke_AND_COMPLEte_recurse -> _dispatch_sync_complete_recurse
Dx_wakeup will wakeup the task. _dispatch_LANe_non_barrier_complete will not be executed until the task is completed, indicating that the task in the current queue has completed. And there’s no barrier function left and we’re going to continue down the flow.
#define dx_wakeup(x, y, z) dx_vtable(x)->dq_wakeup(x, y, z)
Copy the code
Here we search dq_wakeup directly
The global concurrent _dispatch_root_queue_wakeup and the serial and concurrent _dispatch_lane_wakeup are performed in different ways depending on the queue.
First take a look at the _dispatch_lane_wakeup for custom concurrent queues
- Determine whether
barrier
Of the form, will call_dispatch_lane_barrier_complete
Methods to deal with - If there is no
barrier
Form, then go normal concurrent queue process, call_dispatch_queue_wakeup
Methods.
_dispatch_lane_barrier_complete
-
In a serial queue, the system waits for other tasks to complete and then executes them in sequence
-
If it is a concurrent queue, the _dispatch_lane_drain_non_barriers method is called to drain the tasks prior to the fence.
-
Finally, the _dispatch_lane_class_barrier_complete method is called, which unblocks the fence and executes the task behind the fence.
Look again at the _dispatch_root_queue_wakeup for global concurrent queues
-
Barriers are not handled in the global concurrent queue, but in the normal concurrent queue.
-
Why doesn’t the global concurrent queue handle the fence function? Because global concurrent queues are used by the system as well as by us.
-
If you add a barrier function, it will block queue execution and affect system-level execution, so the barrier function is not suitable for global concurrent queues
_dispatch_barrier_SYNc_F_INLINE ends with _dispatch_lane_barrier_sync_INVOke_AND_complete for the next layer state release of the completed task.
Three: signal quantity dispatch_semaphore
There is no way to control the number of concurrent requests in GCD, but we can “curve the country” and use semaphores to solve this problem.
A semaphoreDispatch Semaphore
Is something that holds the signal of counting. There are three methods.
dispatch_semaphore_create
: Creates a Semaphore and initializes the total amount of signalsdispatch_semaphore_signal
: the semaphore is sent, increasing the total number of signals by 1dispatch_semaphore_wait
: Semaphore wait, can reduce the total semaphore by 1, the total signal is less than 0 will wait (blocking thread), otherwise the normal execution.
3.1 Basic Usage
- (void)test { dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_semaphore_t sem = dispatch_semaphore_create(1); // dispatch_async(queue, ^{dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); NSLog(@" Perform task 1"); sleep(1); NSLog(@" Task 1 completed "); dispatch_semaphore_signal(sem); }); // dispatch_async(queue, ^{dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); NSLog(@" Perform task 2"); sleep(1); NSLog(@" Task 2 completed "); dispatch_semaphore_signal(sem); }); // dispatch_async(queue, ^{dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); NSLog(@" Perform task 3"); sleep(1); NSLog(@" Task 3 completed "); dispatch_semaphore_signal(sem); }); }Copy the code
Print result:
- We set the initial semaphore to
1
, which controls the maximum number of concurrent requests1
3.2 Principle Analysis
3.2.1 dispatch_semaphore_create
Dispatch_semaphore_create initializes the semaphore and sets the maximum number of concurrent GCD requests. The maximum number of concurrent GCD requests must be greater than or equal to 0
3.2.2 dispatch_semaphore_signal
long
dispatch_semaphore_signal(dispatch_semaphore_t dsema){
long value = os_atomic_inc2o(dsema, dsema_value, release);
if (likely(value > 0)) {
return 0;
}
if (unlikely(value == LONG_MIN)) {
DISPATCH_CLIENT_CRASH(value,
"Unbalanced call to dispatch_semaphore_signal()");
}
return _dispatch_semaphore_signal_slow(dsema);
}
Copy the code
Os_atomic_inc2o = atomic_inc2O = 1; If the value is still smaller than 0 after being added for a first time, an exception is reported. These devices need to be called to dispatch_semaphore_signal(), and then _dispatch_semaphore_signal_slow is invoked to perform lower-level processing and wait for a long time.
3.2.3 dispatch_semaphore_wait
long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout){
long value = os_atomic_dec2o(dsema, dsema_value, acquire);
if (likely(value >= 0)) {
return 0;
}
return _dispatch_semaphore_wait_slow(dsema, timeout);
}
Copy the code
Os_atomic_dec2o specifies that the atomic operation is reduced by 1. The system checks that value >= 0 and returns success. If the result is less than zero, a long wait is called _dispatch_semaphoRE_WAIT_slow.
_dispatch_semaphoRE_WAIT_slow is processed separately according to the timeout period.
The overall picture:
4: Dispatch_group
4.1 Basic Usage
Use a
- (void)demoTest {// Dispatch_group_t group = dispatch_group_create(); // dispatch_queue_t queue = dispatch_queue_create("cc", DISPATCH_QUEUE_CONCURRENT); Dispatch_group_async (group, queue, ^{sleep(1); NSLog(@" download A %@,[NSThread currentThread]); }); dispatch_group_async(group, queue, ^{ sleep(1); NSLog(@" download B %@,[NSThread currentThread]); }); dispatch_group_async(group, queue, ^{ sleep(1); NSLog(@" download C %@",[NSThread currentThread]); }); // Async: dispatch_group_notify(group, queue, ^{NSLog(@" download completed %@",[NSThread currentThread])); }); // dispatch_group_wait(group, DISPATCH_TIME_FOREVER); // Verify that the scheduling group is asynchronous NSLog(@"end"); }Copy the code
- It prints when A, B, and C are done
The download is complete
Usage 2 dispatch_group_Enter and dispatch_group_leave are used
/** * group dispatch_group_enter, dispatch_group_leave */ - (void)groupEnterAndLeave { NSLog(@"currentThread---%@",[NSThread currentThread]); // Prints the current thread NSLog(@"group-- --begin"); dispatch_group_t group = dispatch_group_create(); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_enter(group); Dispatch_async (queue, ^{NSThread sleepForTimeInterval:2]) dispatch_async(queue, ^{NSThread sleepForTimeInterval:2]; / / simulation time-consuming operation NSLog (@ "1 - % @", [NSThread currentThread]); Dispatch_group_leave (group); }); dispatch_group_enter(group); Dispatch_async (queue, ^{NSThread sleepForTimeInterval:2]) dispatch_async(queue, ^{NSThread sleepForTimeInterval:2]; / / simulation time-consuming operation NSLog (@ "2 - % @", [NSThread currentThread]); Dispatch_group_leave (group); }); Dispatch_group_notify (group, dispatch_get_main_queue(), ^{NSThread sleepForTimeInterval:2]; / / simulation time-consuming operation NSLog (@ "3 - % @", [NSThread currentThread]); // Prints the current thread NSLog(@"group-- end"); }); }Copy the code
Dispatch_group_enter and dispatch_group_leave are the same as dispatch_group_async, but they must be in pairs.
4.2 Source Code Analysis
2 dispatch_group_create
dispatch_group_create
dispatch_group_t
dispatch_group_create(void){
return _dispatch_group_create_with_count(0);
}
Copy the code
_dispatch_group_create_WITH_count is called with a fixed default value of 0, and the group is defined and assigned internally.
Static inline dispatch_group_t _dispatch_group_create_with_count(uint32_t n){// 🌹 defines dispatch_group_t dg = _dispatch_object_alloc(DISPATCH_VTABLE(group), sizeof(struct dispatch_group_s)); // 🌹 assignment dg->do_next = DISPATCH_OBJECT_LISTLESS; dg->do_targetq = _dispatch_get_default_queue(false); if (n) { os_atomic_store2o(dg, dg_bits, (uint32_t)-n * DISPATCH_GROUP_VALUE_INTERVAL, relaxed); os_atomic_store2o(dg, do_ref_cnt, 1, relaxed); // <rdar://22318411> } return dg; }Copy the code
4.2.2 dispatch_group_enter
void
dispatch_group_enter(dispatch_group_t dg)
{
// The value is decremented on a 32bits wide atomic so that the carry
// 🌹 for the 0 -> -1 transition is not propagated to the upper 32bits.
uint32_t old_bits = os_atomic_sub_orig2o(dg, dg_bits,
DISPATCH_GROUP_VALUE_INTERVAL, acquire);
uint32_t old_value = old_bits & DISPATCH_GROUP_VALUE_MASK;
if (unlikely(old_value == 0)) {
_dispatch_retain(dg); // <rdar://problem/22318411>
}
if (unlikely(old_value == DISPATCH_GROUP_VALUE_MAX)) {
DISPATCH_CLIENT_CRASH(old_bits,
"Too many nested calls to dispatch_group_enter()");
}
}
Copy the code
Os_atomic_sub_orig2o Specifies the decrement of dg->dg.bits. The value is 0->-1.
Holdings dispatch_group_leave
void dispatch_group_leave(dispatch_group_t dg) { // The value is incremented on a 64bits wide atomic so that the carry For // 🌹 the -1 -> 0 transition increments the generation atomically. Uint64_t new_state, Old_state = OS_atomic_add_ORIG2O (dg, dg_state,// atomic increment ++ DISPATCH_GROUP_VALUE_INTERVAL, release); uint32_t old_value = (uint32_t)(old_state & DISPATCH_GROUP_VALUE_MASK); //🌹 Wakes up if (Unlikely (old_value == DISPATCH_GROUP_VALUE_1)) {old_state += DISPATCH_GROUP_VALUE_INTERVAL; do { new_state = old_state; if ((old_state & DISPATCH_GROUP_VALUE_MASK) == 0) { new_state &= ~DISPATCH_GROUP_HAS_WAITERS; new_state &= ~DISPATCH_GROUP_HAS_NOTIFS; } else { // If the group was entered again since the atomic_add above, // we can't clear the waiters bit anymore as we don't know for // which generation the waiters are for new_state &= ~DISPATCH_GROUP_HAS_NOTIFS; } if (old_state == new_state) break; } while (unlikely(! os_atomic_cmpxchgv2o(dg, dg_state, old_state, new_state, &old_state, relaxed))); return _dispatch_group_wake(dg, old_state, true); } // leave (1 -> 0, 0 -> 1) If (Unlikely (old_value == 0)) {DISPATCH_CLIENT_CRASH((uintptr_t)old_value, "Unbalanced call to dispatch_group_leave()"); }}Copy the code
Os_atomic_add_orig2o specifies whether old_state is “0” and oldvalue is “0”. Old_value == DISPATCH_GROUP_VALUE_1) is not valid, and the dispatch_group_wake method is invoked.
In other words, if the dispatch_group_leave method is not called, dispatch_group_notify will not be awakened and the following process will not execute. “Dispatch_group_notify” is definitely not called.
4.2.4 dispatch_group_notify
DISPATCH_ALWAYS_INLINE static inline void _dispatch_group_notify(dispatch_group_t dg, dispatch_queue_t dq, dispatch_continuation_t dsn) { uint64_t old_state, new_state; dispatch_continuation_t prev; dsn->dc_data = dq; _dispatch_retain(dq); Run os_atomic_store2O to obtain the underlying status identifier of the dg at 🌹. State prev = OS_MPSC_PUSH_UPDATE_TAIL (OS_MPSC (dg, dg_notify), DSN, do_NEXT); if (os_mpsc_push_was_empty(prev)) _dispatch_retain(dg); os_mpsc_push_update_prev(os_mpsc(dg, dg_notify), prev, dsn, do_next); if (os_mpsc_push_was_empty(prev)) { os_atomic_rmw_loop2o(dg, dg_state, old_state, new_state, release, { new_state = old_state | DISPATCH_GROUP_HAS_NOTIFS; If ((uint32_t)old_state == 0) { Os_atomic_rmw_loop_give_up ({return _dispatch_group_wake(dg, new_state, false); / / wake up}); }}); }}Copy the code
If old_state is equal to 0, the execution of the related synchronous or asynchronous functions, i.e., the execution within the block, is awakened.
-
In the dispatch_group_leave analysis above, we already have the old_state result equal to 0.
-
So this also explains why dispatch_group_enter and dispatch_group_leave combination use of reason, through this kind of control, by cutting, avoid the influence of the asynchronous, to be able to wake up in time and call dispatch_group_notify method
-
We notice that the _dispatch_group_wake method is also called within dispatch_group_leave. This is because of asynchronous execution and task execution is time-consuming. Maybe the dispatch_group_leave code has not gone yet. The dispatch_group_notify method is used, but the dispatch_group_notify task is not executed, it is just added to the group.
-
It waits for the dispatch_group_leave execution to wake up. In this way, tasks in dispatch_group_notify are not discarded and can be executed properly.
4.2.5 dispatch_group_async
Dispatch_group_enter = dispatch_group_leave = dispatch_group_async
void dispatch_group_async(dispatch_group_t dg, dispatch_queue_t dq, dispatch_block_t db) { dispatch_continuation_t dc = _dispatch_continuation_alloc(); uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_GROUP_ASYNC; dispatch_qos_t qos; //🌹 task wrapper qos = _dispatch_continuation_init(dc, dq, db, 0, dc_flags); //🌹 processes tasks _dispatch_continuation_group_async(dg, Dq, dc, qos); }Copy the code
_dispatch_continuation_group_async encapsulates group operations
static inline void _dispatch_continuation_group_async(dispatch_group_t dg, dispatch_queue_t dq, dispatch_continuation_t dc, dispatch_qos_t qos) { dispatch_group_enter(dg); //🌹 enter group dc->dc_data = dg; _dispatch_continuation_async(dq, dc, qos, dc->dc_flags); //🌹 async}Copy the code
Guess what goes in goes out.
- (void)groupDemo{ dispatch_group_t group = dispatch_group_create(); dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_group_enter(group); Dispatch_async (queue, ^{🌹 task 1 NSLog(@" task 1"); sleep(1); dispatch_group_leave(group); }); dispatch_group_async(group, queue, ^{; 🌹 Task 2 NSLog(@" Task 2"); }); Dispatch_group_notify (dispatch_get_main_queue(),^{NSLog(@" finally it's my turn "); }); }Copy the code
On task 2, a breakpoint is made, the stack is viewed, and _dispatch_client_callout is called
Global search for the _dispatch_client_callout call is made at _dispatch_continuation_WITH_group_invoke.
conclusion
enter-leave
As long as it’s in pairs, near or fardispatch_group_enter
At the bottom is through C++ function, the value of the group--
Operation (i.e. 0 -> -1)dispatch_group_leave
At the bottom is through C++ function, the value of the group++
Operation (that is, -1 -> 0)dispatch_group_notify
At the bottom level, it mainly judges groupsstate
Whether is equal to the0
, is notified when equal to 0- The wake up of the block task can be passed
dispatch_group_leave
, can also passdispatch_group_notify
dispatch_group_async
Is equivalent toenter - leave
, and the underlying implementation isenter-leave
Five: Event source dispatch_source
The Dispatch Source is a wrapper around the BSD kernel’s customary kqueue, a technique that performs processing on the application programmer side when an event occurs 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.
dispatch_source_create
: create the sourcedispatch_source_set_event_handler
: Sets the source callbackdispatch_source_merge_data
: Source event setting datadispatch_source_get_data
: Gets the source event datadispatch_resume
: Resume resumedispatch_suspend
: hang
In our daily development, we often use timer NSTimer, such as the countdown to send SMS, or the update of the progress bar. However, NSTimer needs to be added to NSRunloop and is affected by mode. It is interrupted when it is affected by other event sources. When sliding scrollView, mode will be switched and the timer will stop, resulting in inaccurate timer timing.
GCD provides a solution dispatch_source to come up with a similar requirement scenario.
- The time is more accurate.
CPU
Small load, less occupation of resources - You can use child threads to solve the UI problem of timer running on the main thread
- You can pause, you can continue, you don’t have to
NSTimer
The same needs to be created again
Example – Timer
- (void)use033{// Countdown time __block int timeout = 3; Dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0); // create timer dispatch_source_t timer = dispatch_source_create(dispatch_source_timer, 0, 0, globalQueue); -source dispatch source-start Specifies the time when the timer is triggered for the first time. The parameter type is dispatch_time_t, which is an opaque type and we can't manipulate it directly. We need the dispatch_time and dispatch_walltime functions to create them. In addition, the constants DISPATCH_TIME_NOW and DISPATCH_TIME_FOREVER are often useful. - interval Interval - leeway Specifies the accuracy of the dispatch_source_set_timer(timer,dispatch_walltime(NULL, 0),1.0*NSEC_PER_SEC, 0); // Dispatch_source_set_event_handler (timer, ^{ Disable if (timeout <= 0) {// Cancel dispatch source dispatch_source_cancel(timer); }else{ timeout--; Dispatch_async (dispatch_get_main_queue(), ^{dispatch_get_main_queue(), ^{NSLog(@" countdown - %d", timeout); }); }}); // Start dispatch_resume(timer); }Copy the code
-
The timer NSTimer needs to be added to the NSRunloop, resulting in inaccurate count, which can be solved by using the Dispatch Source
-
When using a Dispatch Source, pay attention to the balance between recovery and suspension
-
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).
-
Because the dispatch_source_set_event_handle callback is a block, it 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 a circular reference. Therefore, the correct method is to cancel the timer by using weak+strong or calling dispatch_source_cancel in advance.
Reference:
IOS bottom exploration of multithreading (12) – GCD source analysis (dispatch_source) iOS GCD bottom principle analysis