The first two articles have analyzed the use of GCD queues and functions, the creation of serial queues and concurrent queues, the underlying execution flow of synchronous functions and asynchronous functions, the deadlock of serial queues, the implementation flow of GCD singleton, and so on. This article continues to analyze GCD related content, such as fence functions, semaphores, scheduling groups, etc., from the perspective of usage and underlying principles.

1. Fence function

The most direct function of the fence function is to control the task execution order, to achieve synchronization effect.

The system provides two functions:

  • dispatch_barrier_async

    Submits a barrier block for asynchronous execution and returns immediately.

  • dispatch_barrier_sync

    Submits a barrier block for synchronous execution on a dispatch queue.

The difference between dispatch_barrier_sync and dispatch_barrier_async is that dispatch_barrier_sync and dispatch_barrier_Async block the current thread. It is also important to note that the fence function can only control the same concurrent queue.

1. Use of fence functions

  • Introduce a case

    Customize a concurrent queue and add three asynchronous functions with the following code:

    - (void)demo{ dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT); */ dispatch_async(concurrentQueue, ^{NSLog(@"1")); }); */ dispatch_async(concurrentQueue, ^{sleep(0.5); NSLog(@"2"); }); / / / / / / dispatch_barrier_async barrier function (concurrentQueue, ^ {/ / NSLog (@ "-- % @ -- -- -- -- --", [NSThread currentThread]); / /}); */ dispatch_async(concurrentQueue, ^{NSLog(@"3")); }); // 4 NSLog(@"4"); }Copy the code

    The result is clear, because the queue is a concurrent queue and an asynchronous function, so task 1, task 2, task 3, and task 4 are executed in a chaotic order. See the following results:

  • Add the fence function dispatch_barrier_async

    Now that you have a business requirement to ensure that tasks 1 and 2 are executed before task 3 can be executed, you can add a barrier function, as shown in the following code:

    Add a fence function dispatch_barrier_Async and run it to find that tasks 1 and 2 in the concurrent queue must run before the fence function, and task 3 will only run after the fence function has run. Because task 4 is in the main queue, it does not affect the normal execution of task 4.

  • Add the fence function dispatch_barrier_sync

    In the same case as above, how about changing the fence function to dispatch_barrier_sync?

    The running result shows that the business requirements are still met, that is, task 1 and task 2 must run before the fence function, and task 3 will run only after the fence function runs. Dispatch_barrier_sync also has the added feature of blocking the current thread, so task 4 will not be executed until after the barrier function has executed.

  • Matters needing attention

    • The fence function and other tasks must be in the same queue
    • Global concurrent queues cannot be used

2. The underlying principle of the fence function

For the fence function, we know that can play the role of synchronization, while the global concurrent queue can not be used, with these two points, we analyze the source code, is not it?

Let’s use the example of synchronizing a custom concurrent queue for tracing.

According to dispatch_barrier_sync in the libdispatch.dylib source globally search, fence function implementation. You will eventually find the _dispatch_barrier_sync_F_inline method. The tracing process is not described here. The _dispatch_barrier_sync_f_inline method is shown in the following figure:

Where does it go next? Add the _dispatch_sync_f_slow symbol breakpoint and enter the method successfully, as shown in the following figure:

This method is already familiar, having been analyzed for synchronous function execution and deadlocks, and has been called with the DC_FLAG_BARRIER tag set. The method of _dispatch_sync_f_slow is shown in the following figure:

Continue tracing the process, add a symbol breakpoint of _dispatch_sync_INVOke_and_complete_RECURse, and make it here. See below:

Through the above run stack, it is found that the process is: _dispatch_sync_f_slow -> _dispatch_sync_invoke_and_complete_recurse -> _dispatch_sync_recurse, Finally locate the _dispatch_sync_complete_recurse method, as shown in the following figure:

If you think about it, the function of the fence function is to synchronize, which means that the previous task in the queue is not completed, the fence function will definitely not go. So before you call the fence function, you must recursively complete the tasks in the queue. With such thinking we look at the source code, is not such logic.

