Concurrent vs serial queues in GCD

A simple example: you have a block that takes a minute to execute. You add it to a queue from the main thread. Let's look at the four cases.

  • async - concurrent: the code runs on a background thread. Control returns immediately to the main thread (and UI). The block can't assume that it's the only block running on that queue
  • async - serial: the code runs on a background thread. Control returns immediately to the main thread. The block can assume that it's the only block running on that queue
  • sync - concurrent: the code runs on a background thread but the main thread waits for it to finish, blocking any updates to the UI. The block can't assume that it's the only block running on that queue (I could have added another block using async a few seconds previously)
  • sync - serial: the code runs on a background thread but the main thread waits for it to finish, blocking any updates to the UI. The block can assume that it's the only block running on that queue

Obviously you wouldn't use either of the last two for long running processes. You normally see it when you're trying to update the UI (always on the main thread) from something that may be running on another thread.


First, it's important to know the difference between threads and queues and what GCD really does. When we use dispatch queues (through GCD), we're really queueing, not threading. The Dispatch framework was designed specifically to get us away from threading, as Apple admits that "implementing a correct threading solution [can] become extremely difficult, if not [sometimes] impossible to achieve." Therefore, to perform tasks concurrently (tasks that we don't want freezing the UI), all we need to do is create a queue of those tasks and hand it to GCD. And GCD handles all of the associated threading. Therefore, all we're really doing is queueing.

The second thing to know right away is what a task is. A task is all of the code within that queue block (not within the queue, because we can add things to a queue all of the time, but within the closure where we added it to the queue). A task is sometimes referred to as a block and a block is sometimes referred to as a task (but they are more commonly known as tasks, particularly in the Swift community). And no matter how much or little code, all of the code within the curly braces are considered a single task:

serialQueue.async {
    // this is one task
    // it can be any number of lines with any number of methods
}
serialQueue.async {
    // this is another task added to the same queue
    // this queue now has two tasks
}

And it's obvious mentioning that concurrent simply means at the same time with other things and serial means one after the other (never at the same time). To serialize something, or to put something in serial, just means to execute it from start to finish in its order from left to right, top to bottom, uninterrupted.

There are two types of queues, serial and concurrent, but all queues are concurrent relative to each other. The fact that you want to run any code "in the background" means that you want to run it concurrently with another thread (usually the main thread). Therefore, all dispatch queues, serial or concurrent, execute their tasks concurrently relative to other queues. Any serialization performed by queues (by serial queues), have only to do with the tasks within that single [serial] dispatch queue (like in the example above where there are two tasks within the same serial queue; those tasks will be executed one after the other, never simultaneously).

SERIAL QUEUES (often known as private dispatch queues) guarantee the execution of tasks one at a time from start to finish in the order that they were added to that specific queue. This is the only guarantee of serialization anywhere in the discussion of dispatch queues--that the specific tasks within a specific serial queue are executed in serial. Serial queues can, however, run simultaneously with other serial queues if they are separate queues because, again, all queues are concurrent relative to each other. All tasks run on distinct threads but not every task is guaranteed to run on the same thread (not important, but interesting to know). And the iOS framework does not come with any ready-to-use serial queues, you must make them. Private (non-global) queues are serial by default, so to create a serial queue:

let serialQueue = DispatchQueue(label: "serial")

You can make it concurrent through its attribute property:

let concurrentQueue = DispatchQueue(label: "concurrent", attributes: [.concurrent])

But at this point, if you aren't adding any other attributes to the private queue, Apple recommends that you just use one of their ready-to-go global queues (which are all concurrent). At the bottom of this answer, you'll see another way to create serial queues (using the target property), which is how Apple recommends doing it (for more efficient resource management). But for now, labeling it is sufficient.

CONCURRENT QUEUES (often known as global dispatch queues) can execute tasks simultaneously; the tasks are, however, guaranteed to initiate in the order that they were added to that specific queue, but unlike serial queues, the queue does not wait for the first task to finish before starting the second task. Tasks (as with serial queues) run on distinct threads and (as with serial queues) not every task is guaranteed to run on the same thread (not important, but interesting to know). And the iOS framework comes with four ready-to-use concurrent queues. You can create a concurrent queue using the above example or by using one of Apple's global queues (which is usually recommended):

let concurrentQueue = DispatchQueue.global(qos: .default)

RETAIN-CYCLE RESISTANT: Dispatch queues are reference-counted objects but you do not need to retain and release global queues because they are global, and thus retain and release is ignored. You can access global queues directly without having to assign them to a property.

There are two ways to dispatch queues: synchronously and asynchronously.

SYNC DISPATCHING means that the thread where the queue was dispatched (the calling thread) pauses after dispatching the queue and waits for the task in that queue block to finish executing before resuming. To dispatch synchronously:

DispatchQueue.global(qos: .default).sync {
    // task goes in here
}

ASYNC DISPATCHING means that the calling thread continues to run after dispatching the queue and does not wait for the task in that queue block to finish executing. To dispatch asynchronously:

DispatchQueue.global(qos: .default).async {
    // task goes in here
}

Now one might think that in order to execute a task in serial, a serial queue should be used, and that's not exactly right. In order to execute multiple tasks in serial, a serial queue should be used, but all tasks (isolated by themselves) are executed in serial. Consider this example:

whichQueueShouldIUse.syncOrAsync {
    for i in 1...10 {
        print(i)
    }
    for i in 1...10 {
        print(i + 100)
    }
    for i in 1...10 {
        print(i + 1000)
    }
}

No matter how you configure (serial or concurrent) or dispatch (sync or async) this queue, this task will always be executed in serial. The third loop will never run before the second loop and the second loop will never run before the first loop. This is true in any queue using any dispatch. It's when you introduce multiple tasks and/or queues where serial and concurrency really come into play.

Consider these two queues, one serial and one concurrent:

let serialQueue = DispatchQueue(label: "serial")
let concurrentQueue = DispatchQueue.global(qos: .default)

Say we dispatch two concurrent queues in async:

concurrentQueue.async {
    for i in 1...5 {
        print(i)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
101
2
102
103
3
104
4
105
5

Their output is jumbled (as expected) but notice that each queue executed its own task in serial. This is the most basic example of concurrency--two tasks running at the same time in the background in the same queue. Now let's make the first one serial:

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

101
1
2
102
3
103
4
104
5
105

Isn't the first queue supposed to be executed in serial? It was (and so was the second). Whatever else happened in the background is not of any concern to the queue. We told the serial queue to execute in serial and it did... but we only gave it one task. Now let's give it two tasks:

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
serialQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
2
3
4
5
101
102
103
104
105

And this is the most basic (and only possible) example of serialization--two tasks running in serial (one after the other) in the background (to the main thread) in the same queue. But if we made them two separate serial queues (because in the above example they are the same queue), their output is jumbled again:

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
serialQueue2.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
101
2
102
3
103
4
104
5
105

And this is what I meant when I said all queues are concurrent relative to each other. These are two serial queues executing their tasks at the same time (because they are separate queues). A queue does not know or care about other queues. Now lets go back to two serial queues (of the same queue) and add a third queue, a concurrent one:

serialQueue.async {
    for i in 1...5 {
        print(i)
    }
}
serialQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 1000)
    }
}

