iOS GCD(Grand Central Dispatch)

There are three commonly used thread management methods in iOS, namely NSThread, GCD and NSOperation. Now let’s take a look at GCD among them.

Serial and parallel are for task queues, while synchronization and asynchronous are for threads.

Serial Queue + Sync sequence execution + synchronization

Serial Queue + Async sequence execution + asynchronous (in sequence)

Concurrent Queue + Sync Concurrency + Synchronization (in order)

Concurrent Queue + Async concurrency + asynchronous (true multi-threading) (not in order)

Custom serial queues have the ability to start the main thread and background threads (only one background thread can be started), and deadlocks will not occur. Synchronous tasks are automatically scheduled to be executed on the main thread; when asynchronous tasks are encountered, they are automatically scheduled to be executed on the background thread, so there will be no deadlock.

DispatchWorkItem can add items to the queue

//1. Only trailing closure
let item1 = DispatchWorkItem {<!-- -->
    print("item1")
}

//2. Specify qos (execution priority) or flags (special behavior mark)
let item2 = DispatchWorkItem(qos: .userInteractive, flags: .barrier) {<!-- -->
    print("item2")
}

DispatchQueue

  • Main queue (serial queue) Can only run on the main thread

    let mainQueue = DispatchQueue.main
    
  • Global queue (parallel queue Concurrent)

    let globalQueue = DispatchQueue.global()
    
  • Custom queue (default serial)

    //Serial queue, label name can be chosen casually
    let serialQueue = DispatchQueue(label: "test")
    
    //parallel queue
    let concurrentQueue = DispatchQueue(label: "test", attributes: .concurrent)
    

Add task

asynchronous

let mainQueue = DispatchQueue.main
mainQueue.async(execute: item1)

let globalQueue = DispatchQueue.global()
globalQueue.async(execute: item1)

let serialQueue = DispatchQueue(label: "serial")
serialQueue.async(execute: item1)

let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
concurrentQueue.async(execute: item1)

Synchronize

let mainQueue = DispatchQueue.main
mainQueue.sync(execute: item1) // Will definitely cause deadlock

let globalQueue = DispatchQueue.global()
globalQueue.sync(execute: item1)

let serialQueue = DispatchQueue(label: "serial")
serialQueue.sync(execute: item1)

let concurrentQueue = DispatchQueue(label: "concurrent", attributes: .concurrent)
concurrentQueue.sync(execute: item1)

Queue deadlock, not thread deadlock. The root cause of deadlock caused by adding synchronization tasks to the main queue:

  • The main queue can only run on the main thread.
  • The main queue has no ability to open background threads to do other things.
  • Once the main queue is mixed with synchronous tasks, it will wait for each other with existing asynchronous tasks, leading to deadlock.

DispatchGroup

Multiple tasks can be put into groups for easy management.

When all tasks in the group are executed, the GCD API sends corresponding notifications.

  • notify(): does not block the current thread
  • wait(): blocks the current thread

Custom Serial Queue An asynchronous or synchronous task (A)nestedin another synchronous task (B) can cause deadlock.

The equivalent tasks of A and B are: A1 -> B -> A2.

//Current task
let queue = DispatchQueue.init(label: "name")
queue.sync {<!-- -->
// Deadlock. Synchronization can only be executed here after the outer layer is executed, and the execution of the outer layer needs to be executed here first.
        queue.sync {<!-- -->
            print(Thread.current) // Synchronous task
        }
    // current task
    print(Thread.current)
}

There will be no deadlock when adding synchronous tasks to a parallel queue, because synchronous tasks are scheduled to be executed on the main thread, and asynchronous tasks are scheduled to be executed on the background thread.

All synchronization tasks will eventually be scheduled to run on the main thread. Running long and time-consuming tasks on the main thread will cause severe lag in the interface.

//Both of these methods will cause the interface to freeze (15s)
override func viewDidAppear(_ animated: Bool) {<!-- -->
    //1. Global queue performs synchronization tasks
    DispatchQueue.global().sync {<!-- -->
        sleep(15)//The current thread sleeps for 15 seconds
    }
    //2. The main queue executes asynchronous tasks
    DispatchQueue.main.async {<!-- -->
        sleep(15)//The current thread sleeps for 15 seconds
    }
}

GCD correct approach: Define A and B as asynchronous tasks, nest the asynchronous tasks in the parallel queue, and finally switch to the main queue to refresh the UI

let queue = DispatchQueue(label: "com.apple.request", attributes: .concurrent)

//Asynchronous execution
queue.async {<!-- -->
    
    print("Start requesting data \(Date()) thread: \(Thread.current)")
    sleep(10)//Simulate network request
    print("Data request completed \(Date()) thread: \(Thread.current)")
    
    //Asynchronous execution
    queue.async {<!-- -->
        print("Start processing data \(Date()) thread: \(Thread.current)")
        sleep(5)//Simulate data processing
        print("Data processing completed \(Date()) thread: \(Thread.current)")
        
        //Switch to the main queue and refresh the UI
        DispatchQueue.main.async {<!-- -->
            print("UI refreshed successfully \(Date()) thread: \(Thread.current)")
        }
    }
}

DispatchQueue.main automatically generates the main queue object, which can be obtained

DispatchQueue.global

DispatchQueue() defaults to a sequence queue

DispatchQueue(.concurrent) concurrent queue

Synchronization tasks will be assigned to the main thread.

Global, custom serial queues, and concurrent queues all have the ability to allocate asynchronous tasks to child threads. serial can only start one child thread (which is enough for concurrent tasks).

In the same queue, the synchronization task will wait for the previous task to complete before executing. (First come, first served)

Number is the queue identifier, name is the thread identifier. Use serial to arrange synchronous tasks and asynchronous tasks. The synchronous ones will be assigned to the main thread, and the asynchronous ones will be assigned to an anonymous thread in the background.

let queue = DispatchQueue.init(label: "hei")
queue.async {<!-- -->
    print(Thread.current)
}

queue.sync {<!-- -->
    queue.async {<!-- -->
        print(Thread.current)
    } //Although it is earlier than the print below, the process of sending this asynchronous task to another thread takes time.
    
    print(Thread.current)
}

queue.async {<!-- -->
    print(Thread.current)
}

queue.sync {<!-- -->
    print(Thread.current)
}

// result
// <NSThread: 0x7fde7c806950>{number = 5, name = (null)}
// <_NSMainThread: 0x7fde7cb06570>{number = 1, name = main}
// <NSThread: 0x7fde7c806950>{number = 5, name = (null)}
// <NSThread: 0x7fde7c806950>{number = 5, name = (null)}
// <_NSMainThread: 0x7fde7cb06570>{number = 1, name = main}

Synchronization tasks will deadlock if nested in serial queues.

//Current task
let queue = DispatchQueue.init(label: "name")
queue.sync {<!-- -->
// Deadlock. Synchronization cannot be executed here until the outer layer is executed, and the execution of the outer layer needs to be executed here first.
        queue.sync {<!-- -->
            print(Thread.current) // Synchronous task
        }
    // current task
    print(Thread.current)
}