In the _dispatch_sync_complete_recurse method, recursive processing is performed. If there is a barrier, dx_wakeup is called to wakeup all tasks in the current queue. After the wake is completed, _dispatch_lane_non_barrier_complete is executed, that is, the current queue task has been executed and there is no barrier function, and the following process is executed.

In order to proceed to the following process, the fence function must be removed first. Where is the fence function executed or removed? Track the dX_wakeup execution process. Dx_wakeup is a function defined by macros. Global search is performed and the defined position is found, as shown in the following figure:

This source code has been traced when synchronizing function flow analysis. The underlying layer provides different entry points for different types of queues. Also, we still have a problem. Why is the global concurrent queue not available? We analyze custom and global concurrent queues respectively.

  • Custom concurrent queues

    A custom concurrent queue will call the _dispatch_lane_wakeup method to locate the source code, as shown in the following figure:

    If so, the _dispatch_lane_barrier_complete method is called to handle the process with a barrier function. If not, go through the normal process of concurrent queuing and call the _dispatch_queue_wakeup method.

    _dispatch_lane_barrier_complete, view the processing process, see the following figure:

    If it is a serial queue, it will wait until the other tasks are completed and then execute in sequence. In the case of concurrent queues, _dispatch_lane_drain_non_Barriers is called to complete the task before the barrier. Finally, the _dispatch_lane_class_barrier_complete method is called to complete the clearance of the fence, thus executing the tasks behind the fence.

  • Global concurrent queue

    Dx_wakeup = dispatch_root_queue_wakeup = dispatch_root_queue_wakeup = dispatch_root_queue_wakeup

    In the global concurrent queue flow, there is no related processing flow of fence function, that is, it is processed according to the normal concurrent queue.

    Why is the global concurrent queue not handling the fence function? Because the global concurrent queue is not only used by us, but also by the system. If the barrier function is added, the queue will be blocked, which will affect the system-level operation. Therefore, the barrier function is not applicable to the global concurrent queue.

2. The semaphore

A Semaphore in GCD is a Dispatch Semaphore, which is a signal that holds a count. Dispatch Semaphore provides three functions.

  • dispatch_semaphore_create: Create aSemaphoreAnd initializes the total number of signals
  • dispatch_semaphore_wait: can reduce the total semaphore1, when the total signal amount is0Otherwise, it can execute normally
  • dispatch_semaphore_signal: Sends a signal and allows the total number of signals to increase1To unlock

For the official description of dispatch_semaphore_CREATE, see the following figure:

We can conclude that a semaphore greater than zero indicates the maximum number of concurrent GCD can be controlled.

1. Use of semaphore

  • Case 1

    Introduce the following example, as shown in the code below:

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_semaphore_t sem = dispatch_semaphore_create(1); // Task 1 dispatch_async(queue, ^{dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); / / wait for sleep. (2); NSLog(@" execute task 1"); NSLog(@" Task 1 completed "); dispatch_semaphore_signal(sem); // send a signal}); // Task 2 dispatch_async(queue, ^{dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // Wait, sleep(2); NSLog(@" execute task 2"); NSLog(@" Task 2 completed "); dispatch_semaphore_signal(sem); // send a signal});Copy the code

    This is how we usually use it. In a global concurrent queue, the task is executed asynchronously. The initial value of the current Semaphore is 1, which means that the maximum concurrency of the current queue is 1. “Dispatch_semaphore_wait” means that a signal is blocked, or occupied, and “dispatch_semaphore_signal” means that the occupied signal is released.

  • Case 2

    To tweak the above example, we set the semaphore initial value to 0, which means the maximum concurrency is set to 0. Asynchronously execute two tasks concurrently with a delay of 2 seconds. See the following code:

    The normal understanding would be to perform task 1 first and then task 2, but in reality the opposite is true. Here dispatch_semaphore_wait has the function of locking and dispatch_semaphore_signal has the function of unlocking. When task 1 is executed, dispatch_semaphore_wait locks the dispatch_semaphore_signal lock and waits. When task 2 is executed, dispatch_semaphore_signal unlocks the dispatch_semaphore_signal and sends a signal. Other tasks can be executed to control the flow.

  • Case 3

    The initial semaphore value is set to 0, which means the maximum concurrency is set to 0. Dispatch_semaphore_wait: in the main thread, the asynchronous process stops for 2 seconds. Normally, a print operation should be performed first, and the number output should be equal to 0, but in the actual case, the number is equal to 1. See the code below:

    The reason is the same as in case 2: the current thread is blocked by locking dispatch_semaphore_WAIT. After dispatch_semaphore_signal is unlocked, the current thread continues to execute. The number output is 1.

