Speaking of human words, or directly take π° to see a problem, I would like to call the following method in the main thread execution order?
Additional question 1: Do deadlocks occur during execution, and why?
Additional question 2: Does it crash on execution and why?
func deadLock4(a) {
print("1")
let que = DispatchQueue.init(label: "thread")
que.async {
print("2")
DispatchQueue.main.sync {
print("3")
que.sync {
print("4")}}print("5")}print("6")
que.async {
print("Seven")}print("8")}Copy the code
The correct answer is that outputs 1, 2, and (68) are not in fixed order. 3, the que queue waits for a loop to block causing a deadlock, but does not crash.
If you can say exactly what the order of execution is and understand why deadlocks don’t break, you can leave the rest of the content behind. If you still have doubts and ambiguities, that is the purpose of this article. Look ahead to ππ to understand the order of execution and then solve the deadlock problem.
The GCD core
Automatically according to the use of multi-core CPU, create thread pool abstraction into queue to execute tasks, improve the running efficiency of the program.
Queue type: Determine how tasks are taken out and placed
- Serial queue: Tasks can only be taken out one by one and executed sequentially on a single thread.
- Parallel queue: Multiple tasks can be executed concurrently on different threads.
Task execution: decide on which thread to execute the task, and whether to block the current thread
Sync: Does not have the ability to start a new thread, executes in the current context thread, and blocks.
Async: the ability to start new threads, serial queue multiple asynchrony will only start one thread, the main queue already has the main thread, so asynchrony will not be enabled, parallel queue concurrent start thread execution, a maximum of 64, do not block.
::main queue
- Is a serial queue, and there is already a corresponding unique main thread, so the main queue async adding task will not start a new thread.
- Both sync and Async commit tasks are executed on the main thread.
Execution order problem
- The first is to explicitly block or not block the current thread. Blocking can be understood as the current thread is not able to submit subsequent tasks and execute them.
- Second, it is important to know which thread the tasks are executed on. If the tasks are executed separately on different threads, the order may not be the same.
// Output: 1 2 5 4 6 3 8 (must)
// All execute in main thread
func test1(a) {
print("1")
DispatchQueue.global().sync { //task1
print("2")
print("5")
DispatchQueue.main.async { //task2
Thread.sleep(forTimeInterval: 0.2)
print("3")}}DispatchQueue.main.async { //task3
print("8")}// The rest of this should be considered as a whole
print("4")
Thread.sleep(forTimeInterval: 0.2)
print("6")}Copy the code
- Because sync blocks the main thread from submitting task1, task1 is dependent on tasks 1, 4, and 6 for complete execution.
- After 2 and 5 are executed, the async commits task2 asynchronously and does not block task1’s execution thread main, so task1 completes, so 125 is directly 46, even after sleep main, because tasks are in the queue first, the serial queue can only be pulled out one by one.
- Again, task2 commits earlier than Task3, so task2 is executed first and then Task3.
Eat π°
// The sequence of 1 and 2 is not fixed. The sequence of 3 and 4 is not fixed until 2 is executed
func test2(a) {
DispatchQueue.global().async {
print("1")}DispatchQueue.global().sync {
print("2")}DispatchQueue.global().async {
print("3")}print("4")}// The order of 1, 2, and 3 is not fixed. 3 must come before 4
func test3(a) {
DispatchQueue.global().sync {
print("1")}DispatchQueue.global().async {
print("2")}DispatchQueue.global().sync {
print("3")}print("4")}// the execution of 1 and 2 is not fixed, but 3 is definitely after 1 and 2
func test4(a) {
let que = DispatchQueue.init(label: "thread")
que.async {
print("1")}print("2")
que.sync {
print("3")}}// 1, 2, and 5 are not fixed, 34 is not printed
// Is this strictly a block, but the external task never finishes executing
func deadLock(a) { //task
print("1")
DispatchQueue.global().async {
print("2")
DispatchQueue.main.sync { // Main thread execution
print("3")}print("4")}print("5")
while true{}}Copy the code
Deadlock and crash issues
Occurrence of deadlocks: The interdependent waiting blocks of tasks in serial queues cause deadlocks.
Causes of collapse: The previous serial queue was occupied by the tidπ of the thread on which taskA was executing. After that, taskB was submitted to sync before taskA finished executing and released the π of the queue. If the taskB thread tid was the same as the tidπ of the original queue, it would die π. It will be detected and crash at the same time.
Sync source code:
// Simplified pseudocode
dispatch_barrier_sync_f() {
// Get the tid of the execution thread
dispatch_tid tid = _dispatch_tid_self();
if (unlikely(! _dispatch_queue_try_acquire_barrier_sync(dq, tid))) {// Wait and block the current thread
return _dispatch_sync_f_slow();
}
}
_dispatch_sync_f_slow() {
_dispatch_sync_wait();
}
_dispatch_sync_wait() {
// Deadlocks will be checked later
dq_state = _dispatch_sync_wait_prepare(dq);
_dispatch_lock_is_locked_by(dq_state, tid)
}
// xOR operation: if dq_state is equal to the tid of the current executing thread, it crashes
_dispatch_lock_is_locked_by(lock_value, tid) {
return ((lock_value ^ tid) & DLOCK_OWNER_MASK) == 0;
}
Copy the code
In human language: if _dispatch_queue_try_acquire_barrier_sync returns false, it will wait and block in _dispatch_sync_F_slow, or wait for the last task in the serial queue to complete. Crash if dq_state is equal to the tid of the current executing thread.
Where do I get dQ_state? Look at the following π π
_dispatch_queue_try_acquire_barrier_sync(dispatch_queue_t dq, uint32_t tid)
{
uint64_t init = DISPATCH_QUEUE_STATE_INIT_VALUE(dq->dq_width);
uint64_t value = DISPATCH_QUEUE_WIDTH_FULL_BIT | DISPATCH_QUEUE_IN_BARRIER |
_dispatch_lock_value_from_tid(tid);
uint64_t old_state, new_state;
return os_atomic_rmw_loop2o(dq, dq_state, old_state, new_state, acquire, {
uint64_t role = old_state & DISPATCH_QUEUE_ROLE_MASK;
if(old_state ! = (init | role)) {os_atomic_rmw_loop_give_up(break);
}
new_state = value | role;
});
}
Copy the code
In human terms, sync submits the task to the serial queue for dQ_state:
- If dq_state is not changed to its original value, it is set to tid and returns true, indicating that the current queue has been locked by the thread (tid).
- If dq_state has been modified, return false.
In-depth take a simple π° :
// The most common type of loop waits for a blocking deadlock and crashes directly
func deadLock1(a) { //task tid: indicates the main thread
DispatchQueue.main.sync { //task1 tid: indicates the main thread
print("1")}}// The main loop waits for a blocking deadlock and crashes
//β οΈ : async does not deadlock crash
func deadLock2(a) { / / the task the main thread
let que = DispatchQueue.init(label: "thread")
que.sync { //task1 subthread //β οΈ : if async is used
DispatchQueue.main.sync { / / task2 main thread
print("1")}}}Copy the code
The following functions are called in the main thread, which is also a task. Deadlock crashes occur as follows:
- Dq_state stores the TID of the main thread in the main queue before calling the function.
- DeadLock1: dq_state has been changed for task execution, so _dispatch_queue_try_acquire_barrier_sync returns false, Wait in _dispatch_synC_F_slow and block the current thread (main thread).
- The _dispatch_lock_IS_locked_by deadlock check is also triggered, because task1 is executed in the main thread via sync, so the tid returned by _dispatch_tid_self is the same as the tid in dq_state, deadlocked and crashed.
- In deadLock2, task1 is executed in the child thread, but blocks the main thread (the current thread), so the task still uses the main thread tid to lock the main queue.
- When task1 is executed at the same time, the que will be locked by task1’s child thread tid because it is set for the first time, so _dispatch_queue_try_acquire_barrier_sync returns true, Means sync Task1 does not go to deadlock check in QUE.
β οΈ
- The fundamental difference between deadLock1 and deadLock2 is that deadLock2 uses another que to block the task from the main queue via sync (dq_state is the main queue tid).
- For example, in deadLock2, if task1 is submitted to a QUE with async, task1 does not block the execution of the task, and when the task is finished, the main queue, tid lock, is released. So the sync commit of task2 in deadLock2 resets dq_state, and _dispatch_queue_trY_acquire_barrier_sync returns true, so no deadlock check is triggered and no deadlock is triggered.
- The dq_state is reset. In particular, the sync commit of the main queue is executed on the main thread, so its Dq_state is still the main thread TID (if it is a normal serial QUE, it depends on the current thread).
The key to determining a deadlock crash (a deadlock crash π―) is whether the previous serial queue is held by a taskA thread whose tid is π. If it is held by π, then taskB submitted to its sync will be deadlocked. If the tid of taskB is the same as the original, π will die and crash.
Shallow give a complex π°
// Output: 5, 6, 7, and 8 are not in fixed order, 1 must follow 8, 2 must follow 7, 3, 4
// β οΈ : with sync, output: 5, 6, 7, and 8 are not in fixed order, 1 must come after 8, 2 must come after 7, and the main loop waits for a blocking deadlock to crash.
func deadLock3(a) { //task
let que = DispatchQueue.init(label: "thread")
DispatchQueue.main.async { //task1
print("1")
que.async { //task2 //β οΈ : if sync is used
print("2")
DispatchQueue.main.sync { //task3
print("3")}print("4")}}print("5")
Thread.sleep(forTimeInterval: 1.0)
print("6")
que.async { //task4
print("Seven")}print("8")}Copy the code
- Task1 and Task4 commits async without blocking task execution.
- When task3 submits to the main Queue sync, the task must have completed, and the order indicates that task3 must have completed after 1581 (π€).
- Task3’s sync commit is used to repopulate the main queue with tidπ. This tid is the main thread tid, so there is no deadlock check and no loop waiting for the task to execute.
- Compare and understand β οΈ with sync.
In A popular way, it can be regarded as A serial queue in which taskA is not finished and taskB is dropped to sync in the queue. Since it is A serial queue, only one task can be taken out for execution. Therefore, the condition for taskB to finish execution is taskA (A comes in first), and taskB to finish execution is taskA and removed from the queue. So there is a loop of waiting tasks, resulting in deadlocks.
Further down the deadlock check π°
// Output: 1, 2, and (68) order is not fixed, 3, que queue loop waiting for blocking causes deadlock, does not crash.
// β οΈ : Consider using sync instead
func deadLock4(a) {
print("1")
let que = DispatchQueue.init(label: "thread")
que.async { //task1
print("2")
DispatchQueue.main.sync { // Use sync to make que still tid lock
print("3")
que.sync { //task2
print("4")}}print("5")}print("6")
que.async { //β οΈ : if this is changed to sync
print("Seven")}print("8")}Copy the code
4578 is not printed here, so it is easier to understand (the comparison is shallow π°) : Because task1 in que is blocked by an intermediate task submitted by main Sync, task2 is submitted to que before it is finished, expecting it to block the current thread and execute immediately, but task1 is not finished, so task2 cannot execute. Task2 is also part of task1 execution, so the loop waits blocked.
But it doesn’t crash here π€·πΏβ π€·πΏβ π€·πΏβ for the following reasons:
- Task1 locks the QUE with the child tid thread, and the sync commit on the main queue blocks task1’s release of the que, which is still being locked with the child tid thread.
- In task2, sync is performed on the main thread (sync is performed on the current thread), so the main thread tid is used to compare the sync submission to the locked que. It is not the same TID, so it will not be detected dead π and will not crash.
So a GCD deadlock is basically a task loop waiting block, and a crash is basically a deadlock check during sync commit, which should be avoided in real development because it doesn’t crash but it does hold thread resources.
β οΈ : Think about what happens if sync is used here?