Modern Concurrency: Getting Started

Oct 18 2022 Swift 5.5, iOS 15, Xcode 13.4

Part 1: Asynchronous Code

2. Getting Started

Episode complete

Play next episode

Next
Save for later
About this episode
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 1. Introduction Next episode: 3. Your First Asynchronous App

This video Getting Started was last updated on Oct 18 2022

Part 1: Tasks

In the course materials, locate the starter playground and open it.

Notice you import SwiftUI. This pulls in the private _Concurrency framework that defines Task. Import UIKit also works. The leading underscore of _Concurrency indicates the name could change in a future release, so importing SwiftUI or UIKit future-proofs your code better than directly importing _Concurrency.

Here’s some ordinary sequential code:

print("Doing some work on a task")
let sum = (1...100000).reduce(0, +)
print("1 + 2 + 3 ... 100000 = \(sum)")
print("Doing some work on the main actor")

MainActor is a type that runs code on the main thread. It’s the modern alternative to DispatchQueue.main. As they’re written now, all 4 lines of code run on the main actor — the main thread.

Run this code by clicking the run button next to the last line:

Doing some work on a task
1 + 2 + 3 ... 100000 = 5000050000
Doing some work on the main actor

This code runs sequentially, so the print messages appear in the same order as they’re written.

Now, wrap the first 3 lines in a Task:

🟩Task {🟥
  print("Doing some work on a task")
  let sum = (1...100000).reduce(0, +)
  print("1 + 2 + 3 ... 100000 = \(sum)")
🟩}🟥   // typing this re-indents the 3 lines
print("Doing some work on the main actor")

The Task type takes a trailing closure where you write the work you want to do. And run this code:

Doing some work on a task
Doing some work on the main actor
1 + 2 + 3 ... 100000 = 5000050000

Now the “main actor” message appears before the sum.

If your sum still appears before the main actor statement, just add more zeros so it takes longer to run.

The code in the Task closure runs sequentially, but the Task itself runs on a background thread. While it’s computing sum, the main thread runs the next line of code, so the “main actor” message appears before the Task prints sum.

Check if running on main actor

Comment out all this code, then scroll down to see some code that helps you check if code is running on the main actor:

let specificKey = DispatchSpecificKey<String>()
DispatchQueue.main.setSpecific(key: specificKey, value: "main")
if DispatchQueue.getSpecific(key: specificKey) == "main" {
  print("\nPlayground runs on main actor")
}

Task {
  print("\nDoing some work on a task")
  if DispatchQueue.getSpecific(key: specificKey) == "main" {
    print("Task runs on main actor")
  } else {
    print("Task doesn't run on main actor")
  }
}
print("Doing some work on the main actor")

This code sets a specific key value for the main actor, then checks for this value in the playground and in the Task. Run this code:

Playground runs on main actor

Doing some work on a task
Task doesn't run on main actor

And here’s your confirmation. So the Task runs on a background thread. What if you have more than one Task?

Scroll up and uncomment the earlier Task code , then duplicate the Task , give the second Task a name , then edit the print statements so you know which Task is printing . Also, add one more print statement after the first Task and make the last print statement look a little different .

Task {
  print("\nDoing some work on an 🟩unnamed🟥 task")
  let sum = (1...100000).reduce(0, +)
  print("🟩Unnamed task done: 🟥1 + 2 + 3 ... 100000 = \(sum)")
}
print("Doing some work on the main actor")
🟩print("Doing more work on the main actor")🟥

// This task runs after previous task finishes
🟩let task = Task {
  print("\nDoing some work on a named task")
  let sum = (1...100000).reduce(0, +)
  print("🟩Named🟥 task done: 1 + 2 + 3 ... 100000 = \(sum)")
}
print("Doing yet 🟩more🟥 work on the main actor")
🟥

Now, run this:

Doing some work on an unnamed task
Doing some work on the main actor
Unnamed task done: 1 + 2 + 3 ... 100000 = 5000050000
Doing more work on the main actor

Doing some work on a named task
Doing yet more work on the main actor
Named task done: 1 + 2 + 3 ... 100000 = 5000050000

Your main actor messages might appear in different places, but your named task doesn’t start until the unnamed task finishes.

The playground uses at most 2 threads, and one is reserved for the main actor, so you have only 1 thread for running tasks. In episode 4, you’ll use the name of the named task to cancel it.

So far, you’ve just moved a slow operation off the main actor. But you haven’t explicitly used the new keywords async and await. Coming right up!

Click through to the next page…

Part 2: async/await

Scroll down to the section titled Start here. You might’ve seen the sleep(_:) function before.

It works like this:

sleep(1)
print("wake up")

Run this. And after 1 second, the print message appears.

This sleep function is synchronous: It blocks its thread. In this case, it blocks the main thread, so the print message doesn’t appear until sleep(1) finishes. But there’s a non-blocking Task.sleep(_:) method.

Above the sleep code, add this code:

print("Hello")
Task.sleep(until: .now + .seconds(1), clock: .continuous)  
// I'm using Xcode 14, which has this new Task.sleep method, so I can use seconds instead of nanoseconds.
print("Goodbye")

Task.sleep(_:) is an asynchronous function and it can throw a CancellationError.

You can’t run an asynchronous function on the main thread. You must send it to a background thread.

Wrap all 3 lines in a Task:

 🟩Task {🟥
  print("\nHello")
  Task.sleep(until: .now + .seconds(1), clock: .continuous)
  print("Goodbye")
 🟩}🟥

It can throw errors, so you must use try:

 Task {
  print("\nHello")
  🟩try🟥 Task.sleep(until: .now + .seconds(1), clock: .continuous)
  print("Goodbye")
 }

And this error message means you must use the await keyword when you call an async function.

Go ahead and click Fix:

 Task {
  print("\nHello")
  try 🟩await🟥 Task.sleep(until: .now + .seconds(1), clock: .continuous)
  print("Goodbye")
 }

await gives the runtime a suspension point — a place to pause this task, consider if there are other tasks to run on this thread and then resume running this task.

You always use the keywords try and await in this order.

First, you await the result of the asynchronous operation,

and then you check the result for errors. Now, run this:

Hello appears, and 1 second later, Goodbye appears.

Did you notice something wonderful? Task.sleep(:) is an asynchronous function. In the old concurrency model, you would expect to write some completion code in an escaping closure where you would have to capture self or some other variable. You pretty much don’t have to do this when you use the new async/await syntax!

Asynchronous functions

When you define an asynchronous function that can throw errors, you reverse the order of the keywords.

Scroll up to the section titled Asynchronous Functions and create a function:

func helloPauseGoodbye() async throws {
}

Copy-paste the code in the Task.sleep closure into this function and edit the print messages:

func helloPauseGoodbye() async throws {
  🟩
  print("Hello function")  // add " function"
  try await Task.sleep(until: .now + .seconds(1), clock: .continuous)
  print("Goodbye function")  // add " function"
  🟥
}
  • And, below this async throwing function, create a Task to call it:
Task {
  try await helloPauseGoodbye()
}

First you await the completion of the async method, which could throw errors.

So then you check for thrown errors.

When you run this, it works the same as before.

Next, you’ll work with a more interesting asynchronous function.

URLSession

Scroll up to the section titled URLSession. You’ll download and decode a list of learning domains from raywenderlich.com.

Downloading is always an asynchronous operation. You’ve probably used URLSession.dataTask(with:completionHandler:) to do this.

Open the navigator panel and look at Part2/Sources/Domains.swift: These are the Decodable structures that mirror the nested JSON in the server response:

// all annotated public
struct Domains: Decodable {
  let data: [Domain]
}
struct Domain: Decodable {
  let attributes: Attributes
}
struct Attributes: Decodable {
  let name: String
  let description: String
  let level: String
}

Your goal is an array of Domain values.

Each domain has an Attributes property.

And these are the attributes you’ll print.

Select the Part 2 page and close the navigator panel. Add a function to fetch the domains:

func fetchDomains() -> [Domain] {
  let url = URL(string: "https://api.raywenderlich.com/api/domains")!
  // and add a dummy return so the compiler doesn't complain
  return []
}

Add async call: async throws / try await

Now, before the return statement, fetch the data:

func fetchDomains() -> [Domain] {
  let url = URL(string: "https://api.raywenderlich.com/api/domains")!
  🟩let (data, _) = try await URLSession.shared.data(from: url)🟥
  // data(from:) is grayed out because the compiler doesn't think it should be used here
  // select it anyway
  return []
}

Xcode flags an error: ‘async’ call in a function that does not support concurrency

because this URLSession data(from:) method is a built-in async method. To call it from fetchDomains(), you need to tell the compiler that fetchDomains() is also an async method.

Xcode suggests the correct fix — add ‘async’ to function, so click Fix:

func fetchDomains() 🟩async 🟥 -> [Domain] {

Xcode also complains Errors thrown from here are not handled because data(from:) has an advantage over the old dataTask completion handler: It can throw network errors.

So add throws to the function signature:

func fetchDomains() async 🟩throws 🟥 -> [Domain] {

As you saw with helloPauseGoodbye(), the keyword async always comes before the keyword throws.

And when you call a throwing async function, try comes before await.

Now, look more closely at this interesting line of code.

This URLSession async method returns a Data instance and a URLResponse object, just like the old URLSession dataTask method. You won’t use the response now, so it’s just this underscore.

Instead of passing a completion handler, calling data(from:) suspends execution in fetchDomains() until the data and response arrive from the server.

Handle data, response

Normally, you would first verify the server response, but skip that and just decode the data. Replace the dummy return [] line:

return try JSONDecoder().decode(Domains.self, from: data).data

This is pretty much what you would write in a dataTask completion handler, except you would have to handle any decoder errors right away. Now to see if this works.

Call async method from Task

fetchDomains() is an async function, so you need to call it in a Task:

Task {
  let domains = try await fetchDomains()
}

It can throw errors, so you need a do-catch:

Task {
  🟩
  do {
    let domains = try await fetchDomains()
  } catch {
    print(error)
  }
  🟥
}

fetchDomains returns an array, so loop over it to print out the domains:

Task {
  do {
    let domains = try await fetchDomains()
    🟩
    for domain in domains {
      let attr = domain.attributes
      print("\(attr.name): \(attr.description) - \(attr.level)")
    }
    🟥
  } catch {
    print(error)
  }
}

And now, run this task.

iOS & Swift: Learn iOS development with SwiftUI and UIKit - production
Android & Kotlin: Learn Android development in Kotlin - production
Flutter: Learn multi-platform development with Flutter - production
Server-Side Swift: Learn web development with Swift - beta
Game Technologies: Learn how to create games using popular game technologies. - beta
macOS: Learn macOS development - archived
Archive:  - archived

So that’s how Swift concurrency works in a playground. In the next episode, you’ll start using all this in an actual app.