Chapters

Hide chapters

Swift Apprentice: Beyond the Basics

First Edition · iOS 16 · Swift 5.8 · Xcode 14.3

Section I: Beyond the Basics

Section 1: 13 chapters
Show chapters Hide chapters

12. Concurrency
Written by Ehab Amer

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

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

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

Unlock now

The code you’ve written in the previous chapters of this book is all synchronous, meaning that it executes statement-by-statement, one step at a time, on what’s known as the main thread. Synchronous code is the most straightforward code to write and reason about, but it comes with a cost. Operations that take time to complete, including reading from a network or database, can stop your program and wait for the operation to finish. For an interactive program such as a mobile app, this is a poor user experience because a great app needs to be fast and responsive.

By executing these operations asynchronously, your program can work on other tasks, such as updating the user interface while it waits for the blocking operation to complete. Working asynchronously introduces concurrency into your code. Your program will work on multiple tasks simultaneously.

Swift has always been capable of using concurrency libraries, such as Apple’s C-language-based Grand Central Dispatch. More recently, the core team has introduced a suite of language-level concurrency features, making it more efficient, safer and less error-prone than ever before.

This chapter gets you started in this new world of concurrency. You’ll learn essential concepts, including:

  • How to create unstructured and structured tasks.
  • How to perform cooperative task cancellation.
  • How to use the async / await pattern.
  • How to create and use actor and Sendable types.

Note: You may have heard of multithreaded programming. Concurrency in operating systems is built on top of threads, but you don’t need to manipulate them directly. In Swift-concurrency-speak, you use the term main actor instead of main thread. Actors are responsible for maintaining the consistency of objects you run concurrently in your program.

Basic Tasks

You’ll start with something super simple: Creating an unstructured task, which is an object that encapsulates some concurrent work. You can do that in an iOS Playground like this:

Task {
  print("Doing some work on a task")
}

print("Doing some work on the main actor")

The Task type takes a trailing closure with some work — print a message in this case — to do simultaneously with the main actor. Running this playground prints:

Doing some work on a task
Doing some work on the main actor

Changing the Order

In the example above, the code executed in the order the statements in the playground occurred. To see how that can change, replace the Task with some real work, like this:

Task {
  print("Doing some work on a task")
  let sum = (1...100).reduce(0, +)
  print("1 + 2 + 3 ... 100 = \(sum)")
}

print("Doing some work on the main actor")
Doing some work on a task
Doing some work on the main actor
1 + 2 + 3 ... 100 = 5050

Canceling a Task

Next, you’ll practice canceling a task. To do this, replace the code with the following:

let task = Task {
  print("Doing some work on a task")
  let sum = (1...100).reduce(0, +)
  try Task.checkCancellation()
  print("1 + 2 + 3 ... 100 = \(sum)")
}

print("Doing some work on the main actor")
task.cancel()
Doing some work on a task
Doing some work on the main actor

Suspending a Task

Suppose you want to print the message Hello, wait for a second and then print Goodbye. You’d add this to your playground:

print("Hello")
Task.sleep(for: .seconds(1))
print("Goodbye")

Task {
  print("Hello")
  try Task.sleep(for: .seconds(1))
  print("Goodbye")
}

Task {
  print("Hello")
  try await Task.sleep(for: .seconds(1))
  print("Goodbye")
}

Wrapping it in a Function

Suppose you want to put that functionality into, well, a function. You might start like this:

func helloPauseGoodbye() {
  print("Hello")
  try await Task.sleep(for: .seconds(1))
  print("Goodbye")
}

func helloPauseGoodbye() async throws {
  print("Hello")
  try await Task.sleep(for: .seconds(1))
  print("Goodbye")
}

Task {
  try await helloPauseGoodbye()
}

The Structure of Tasks

You might have heard that Swift implements structured concurrency. That’s because tasks organize themselves into a tree-like structure with parent and child tasks.

Doing some work on a task
Doing some work on the main actor
Hello
Hello
1 + 2 + 3 ... 100 = 5050
Goodbye
Goodbye

