In the previous part of our exploration of GCD functions and queue principles, we left some problems, such as:
- How do deadlocks occur?
- Where are threads created for asynchronous functions?
- The underlying through
_dispatch_worker_thread2
Method completes the callback execution of the task, so where does the call originate? GCD
What is the logic of the singleton in?
In this article, these contents are completed.
-
Introduce a question, what’s the difference between synchronous and asynchronous functions?
- Whether to create a thread
- Asynchrony and synchronization of function calls
- The emergence of a deadlock situation
Here’s a detailed analysis
1. Synchronization function
In the previous article, when analyzing the synchronization functions, the _dispatch_sync_F_inline process was tracked, as shown below:
Using the symbolic breakpoint, we can determine that if the queue is serial, it will go to the _dispatch_barrier_sync_f flow, which is also consistent with our analysis, because here dq_width=1, so it is serial. If it’s a concurrent queue, it goes to _dispatch_sync_f_slow.
Specific synchronization function analysis process see GCD function and queue principle exploration. Without further elaboration here, we will analyze the deadlock situation of the synchronous function serial queue at the midpoint.
-
A deadlock
Enter the _dispatch_barrier_sync_f process to analyze the synchronous serial execution process. See below:
This method calls _dispatch_barrier_sync_F_inline. This method has a judgment to determine whether the queue is in a wait or suspended state, as shown in the following figure:
See the following figure for the judgment method:
In this process, the status of the queue will be judged and the underlying execution process will be abandoned, that is, the queue will not schedule other tasks and return to control processing. If the current queue is suspended or blocked, the _dispatch_sync_f_slow method is executed (the same as the synchronous function concurrent queue).
So what line of code in the _dispatch_sync_f_slow method is the true deadlock feedback?
Before we analyze, let’s create a deadlock code and see where the error occurs. See the code below:
// for the main thread dispatch_sync(dispatch_get_main_queue(), ^{NSLog(@"1"); });Copy the code
The above case results in a deadlock. In the current main thread, there is a synchronization function that adds tasks to the main queue. Run the program, as shown below:
The execution structure is consistent with our analysis. The deadlock finally calls the _dispatch_sync_f_slow method, and the real deadlock is __DISPATCH_WAIT_FOR_QUEUE__(&dsc, dq); , see the picture below:
Enter the __DISPATCH_WAIT_FOR_QUEUE__ method and check its implementation. See the following figure:
The app first gets the status of the queue to be used, and then uses the _dq_state_drain_locked_by method to compare it to the current queue, which is considered a deadlock. Access the _dq_state_drain_locked_by method to see its logic:
DLOCK_OWNER_MASK is a large number, indicating that 0 is returned only when lock_value ^ tid = 0. In other words, the queue used at this time is the same as the queue currently waiting.
#define DLOCK_OWNER_MASK ((dispatch_lock)0xfffffffc) Copy the code
At this point, the logic of deadlock is clear, the current queue is in the waiting state, and there are new tasks to use this queue to schedule, which produces a contradiction, into the mutual waiting state, and then deadlock.
2. Asynchronous functions
In the last article, we used DX_push to locate that the underlying layer provides different entry points for calls to different types of queues, such as global concurrent queues that call the _dispatch_root_queue_push method. Trace the source code through the lower symbol breakpoint, eventually locating an important method _dispatch_root_queue_poke_slow. (specific asynchronous function analysis process see GCD function and queue principle exploration here is no longer described) source code as follows:
The _dispatch_root_queues_init method was also analyzed earlier, using singletons. In this method, the singleton method is used to initialize the thread pool, configure the work queue, initialize the work queue and so on. One of the key Settings is the execution function, which sets the task execution function to _dispatch_worker_thread2. See below:
The execution of the call here is invoked through the workloop, that is, the call is not called in time, but through the OS to complete the call, indicating that the key of asynchronous call is to be able to obtain the corresponding method when the need to execute, asynchronous processing, while synchronous function is called directly.
So where does it call? Continue parsing the _dispatch_root_queue_poke_slow method. If it is a global queue, a thread will be created to execute the task, as shown in the following figure:
Process the thread pool, obtain the thread from the thread pool, execute the task, and determine the change of the thread pool. See below:
Remaining indicates the number of available threads. When the number of available threads equals to 0, the pThread pool is full and returns directly. The bottom layer uses pthread to open up the thread, as shown in the following figure:
That is, _dispatch_worker_thread2 is triggered by the OC_ATMOIC atom via pThread.
-
How many threads can be created?
Reading the previous source code, we find that the queue thread pool size is dgq_thread_pool_size. Dgq_thread_pool_size is assigned to thread_pool_size, as shown in the following figure:
The initial value of thread_pool_size is DISPATCH_WORKQ_MAX_PTHREAD_COUNT. Global search, defined as follows:
255 indicates the theoretical maximum number of thread pools. But how much can actually be opened up? That’s not certain. In apple’s official full Thread Management, it is stated that the minimum allowable stack size for helper threads is 16 KB, and the stack size must be multiple of 4 KB. See below:
In other words, the stack space of a helper thread is 512KB, while the minimum space of a thread is 16KB. In other words, the more memory required to create a thread, the smaller the number of threads can be created with a given stack space. For an iOS machine with 4GB of memory, the memory is divided into kernel mode and user mode. If all the kernel mode is used to create threads, that is 1GB of space, which means that up to 1024KB / 16KB threads can be created. Of course, this is only a theoretical value.
3. The GCD singleton
-
Use the singleton
It can be executed only once.
static dispatch_once_t token; dispatch_once(&token, ^{ // code }); Copy the code
-
Definition of singletons
To see how this works, we first need to find the definition of a GCD singleton. See below:
Search globally for _dispatch_once in the libdispatch.dylib source code.
Here for different cases made some special processing, such as fence function, here only analyze dispatch_once, enter the implementation of dispatch_once, see the following figure:
Dispatch_once_f is finally called. See the source code implementation below:
-
Implementation logic
Think: What if we want to control a piece of code to be executed only once? First, create an identity. If the identity has been specially marked, it has already been executed. If it is not specifically marked, it can be executed. At the same time, in order to ensure the safety of the site, the key process needs to be locked.
Let’s examine the source code implementation logic.
Val is the onceToken created outside of it. The token is static and is created differently everywhere. See the following code:
dispatch_once_gate_t l = (dispatch_once_gate_t)val; Copy the code
The underlying atomicality of L is then associated to a variable of uintptr_t v, which is fetched from the underlying variable by os_atomic_LOAD and associated to the variable V. If v is equal to DLOCK_ONCE_DONE, that is, it has already been processed once. See the following code:
// Get the underlying atomicity association #if! DISPATCH_ONCE_INLINE_FASTPATH || DISPATCH_ONCE_USE_QUIESCENT_COUNTER uintptr_t v = os_atomic_load(&l->dgo_once, acquire); If (v == DLOCK_ONCE_DONE) {// v DLOCK_ONCE_DONE has already done once, direct return return; }Copy the code
If it hasn’t been done before, atomic processing compares its state, unlocks it, and eventually returns a bool, and in multithreaded cases, only one of them can get the lock returns yes. See the following code:
To ensure multi-threaded security, a lock is placed through _dispatch_lock_value_for_self to ensure multi-threaded security. If yes is returned, the _dispatch_once_callout method is executed to execute the singleton task and broadcast to the outside world, as shown in the following figure:
What does the broadcast do? See below:
If the token is not done, set it to done. The lock in the _dispatch_once_gate_tryenter method is also handled.
When the token is marked done, it will be returned directly at the entrance, as shown in the following figure:
-
Waiting for the
If there is multithreaded processing and no lock is acquired, it will call _dispatch_onCE_WAIT and wait. Here, spin lock is enabled and atomic processing is performed inside. In the loop process, if onCE_done has been set by another thread, it will give up processing. See below: