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 tovalPackaged inlThrough theos_atomic_loadPull it from the bottom, associate it with a variablevOn. ifvThis is equal toDLOCK_ONCE_DONEYou’ve already dealt with it oncereturnTo 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_asyncIt doesn’t block the main thread, soAt the endPrint first. But it blocks the task threadconcurrentQueue, so123and456Prior to the789Print, 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_slowand__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 whetherbarrierOf the form, will call_dispatch_lane_barrier_completeMethods to deal with
  • If there is nobarrierForm, then go normal concurrent queue process, call_dispatch_queue_wakeupMethods.

_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 SemaphoreIs something that holds the signal of counting. There are three methods.

  • dispatch_semaphore_create: Creates a Semaphore and initializes the total amount of signals
  • dispatch_semaphore_signal: the semaphore is sent, increasing the total number of signals by 1
  • dispatch_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 to1, 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 doneThe 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-leaveAs long as it’s in pairs, near or far
  • dispatch_group_enterAt the bottom is through C++ function, the value of the group--Operation (i.e. 0 -> -1)
  • dispatch_group_leaveAt the bottom is through C++ function, the value of the group++Operation (that is, -1 -> 0)
  • dispatch_group_notifyAt the bottom level, it mainly judges groupsstateWhether is equal to the0, is notified when equal to 0
  • The wake up of the block task can be passeddispatch_group_leave, can also passdispatch_group_notify
  • dispatch_group_asyncIs 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 source
  • dispatch_source_set_event_handler: Sets the source callback
  • dispatch_source_merge_data: Source event setting data
  • dispatch_source_get_data: Gets the source event data
  • dispatch_resume: Resume resume
  • dispatch_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.CPUSmall 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 toNSTimerThe 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