Dispatch Queue Tutorial

Photo by Alex Alvarez on Unsplash
Photo by Alex Alvarez on Unsplash
Grand Central Dispatch (GCD) provides efficient concurrent processing so that we don’t need to directly manage multiple threads. Its dispatch queues can execute tasks serially or concurrently.

Grand Central Dispatch (GCD) provides efficient concurrent processing so that we don’t need to directly manage multiple threads. Its dispatch queues can execute tasks serially or concurrently. We only need to submit code as tasks to dispatch queues. We no longer need to worry about creating too many threads!

What is Dispatch Queue?

Dispatch queue is one of the tools of Grand Central Dispatch (GCD). It allows you to execute a piece of code asynchronously or synchronously. This is the so-called concurrency processing.

Generally speaking, when we write a parallel program, it is nothing more than creating a new thread to execute parallel code. This is called multi-threading. However, it is not that creating more threads to process many tasks in parallel can speed up application performance. When there are too many threads, CPUs will spend too much time busy switching between threads. And too few threads can’t make full use of CPUs. The question is how many threads are appropriate? As mentioned in Concurrency and Application Design, the appropriate number of threads is determined based on CPU cores. For application developers, it is very difficult to calculate the optimal number of threads according to CPUs and operating systems and adjust the number of threads in real time. In addition, the code for managing threads is similar, and your code may not have been optimized, but GCD code has been optimized to manage threads efficiently.

GCD is a framework used to help us manage threads. GCD provides three tools, namely Dispatch QueuesDispatch Sources, and Operation Queues. We only discuss dispatch queues in this article. For GCD, the code that we want to process in parallel is called a task. We submit tasks to a dispatch queue, and GCD will take out tasks one by one from the dispatch queue, and select an idle thread from the thread pool to execute the task, as shown in the figure below. The order to execute tasks is first-in, first out (FIFO).

Dispatch Queues
Dispatch Queues

Serial Dispatch Queues

Serial queue perform tasks one by one. Only when the current task is finished, it continue to execute the next task, as shown in the figure below.

Serial Dispatch Queues
Serial Dispatch Queues

Because serial queues only execute one task at the same time, there will be no resource race conditions between tasks. So, when you want to avoid race conditions between tasks, you can use serial queues.

Let’s take a look at how to use serial queues. In the following code, we create an instance of DispatchQueue, and the parameter label refers to the name of this dispatch queue. Then, call DispatchQueue.async() to submit a task.

let serialQueue = DispatchQueue(label: "com.waynestalk.serial")
print("start")
serialQueue.async {
    print("Task 1")
}
serialQueue.async {
    print("Task 2")
}
print("end")

The above code will output the following:

start
end
Task 1
Task 2

After DispatchQueue.async() submit a task to the serial queue, it will return immediately, and the task will be scheduled for execution later. This is to perform tasks in an asynchronous manner.

DispatchQueue also provides a synchronous way to perform tasks, such as the code below.

let serialQueue = DispatchQueue(label: "com.waynestalk.serial")
print("start")
serialQueue.sync {
    print("Task 1")
}
serialQueue.sync {
    print("Task 2")
}
print("end")

The code above will output the following:

start
Task 1
Task 2
end

We can see that DispatchQueue.sync()would block the current thread, waiting for serial queue executing the task.

You may notice the above code using DispatchQueue.sync()simply not able to be processed in parallel, because it has to wait for the task to complete before continuing execution. Therefore, most of time we use DispatchQueue.async(). When you have to use DispatchQueue.sync(), perhaps you can think about if there is a way to use DispatchQueue.async()to achieve your goal.

Concurrent Dispatch Queues

Concurrent queue allow you to perform several tasks at the same time. The order in which it performs tasks is also first in, first out. But after it starts to execute a task, it does not need to wait for the task to complete, and immediately execute the next task. How many tasks can it perform simultaneously? This number is defined in system, we cannot change it from applications, and in fact, you don’t need to care about it.

Concurrent Dispatch Queues
Concurrent Dispatch Queues