1
2
3
4
5
101
102
103
104
105
1001
1002
1003
1004
1005

That's kind of unexpected, why did the concurrent queue wait for the serial queues to finish before it executed? That's not concurrency. Your playground may show a different output but mine showed this. And it showed this because my concurrent queue's priority wasn't high enough for GCD to execute its task sooner. So if I keep everything the same but change the global queue's QoS (its quality of service, which is simply the queue's priority level) let concurrentQueue = DispatchQueue.global(qos: .userInteractive), then the output is as expected:

1
1001
1002
1003
2
1004
1005
3
4
5
101
102
103
104
105

The two serial queues executed their tasks in serial (as expected) and the concurrent queue executed its task quicker because it was given a high priority level (a high QoS, or quality of service).

Two concurrent queues, like in our first print example, show a jumbled printout (as expected). To get them to print neatly in serial, we would have to make both of them the same serial queue (the same instance of that queue, as well, not just the same label). Then each task is executed in serial with respect to the other. Another way, however, to get them to print in serial is to keep them both concurrent but change their dispatch method:

concurrentQueue.sync {
    for i in 1...5 {
        print(i)
    }
}
concurrentQueue.async {
    for i in 1...5 {
        print(i + 100)
    }
}

1
2
3
4
5
101
102
103
104
105

Remember, sync dispatching only means that the calling thread waits until the task in the queue is completed before proceeding. The caveat here, obviously, is that the calling thread is frozen until the first task completes, which may or may not be how you want the UI to perform.

And it is for this reason that we cannot do the following:

DispatchQueue.main.sync { ... }

This is the only possible combination of queues and dispatching methods that we cannot perform—synchronous dispatching on the main queue. And that's because we are asking the main queue to freeze until we execute the task within the curly braces... which we dispatched to the main queue, which we just froze. This is called deadlock. To see it in action in a playground:

DispatchQueue.main.sync { // stop the main queue and wait for the following to finish
    print("hello world") // this will never execute on the main queue because we just stopped it
}
// deadlock

One last thing to mention is resources. When we give a queue a task, GCD finds an available queue from its internally-managed pool. As far as the writing of this answer, there are 64 queues available per qos. That may seem like a lot but they can quickly be consumed, especially by third-party libraries, particularly database frameworks. For this reason, Apple has recommendations about queue management (mentioned in the links below); one being:

Instead of creating private concurrent queues, submit tasks to one of the global concurrent dispatch queues. For serial tasks, set the target of your serial queue to one of the global concurrent queues. That way, you can maintain the serialized behavior of the queue while minimizing the number of separate queues creating threads.

To do this, instead of creating them like we did before (which you still can), Apple recommends creating serial queues like this:

let serialQueue = DispatchQueue(label: "serialQueue", qos: .default, attributes: [], autoreleaseFrequency: .inherit, target: .global(qos: .default))

And using an extension, we can get it down to this:

extension DispatchQueue {
    public class func serial(label: String, qos: DispatchQoS = .default) -> DispatchQueue {
        return DispatchQueue(label: label,
                             qos: qos,
                             attributes: [],
                             autoreleaseFrequency: .inherit,
                             target: .global(qos: qos.qosClass))
    }
}

let defaultSerialQueue = DispatchQueue.serial(label: "xyz")
let serialQueue = DispatchQueue.serial(label: "xyz", qos: .userInteractive)

// Which now looks like the global initializer
let concurrentQueue = DispatchQueue.global(qos: .default)

For further reading, I recommend the following:

https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008091-CH1-SW1

https://developer.apple.com/documentation/dispatch/dispatchqueue


Here are a couple of experiments that i have done to make me understand about these serial, concurrent queues with Grand Central Dispatch.

 func doLongAsyncTaskInSerialQueue() {

   let serialQueue = DispatchQueue(label: "com.queue.Serial")
      for i in 1...5 {
        serialQueue.async {

            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) completed downloading")
        }
    }
}

Task will run in different thread(other than main thread) when you use async in GCD. Async means execute next line do not wait until the block executes which results non blocking main thread & main queue. Since its serial queue, all are executed in the order they are added to serial queue.Tasks executed serially are always executed one at a time by the single thread associated with the Queue.

func doLongSyncTaskInSerialQueue() {
    let serialQueue = DispatchQueue(label: "com.queue.Serial")
    for i in 1...5 {
        serialQueue.sync {
            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) completed downloading")
        }
    }
}

Task may run in main thread when you use sync in GCD. Sync runs a block on a given queue and waits for it to complete which results in blocking main thread or main queue.Since the main queue needs to wait until the dispatched block completes, main thread will be available to process blocks from queues other than the main queue.Therefore there is a chance of the code executing on the background queue may actually be executing on the main thread Since its serial queue, all are executed in the order they are added(FIFO).

func doLongASyncTaskInConcurrentQueue() {
    let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
    for i in 1...5 {
        concurrentQueue.async {
            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) completed downloading")
        }
        print("\(i) executing")
    }
}

Task will run in background thread when you use async in GCD. Async means execute next line do not wait until the block executes which results non blocking main thread. Remember in concurrent queue, task are processed in the order they are added to queue but with different threads attached to the queue. Remember they are not supposed to finish the task as the order they are added to the queue.Order of task differs each time threads are created as necessarily automatically.Task are executed in parallel. With more than that(maxConcurrentOperationCount) is reached, some tasks will behave as a serial until a thread is free.

func doLongSyncTaskInConcurrentQueue() {
  let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
    for i in 1...5 {
        concurrentQueue.sync {
            if Thread.isMainThread{
                print("task running in main thread")
            }else{
                print("task running in background thread")
            }
            let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
            let _ = try! Data(contentsOf: imgURL)
            print("\(i) completed downloading")
        }
        print("\(i) executed")
    }
}

Task may run in main thread when you use sync in GCD. Sync runs a block on a given queue and waits for it to complete which results in blocking main thread or main queue.Since the main queue needs to wait until the dispatched block completes, main thread will be available to process blocks from queues other than the main queue.Therefore there is a chance of the code executing on the background queue may actually be executing on the main thread. Since its concurrent queue, tasks may not finish in the order they are added to queue. But with synchronous operation it does although they may be processed by different threads. So, it behaves as this is the serial queue.

Here is a summary of these experiments

Remember using GCD you are only adding task to the Queue and performing task from that queue. Queue dispatches your task either in main or background thread depending on whether operation is synchronous or asynchronous. Types of queues are Serial,Concurrent,Main dispatch queue.All the task you perform is done by default from Main dispatch queue.There are already four predefined global concurrent queues for your application to use and one main queue(DispatchQueue.main).You can also manually create your own queue and perform task from that queue.

UI Related task should always be performed from main thread by dispatching the task to Main queue.Short hand utility is DispatchQueue.main.sync/async whereas network related/heavy operations should always be done asynchronously no matters which ever thread you are using either main or background

EDIT: However, There are cases you need to perform network calls operations synchronously in a background thread without freezing UI(e.g.refreshing OAuth Token and wait if it succeed or not).You need to wrap that method inside a asynchronous operation.This way your heavy operations are executed in the order and without Blocking main thread.

func doMultipleSyncTaskWithinAsynchronousOperation() {
    let concurrentQueue = DispatchQueue(label: "com.queue.Concurrent", attributes: .concurrent)
    concurrentQueue.async {
        let concurrentQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.default)
        for i in 1...5 {
            concurrentQueue.sync {
                let imgURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/0/07/Huge_ball_at_Vilnius_center.jpg")!
                let _ = try! Data(contentsOf: imgURL)
                print("\(i) completed downloading")
            }
            print("\(i) executed")
        }
    }
}

EDIT EDIT: You can watch demo video here