Decoding an API — Learning Domains

So far, you’ve just seen contrived printing examples. To get more practice, you’ll asynchronously download and decode all of the “learning domains” from Kodeco using the website’s API. This activity will involve:

func fetchDomains() async throws -> [Domain] {
  [] // Fill in the implementation later
}
{
  "data":[
    {
      "id":"1",
      "type":"domains",
      "attributes":{
        "name":"iOS \u0026 Swift",
        "slug":"ios",
        "description":"Learn iOS development with SwiftUI and UIKit",
        "level":"production",
        "ordinal":1
      }
    }
  ]
}
struct Domains: Decodable {
  let data: [Domain]
}

struct Domain: Decodable {
  let attributes: Attributes
}

struct Attributes: Decodable {
  let name: String
  let description: String
  let level: String
}

Async/Await in Action

Swift’s concurrency features make asynchronous code nearly as easy to read and write as synchronous code. Here’s how you implement fetchDomains:

func fetchDomains() async throws -> [Domain] {
  // 1
  let url = URL(string: "https://api.kodeco.com/api/domains")!
  // 2
  let (data, _) = try await URLSession.shared.data(from: url)
  // 3
  return try JSONDecoder().decode(Domains.self, from: data).data
}
Task {  // 1
  do {  // 2
    let domains = try await fetchDomains() // 3
    for domain in domains {                // 4
      let attr = domain.attributes
      print("\(attr.name): \(attr.description) - \(attr.level)")
    }
  } catch {
    print(error)
  }
}

Asynchronous Sequences

Another powerful abstraction that Swift concurrency gives you is the asynchronous sequence. Getting each element may cause the task to suspend:

func findTitle(url: URL) async throws -> String? {
  for try await line in url.lines {
    if line.contains("<title>") {
      return line.trimmingCharacters(in: .whitespaces)
    }
  }
  return nil
}
Task {
  if let title = try await findTitle(url: URL(string:
                                     "https://www.kodeco.com")!) {
    print(title)
  }
}
<title>Kodeco | Learn iOS, Android &amp; Flutter</title>

Ordering Your Concurrency

In the previous examples, you made a new unstructured Task block whenever you needed an asynchronous context that could suspend and resume. Suppose you want to get the titles of two web pages.

func findTitlesSerial(first: URL,
            second: URL
) async throws -> (String?, String?) {
  let title1 = try await findTitle(url: first)
  let title2 = try await findTitle(url: second)
  return (title1, title2)
}
func findTitlesParallel(first: URL,
                        second: URL
) async throws -> (String?, String?) {
  async let title1 = findTitle(url: first)   // 1
  async let title2 = findTitle(url: second)  // 2
  let titles = try await [title1, title2]    // 3
  return (titles[0], titles[1])              // 4
}

Asynchronous Properties and Subscripts

Just as you saw with throws in Chapter 5, “Error Handling”, you can mark read-only computed properties with async:

extension Domains {
  static var domains: [Domain] {
    get async throws {
      try await fetchDomains()
    }
  }
}
Task {
  dump(try await Domains.domains)
}
extension Domains {
  enum Error: Swift.Error { case outOfRange }

  static subscript(_ index: Int) -> String {
    get async throws {
      let domains = try await Self.domains
      guard domains.indices.contains(index) else {
        throw Error.outOfRange
      }
      return domains[index].attributes.name
    }
  }
}

Task {
  dump(try await Domains[4])  // "Game Tech"
}

Introducing Actors

So far, you’ve seen how to introduce concurrency into your code. However, concurrency isn’t without its risks. In particular, concurrent code can access and mutate the same state simultaneously, causing unpredictable results.

// 1
class Playlist {
  let title: String
  let author: String
  private(set) var songs: [String]

  init(title: String, author: String, songs: [String]) {
    self.title = title
    self.author = author
    self.songs = songs
  }

  func add(song: String) {
    songs.append(song)
  }

  func remove(song: String) {
    guard !songs.isEmpty, let index = songs.firstIndex(of: song) else {
      return
    }
    songs.remove(at: index)
  }

