Home iOS & Swift Books Swift Apprentice

29
Concurrency Written by Cosmin Pupăză

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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, such as reading from a network or database, 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 the app feels slow and unresponsive.

By executing these operations asynchronously, your program is free to work on other tasks 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. Still, more recently, the core team has introduced a suite of language-level features, making concurrency 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 Swift is built on top of threads, but you don’t need to manipulate them directly. In Swift-concurrency-speak, the term main actor is used in place 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:

import SwiftUI

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 — printing 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

Note: The import to SwiftUI pulls in the private _Concurrency framework that defines Task. Importing UIKit will also work. (The leading underbar on _Concurrency indicates the name may change in a future release, so importing SwiftUI or UIKit is more future-proof than importing _Concurrency directly.)

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(nanoseconds: 1_000_000_000)
print("Goodbye")

Task {
  print("Hello")
  try Task.sleep(nanoseconds: 1_000_000_000)
  print("Goodbye")
}

Task {
  print("Hello")
  try await Task.sleep(nanoseconds: 1_000_000_000)
  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(nanoseconds: 1_000_000_000)
  print("Goodbye")
}

func helloPauseGoodbye() async throws {
  print("Hello")
  try await Task.sleep(nanoseconds: 1_000_000_000)
  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” at raywenderlich.com 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.raywenderlich.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.raywenderlich.com")!) {
    print(title)
  }
}
<title>raywenderlich.com | High quality programming tutorials: iOS, Android, Swift, Kotlin, Flutter, Server Side Swift, Unity, and more!</title>

Ordering your concurrency

In the previous examples, you just made a new unstructured Task block whenever you needed an asynchronous context that can 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 22, “Error Handling”, you can mark read-only computed properties with async. For example:

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])  // "Unity", as of this writing
}

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 Playlist to an 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: "Cosmin", 
                         songs: ["Nothing else matters"])
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: "Nothing else matters", 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 Cosmin."

Sendable

Types conforming to the Sendable protocol are isolated from shared mutations, so they’re safe to use concurrently. These types have value semantics, which you read about in detail in Chapter 25, “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 the last 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 if you get stuck.

Challenge 1: Safe teams

Using the above Playlist example as a guild, 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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.