2. Semaphore principle analysis

How to lock and unlock dispatch_SEMaphore_wait and Dispatch_semaphore_Signal? With that in mind, let’s explore the source code.

  • Dispatch_semaphore_wait principle

    See the following figure for the implementation source code:

    Os_atomic_dec2o subtraction, that is, subtraction the value passed in the creation. To control the number of concurrency. For example, if the number of concurrent tasks is 3, the number becomes 2 after the method is called, indicating that one concurrent task is occupied and two tasks can be executed at the same time. However, if the initial value is 0 and the subtraction is negative, the _dispatch_semaphore_wait_slow method is invoked. _dispatch_semaphore_wait_slow method source implementation see the following figure:

    Which branch should I take? In the example above, when we call dispatch_semaphore_wait, the flag that is passed in is DISPATCH_TIME_FOREVER, which means to wait forever. Enter the implementation process of _dispatch_SEMa4_WAIT, as shown in the following figure:

    The _dispatch_sema4_WAIT does the do-while loop. When the condition is not met, the loop will continue, resulting in the blocking of the process. This explains the results of cases 2 and 3 above.

  • Dispatch_semaphore_signal principle

    The implementation source code is as follows:

    Os_atomic_inc2o is the add operation. It releases the available concurrent data by releasing the execute permission obtained by dispatch_semaphore_WAIT. When the initial value of the semaphore is 0, the add operation is called and the value is greater than 0, thus obtaining execution permission. However, if it is still less than 0 after being added once, an exception will be reported: start with both calls to dispatch_semaphore_signal(). And call the _dispatch_semaphore_signal_slow method, what does that method do? See below:

    Here _dispatch_sema4_signal also opens a do-while loop until the conditions are met to run.

  • To summarize the main role of Dispatch Semaphore in actual development:

    • Keep threads synchronized to convert asynchronously executed tasks to synchronously executed tasks
    • Ensure thread safety by locking the thread

3. The scheduling group

The dispatch_group function controls the execution sequence of tasks. The following methods are provided:

  • dispatch_group_createCreate a group
  • dispatch_group_asyncGroup tasks and execute them
  • dispatch_group_notifyNotification of completion of a group task
  • dispatch_group_waitWaiting time for group task execution
  • dispatch_group_enterInto the group
  • dispatch_group_leaverent

“Dispatch_group_enter” and “dispatch_group_leave” must be used together.

1. Scheduling group usage

  • Call group case

    Introduce a case where there is a business requirement that task 1, task 2, and task 3 be completed before task 4 can be performed. You can use the following methods to use a scheduling group:

    Add each queue to the group, and then call task 4 when the tasks in the group are complete, using dispatch_group_wait. The dispatch_group_wait() function waits until the contents of the preceding group have been executed before executing the following contents, but this can cause thread blocking problems. This causes task 5 in the main thread to not run properly until the task group’s task is complete.

  • The use of dispatch_group_notify

    To solve the above problems, use “dispatch_group_notify” to notify the completion of a task, as shown in the following figure:

    In this way, task 5 is not blocked. When tasks in the task group are completed, task 4 is notified to execute.

  • The use of in-group and out-group

    Using dispatch_group_Enter and dispatch_group_leave together can also achieve the above effect, as shown in the following figure:

    Note that to use this method, an Enter must correspond to a leave, in pairs! Task 4 is executed only after all tasks are executed and out of the group, and the execution of task 5 is not blocked.

2. Analysis of scheduling group principles

Here’s a question to consider: why do dispatch_group_Enter and dispatch_group_leave groups have the same effect as dispatching group dispatch_group_async?

  • dispatch_group_create

    Let’s first look at the scheduling group creation process. The implementation of the dispatch_group_create method is shown in the following figure:

    The _dispatch_group_create_with_count method is called and 0 is passed by default. The implementation of _dispatch_group_create_with_count is shown in the following figure:

    Os_atomic_store2o is used for saving.

  • dispatch_group_enter

    View dispatch_group_Enter implementation source code, see the following figure:

    Os_atomic_sub_orig2o performs — subtracting with old_bits equal to -1.

  • dispatch_group_leave

    View dispatch_group_leave implementation source, see the following figure:

    Os_atomic_add_orig2o ++ ++ old_state = 0. And 0&DISPATCH_GROUP_VALUE_MASK is still equal to 0, which means old_value is equal to 0. In the meantime, DISPATCH_GROUP_VALUE_1 is defined in the following code:

        #define DISPATCH_GROUP_VALUE_MASK       0x00000000fffffffcULL
        #define DISPATCH_GROUP_VALUE_1          DISPATCH_GROUP_VALUE_MASK
    Copy the code

    Obviously old_value is not equal to DISPATCH_GROUP_VALUE_MASK, so the process will go into the outer if and call the _dispatch_group_wake method to wake up. Who will be woken up? What is called is “dispatch_group_notify”. In other words, if the “dispatch_group_leave” method is not called, then the “dispatch_group_notify” method will not be called and the following process will not be executed.

  • dispatch_group_notify

    Check the source code of dispatch_group_notify. It is found that when old_state is equal to 0, the related synchronous asynchronous function execution process will be woken up. See below:

    In the dispatch_group_LEAVE analysis, we already have the olD_state result equal to 0.

    This explains why dispatch_group_Enter and dispatch_group_leave should be used together. The semaphore control can avoid the influence of asynchronism and can wake up and call the dispatch_group_notify method in time.

  • Dispatch_group_async encapsulation

    Another question, why is “dispatch_group_async” equal to “dispatch_group_Enter” and “dispatch_group_leave”? Let’s explore the dispatch_group_async encapsulation.

    In the same way, search for the definition of dispatch_group_async in the libdispatch.dylib source code.

    The _dispatch_continuation_group_async method is called and its implementation is checked:

    In this method, you can see that when the dispatch_group_async method is called to add a task to the group, the dispatch_group_Enter method is called, changing the semaphore 0 to -1.

    If you need to reset the semaphore, you must call the dispatch_group_leave method after the task has completed. To continue tracing the code, call the _dispatch_continuation_async method, which is implemented in the source code below:

    Familiar! Back to the flow of asynchronous functions! Specific asynchronous function analysis process see GCD function and queue principle exploration here no longer trace analysis.

    The asynchronous function eventually calls the _dispatch_worker_thread2 method, which is also analyzed in the GCD function and the queue principle exploration, by looking at the stack to see how it works. See below:

    The tracing process calls the _dispatch_continuation_POP_inline -> _dispatch_continuation_invoke_inline method. The value of dispatch_continuation_invoke_inline can be implemented as follows:

    The group case is handled in this method by calling the _dispatch_continuation_with_group_invoke method. See below:

    After the _dispatch_client_callout function call is made here, the dispatch_group_leave method is called, changing the semaphore from -1 to 0.

So far, the closed loop is completed, and the underlying principles and relationships of scheduling group, incoming group, outgoing group and notification are completely analyzed.