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’re accessing parts of this content for free, with some sections shown as obfuscated text.

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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]