  func move(song: String, from playlist: Playlist) {
    playlist.remove(song: song)
    add(song: song)
  }

  func move(song: String, to playlist: Playlist) {
    playlist.add(song: song)
    remove(song: song)
  }
}

Converting a Class to an Actor

Here’s how you convert your Playlist from class to actor:

// 1
actor Playlist {
  let title: String
  let author: String
  private(set) var songs: [String]

  init(title: String, author: String, songs: [String]) {
    self.title = title
    self.author = author
    self.songs = songs
  }

  func add(song: String) {
    songs.append(song)
  }

  func remove(song: String) {
    guard !songs.isEmpty, let index = songs.firstIndex(of: song) else {
      return
    }
    songs.remove(at: index)
  }

  // 3
  func move(song: String, from playlist: Playlist) async {
    // 2
    await playlist.remove(song: song)
    add(song: song)
  }

  func move(song: String, to playlist: Playlist) async {
    await playlist.add(song: song)
    remove(song: song)
  }
}

Making the Code Concurrent

You can now safely use playlists in concurrent code:

let favorites = Playlist(title: "Favorite songs",
                         author: "Ehab",
                         songs: ["Where My Heart Will Take Me"])
let partyPlaylist = Playlist(title: "Party songs",
                             author: "Ray",
                             songs: ["Stairway to Heaven"])
Task {
  await favorites.move(song: "Stairway to Heaven", from: partyPlaylist)
  await favorites.move(song: "Where My Heart Will Take Me", to: partyPlaylist)
  await print(favorites.songs)
}

Using the Noninsulated Keyword

Actors, incidentally, are first-class types and can implement protocols, just like classes, structs and enums do:

extension Playlist: CustomStringConvertible {
  nonisolated var description: String {
    "\(title) by \(author)."
  }
}

print(favorites) // "Favorite songs by Ehab."

Sendable

Types conforming to the Sendable protocol are isolated from shared mutations, so they’re safe to use concurrently or across threads. These types have value semantics, which you read about in detail in Chapter 8, “Value Types & Reference Types.” Actors only deal with Sendable types; in future versions of Swift, the compiler will enforce this.

final class BasicPlaylist {
  let title: String
  let author: String

  init(title: String, author: String) {
    self.title = title
    self.author = author
  }
}

extension BasicPlaylist: Sendable {}
// 1
func execute(
  task: @escaping @Sendable () -> Void,
  with priority: TaskPriority? = nil
) {
  Task(priority: priority, operation: task)
}

// 2
@Sendable func showRandomNumber() {
  let number = Int.random(in: 1...10)
  print(number)
}

execute(task: showRandomNumber)

Challenges

Here’s a set of challenges to test your concurrency knowledge. It’s best to try and solve them yourself, but solutions are available in the challenges download folder or at the printed book’s source code link in the introduction.

Challenge 1: Safe Teams

Using the above Playlist example as a guide, change the following class to make it safe to use in concurrent contexts:

class Team {
  let name: String
  let stadium: String
  private var players: [String]

  init(name: String, stadium: String, players: [String]) {
    self.name = name
    self.stadium = stadium
    self.players = players
  }

  private func add(player: String) {
    players.append(player)
  }

  private func remove(player: String) {
    guard !players.isEmpty, let index = players.firstIndex(of: player) else {
      return
    }
    players.remove(at: index)
  }

  func buy(player: String, from team: Team) {
    team.remove(player: player)
    add(player: player)
  }

  func sell(player: String, to team: Team) {
    team.add(player: player)
    remove(player: player)
  }
}

Challenge 2: Custom Teams

Conform the asynchronous-safe type from the previous challenge to CustomStringConvertible.

Challenge 3: Sendable Teams

Make the following class Sendable:

class BasicTeam {
  var name: String
  var stadium: String

  init(name: String, stadium: String) {
    self.name = name
    self.stadium = stadium
  }
}

Key Points

Concurrent programming is a crucial topic. Future versions of Swift will likely refine the tools and approaches for writing robust concurrent programs.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now