Similarly, we use DispatchQueue to create a concurrent queue, but pass .concurrent to the parameter attributes.

let concurrentQueue = DispatchQueue(label: "com.waynestalk.concurrent", attributes: .concurrent)
print("start")
concurrentQueue.async {
    sleep(2)
    print("Task 1")
}
concurrentQueue.async {
    sleep(1)
    print("Task 2")
}
print("end")

The above code will output the following:

start
end
Task 2
Task 1

You can see that the concurrent queue processes Task 1 and Task 2 in parallel.

Concurrent queues also has .sync() method, it also first block the current thread, wait for the task to complete before returning.

Quality-of-Service (QoS)

An application can create many dispatch queues, so we may want a dispatch queue to have a higher priority than other dispatch queues. DispatchQueue certainly provides such a function, which is called Quality-ofService (QoS) .

The following code shows how to configure QoS for a dispatch queue. It is just setting value on DispatchQueue‘s qos parameter.

let concurrentQueue = DispatchQueue(label: "com.waynestalk.concurrent", qos: .userInitiated, attributes: .concurrent)
let serialQueue = DispatchQueue(label: "com.waynestalk.serial", qos: .background)

DispatchQoS.QoSClass defines 6 priorities, as follows (the priority order is from high to low):

  • .userInteractive
  • .userInitiated
  • .default
  • .utility
  • .background
  • .unspecified

Global Dispatch Queues

The system creates global concurrent queues for each application by default. Therefore, you should first consider using these global concurrent queues, which can be obtained as follows:

let defaultConcurrentQueue = DispatchQueue.global()
let backgroundConcurrentQueue = DispatchQueue.global(qos: .background)

The system only creates global concurrent queues. So, if you want to use serial queues, you still have to create it yourself.

Main Dispatch Queue

Main dispatch queue is a global serial dispatch queue, and it will perform tasks on main thread. Main thread is the only thread that can update screen. The following code shows how to execute tasks in main dispatch queue.

DispatchQueue.main.async {
    print("Task 1")
}

There are many opportunities to use main dispatch queue. The most typical one is to obtain network data in a background thread, and then display the data on screen in main thread, as follows:

// in main thread
...

DispatchQueue.global.async {
    // in a background thread
    // get data from internet
    ....

    DispatchQueue.main.async {
        // in main thread
        // update user interface
        ...
    }
}

Avoid creating too many Threads

Since GCD will help us manage threads, why should we avoid creating too many threads? This is because GCD will try to ensure that all dispatch queues can operate normally, so in some cases it will create too many threads to execute dispatch queues. There is a detailed discussion in Avoiding Excessive Thread Creation.

Blocking Current Thread

When a task is executed in a concurrent dispatch queue, and then it calls a method to block the current thread, the system will create another thread to perform other tasks in the concurrent dispatch queue. When there are too many tasks to block their threads, it will consume the resources of the application. So we should try to avoid calling methods that block thread in tasks.

Creating Too Many Concurrent Dispatch Queues

When we create too many concurrent dispatch queues, it will also cause the system to create too many threads to perform tasks. Of course serial dispatch queues will also have an impact, but the impact is relatively small. Because it executes one task at a time, only one thread is needed. So when we need to use concurrent dispatch queues, we should first consider using global dispatch queues before creating a new one.

If you really want to create your own concurrent dispatch queues, or you have to create serial dispatch queues, you can associate your newly created dispatch queue with global dispatch queues, as follows:

let globalQueue = DispatchQueue.global()
let queue = DispatchQueue(label: "com.waynestalk.serial", target: globalQueue)
queue.async {
    print("Task is being executed on a global queue")
}

You can refer to the detailed description of the parameter target DispatchQueue.setTarget().

Conclusion

DispatchQueue is very easy to use. When you need to execute code in parallel, you only need to call DispatchQueue.async() method. There is no need to create new threads, nor to manage numerous threads. GCD will handle these things very efficiently. We can focus more on logical development.

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like