iOS Concurrency with GCD & Operations

Sep 12 2023 · Swift 5.8, macOS 13, iOS 16, Xcode 14.3

Part 1: Grand Central Dispatch

03. Use Dispatch Queues

Episode complete

Play next episode

Next
About this episode
Leave a rating/review
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 02. GCD Next episode: 04. Use Dispatch Work Items

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Notes: 03. Use Dispatch Queues

GCD’s Main Queue vs. Main Thread

Heads up... You've reached locked video content where the transcript will be shown as obfuscated text.

You’re going to take a closer look at serial vs concurrent queues, and synchronous vs asynchronous dispatch.

PlaygroundPage.current.finishExecution()

Create dispatch queues

Every task eventually ends up being executed on a global dispatch queue. You can dispatch a task directly to a global dispatch queue. In an app, you might dispatch to the userInitiated global queue. You reference this with the DispatchQueue class function global:

let userQueue = DispatchQueue.global(qos: .userInitiated)
let defaultQueue = DispatchQueue.global()
let mainQueue = DispatchQueue.main

Dispatch async to global queue

Here are some simple tasks: Task 1 has a 1-second sleep, to make it take longer to run than task 2.

func task1() {
  print("Task 1 started")
  // make task1 take longer than task2
  sleep(1)
  print("Task 1 finished")
}

func task2() {
  print("Task 2 started")
  print("Task 2 finished")
}
userQueue.async {
  task1()
}
userQueue.async {
  task2()
}
duration {
  ...
}
=== Starting userInitated global queue ===
Task 1 started
Task 2 started
Task 2 finished
Task 1 finished
7.402896881103516e-05

Create private serial queue

Concurrent execution is the default global queue behavior. What if you want to run tasks serially? One at a time. The only global serial queue is DispatchQueue.main, which you should only use for user interface activity. You can create a private serial queue, if you want tasks to run exactly in the order they arrive. This is useful for ensuring serial access to a resource, to avoid data races or deadlocks. You create a private queue with the DispatchQueue initializer. Serial is the default attribute for a private dispatch queue, so you only need to specify the queue’s label:

let mySerialQueue = DispatchQueue(label: "com.kodeco.serial")

Dispatch async to private serial queue

Now dispatch the tasks onto your private serial queue:

duration {
  mySerialQueue.async {
    task1()
  }
  mySerialQueue.async {
    task2()
  }
}
=== Starting mySerialQueue ===
Task 1 started
Task 1 finished
Task 2 started
Task 2 finished

Create private concurrent queue; dispatch async

Next, you’ll look at a private concurrent queue, something you could use to group the tasks triggered by a user action, keeping them separate from the global queues. In Part 2, you’ll need a private concurrent queue to create a dispatch barrier — it’s one solution for the readers and writers data race problem. To create a private concurrent queue, you specify the .concurrent attribute:

let workerQueue = DispatchQueue(label: "com.kodeco.worker", attributes: .concurrent)
duration {
  workerQueue.async {
    task1()
  }
  workerQueue.async {
    task2()
  }
}
sleep(2)
=== Starting workerQueue ===
Task 1 started
Task 2 started
Task 2 finished
Task 1 finished

Dispatch sync to private serial queue

So far, you’ve always dispatched a task asynchronously, whether the queue is concurrent or serial — the async method returns right away to the current thread, so it can immediately execute the next statement. The concurrent queues create multiple threads to do their work; the serial queue runs tasks on its single thread.

duration {
  userQueue.sync {  // just change this
    task1()
  }
  userQueue.async {
    task2()
  }
}
=== Starting userInitated global queue ===
Task 1 started
Task 1 finished
Task 2 started
Task 2 finished
1.002355098724365
userQueue.async { task1() }

Data races

sync is very useful for avoiding data races — if the queue is a serial queue, and it’s the only way to access an object, the sync method behaves as a mutual exclusion lock, guaranteeing that all threads get consistent values.

var value = 42

func changeValue() {
  sleep(1)
  value = 0
}
mySerialQueue.async {
  changeValue()
}
value
value [42 in sidebar]
value = 42
mySerialQueue.sync {
  changeValue()
}
value
dispatchPrecondition(condition: .notOnQueue(mainQueue))
dispatchPrecondition(condition: .onQueue(mainQueue))
value [0 in sidebar]