iOS Concurrency with GCD & Operations

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

Part 1: Grand Central Dispatch

04. Use Dispatch Work Items

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: 03. Use Dispatch Queues Next episode: 05. Challenge: A Better Way to Download Images

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.

Transcript: 04. Use Dispatch Work Items

Use a Dispatch Work Item

Often, you use a dispatch queue to “fire and forget” background tasks. If you want a lot more control over organizing and managing tasks, you’d use operations and operation queues instead — you’ll learn about these in part 3 of this course. But you can get a little more control over dispatch queue tasks with DispatchWorkItems and DispatchWorkGroups. You’ll learn about dispatch work groups in a later video.

You can create and execute a DispatchWorkItem if you want to be notified when the task completes, or if you might need to cancel the task.

Open the playground in the starter folder. Here are the main queue, a queue with userInitiated quality of service, and two tasks:

let mainQueue = DispatchQueue.main
let userQueue = DispatchQueue.global(qos: .userInitiated)
func task1() {
  print("Task 1 started")
  sleep(4)
  print("Task 1 finished")
}
func task2() {
  print("Task 2 started")
  print("Task 2 finished")
}

In the previous exercise, this is how you ran task 2 in the background:

userQueue.async {
  task2()
}

Run this by clicking the run button over the line number. The output appears in the debug area, down here:

>> Just run task 2 in the background.
Task 2 started
Task 2 finished

Now create a DispatchWorkItem with task 1:

let workItem = DispatchWorkItem {
  task1()
}

And run it on userQueue:

userQueue.async(  // choose async(execute:) from menu

Instead of the async closure you’ve been using, choose this execute method.

userQueue.async(execute: workItem)

And pass your workItem as the execute argument. Click next to this last line here, to run the playground up to this line.

>> Run task 1 as a work item.
Task 1 started
Task 1 finished

It works exactly the same as running task1 by itself.

If the current thread really needs workItem to finish before it can continue, call .wait to tell the system to give it priority. If necessary or possible, the system will increase the priority of other tasks in the same queue. It does not do this when you wait for dispatch groups, which you’ll learn about in a later video.

if workItem.wait(timeout: .now() + 3) == .timedOut {
  print("I got tired of waiting.")
} else {
  print("Work item completed.")
}

Run the playground up to this line:

>> Waiting for task 1...
Task 1 started
I got tired of waiting.
Task 1 finished

The wait times out but task 1 continues to run and eventually finishes.

Work items have other advantages over simple tasks. First, you can construct a simple dependency, for example, to update the user interface after task 1 completes.

There’s some helper code here. You’ll use it later. Now, define two work items. Later, you’ll add code to see which queue each work item runs on.

let backgroundWorkItem = DispatchWorkItem {
  task1()
}
let updateUIWorkItem = DispatchWorkItem {
  task2()
}

This code pretends that task2 updates the user interface based on what task1 does. So first, execute backgroundWorkItem on the user queue:

userQueue.async(execute: backgroundWorkItem)

Next, specify that updateUIWorkItem should run after backgroundWorkItem completes:

backgroundWorkItem.notify(queue: mainQueue, 
                          execute: updateUIWorkItem)

The work items can run on different queues. In fact, if updateUIWorkItem updates the user interface, you must run it on the main queue.

  • To see proof of which queue each task runs on, scroll back to that helper code:
enum Queues { case main, user }
// TODO later: Set specific key for each queue.

And add these lines below the TODO:

let specificKey = DispatchSpecificKey<Queues>()
mainQueue.setSpecific(key: specificKey, value: .main)
userQueue.setSpecific(key: specificKey, value: .user)

You’ve created a DispatchSpecificKey with values from the Queues enum and set the value of this key for the main and user queues.

Uncomment this helper function. It refers to specificKey so it would have flagged an error before. And now call this function in your work items:

let backgroundWorkItem = DispatchWorkItem {
  task1()
  whichQueue(workItem: "backgroundWorkItem")
}
let updateUIWorkItem = DispatchWorkItem {
  task2()
  whichQueue(workItem: "updateUIWorkItem")
}

Scroll down to the .notify statement and run the playground up to this line:

>> Run task 2 work item after task 1 work item finishes.
Task 1 started
Task 1 finished
>> backgroundWorkItem is running on userQueue
Task 2 started
Task 2 finished
>> updateUIWorkItem is running on mainQueue

Task 2 runs after task 1, even though task 1 takes longer. And backgroundWorkItem ran on userQueue, and updateUIWorkItem on mainQueue.

Finally, here’s how you cancel a work item:

if !updateUIWorkItem.isCancelled {
  updateUIWorkItem.cancel()
}

You check whether the second work item is cancelled. If not, you cancel it. This is the last bit of code, so just run the playground: Task 2 doesn’t run at all.

>> Run task 2 work item after task 1 work item finishes.
Task 1 started
>> Cancel task 2 work item before task 1 work item finishes.
Task 1 finished
>> backgroundWorkItem is running on userQueue

Task 1 might finish before / after the cancel message prints.

And that’s the end of this video.