Chapters

Hide chapters

Modern Concurrency in Swift

Second Edition · iOS 16 · Swift 5.8 · Xcode 14

Section I: Modern Concurrency in Swift

Section 1: 11 chapters
Show chapters Hide chapters

8. Getting Started With Actors
Written by Marin Todorov

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

In the last chapter, you used the TaskGroup and ThrowingTaskGroup APIs to execute tasks in parallel. This lets you do work faster should a single CPU core not suffice your needs.

You explored TaskGroup‘s ingenious design, which allows you to run tasks in parallel but still collect the execution’s results in a safe, serial manner by iterating the group as an asynchronous sequence.

TaskGroup execution Task 5 Task 4 Task 3 Task 2 Task 1 Collect task results Clean up

As mentioned in the “Mutating Shared State” subsection of the previous chapter, some problems require you to update your state from a concurrent context. That’s one of the challenging aspects of concurrent programming: taming different threads that try to access the same piece of memory at the same time.

worker(number:) worker(number:) worker(number:) worker(number:) worker(number:) worker(number:) thread 2 🔥 thread 1 myVariable crash or inconsistency at “best”

This chapter will cover how the new Swift concurrency model addresses data races by using a new type: actor.

Before you dive into this new type, you’ll take a moment to understand what the issue with updating mutable state really is.

Understanding Thread-safe Code

You might have seen methods described as thread-safe in documentation from Apple or third-party frameworks.

This usually means that, regardless of whether you’re calling an API from the main thread or a so-called background thread, the method will always behave as expected. In other words, the method will still work, even when multiple threads call it at the same time.

Note: The concept of thread-safe code is also sometimes referred to as linearizability or atomicity, which aims to limit the outcomes of concurrently accessing an object from multiple processes.

Unfortunately, in Objective-C and versions of Swift before 5.5, there was no syntax to mark a method as thread-safe. You had to rely on each method’s documentation to find out whether it was safe or not.

Third-party frameworks sometimes give you access to their source, but that doesn’t always solve the problem. For example, can you tell immediately if this piece of code is thread-safe?

class Counter {
  private var count = 0

  func increment() {
    count += 1
  }
}

As you see, nothing stands out when you look at Counter that would make it particularly unsafe.

And yet, if two threads running in parallel both call Counter.increment(), you could end up increasing count by either one or two — the exact outcome being unpredictable. Even worse, if the two calls to Counter.increment() happen at precisely the same moment — your app will crash.

Even more worrisome is that crashes rarely happen when you compile your app for debugging — for example, when the app is running in your iOS simulator or you started it from Xcode on your device. Release builds are the ones that are optimized and fast enough to produce a data-race crash.

Therefore, you can say that any code that doesn’t take proactive steps towards protecting shared mutable state from concurrent access is inherently not thread-safe.

Traditionally developers used locks, semaphores or serial dispatch queues to ensure exclusive access to shared state. With a lock, for example, a thread locks the access to a shared resource, and other threads need to wait for it to unlock before they can read or write to that same resource.

Effectively, threads lock each other out to guarantee exclusive access to the resource:

Waiting for lock... thread 1 thread 2 uses myVariable uses myVariable method() method() Lock access to myVariable Unlock access to myVariable Lock access to myVariable Unlock access to myVariable

Concurrent code that uses lock APIs — like OSAllocatedUnfairLock — is fairly fast and safe when written well. The previous code sample looks like this when you use a lock:

class Counter {
  private var lock = OSAllocatedUnfairLock()
  private var count = 0

  func increment() {
    lock.withLock {
      count += 1
    }
  }
}

The code does actually look pretty straightforward — every code that’s wrapped in a withLock {...} runs exclusively to any other code that uses the same lock.

However, do you remember why you looked into this section’s code sample in the first place? As a developer using this API, how can you tell if calling Counter.increment() is thread-safe or not? Furthermore, how can the compiler itself know your code is thread-safe, so it can help protect you from any races resulting from a developer mistake, for example?

If you don’t have access to the code, or the free time to read it thoroughly, there’s really no way to tell if it’s really safe. That’s where actors come in.

Meeting Actor

The actor type is one of the concurrency-related improvements introduced in Swift 5.5. actor is a programming type just like its peers: enum, struct, class and so on. More specifically, it’s a reference type like class.

actor Counter {
  private var count = 0

  func increment() {
    count += 1
  }
}
ocqok joceaq ileviqeb ivvlq oqrfp ospyn andxv wesheq() dqPojiapbe xtGanaekli suljay() sdTecaujmi utsim apjes uqcaf ydams zuhjospoyk bomr

ikgufVihvaj() EqqamRpxi shodo etofabiow ounkopo uzhylbbamuiq esmowc RbOpdis dyKeg npHelkah() ocpoya yxzrskiroos enduhl

Recognizing the Main Actor

You’ve already worked with actors in this book, although they were only mentioned in passing. Any time you had to work on UI-related code, you ran it on the main actor.

Getting Started with Actors

EmojiArt is an app that lets you browse an online catalog of digital emoji art. To verify that the digital art is authentic, the app reads the feed of current works of art from the server, verifies the digital signature of the images and, only then, displays them onscreen.

Mutating State Concurrently

Open the app’s model file, EmojiArtModel.swift, and add this new code inside the class:

private(set) var verifiedCount = 0

func verifyImages() async throws {
  try await withThrowingTaskGroup(of: Void.self) { group in

  }
}
imageFeed.forEach { file in
  group.addTask { [unowned self] in
    try await Checksum.verify(file.checksum)
    self.verifiedCount += 1
  }
}

try await group.waitForAll()

Showing the Art and Updating the Progress

You’ll start by adding a call to verifyImages(...) in the screen showing the verification indicator. Open LoadingView.swift and scroll to the task { ... } view modifier.

try await model.verifyImages()
withAnimation {
  isVerified = true
}

.onReceive(timer) { _ in
  guard !model.imageFeed.isEmpty else { return }

  progress = Double(model.verifiedCount) / Double(model.imageFeed.count)
}

Detecting Race Conditions

One way to detect data races in your code is to enable the Thread Sanitizer in your Xcode project scheme. Click the scheme selector in Xcode’s toolbar and select Edit scheme…:

Using Actors to Protect Shared Mutable State

To protect EmojiArtModel.verifiedCount from concurrent access, you’ll convert EmojiArtModel from a class to an actor. Since actors exhibit a lot of typical class behavior, such as by-reference semantics, the change shouldn’t be too complex.

actor EmojiArtModel: ObservableObject
"Actor-isolated property 'verifiedCount' can not be mutated from a Sendable closure".
private func increaseVerifiedCount() {
  verifiedCount += 1
}
await self.increaseVerifiedCount()

Sharing Data Across Actors

Given that you mostly use EmojiArtModel.imageFeed to drive the app’s UI, it makes sense to place this property on the main actor. But how can you share it between the main actor and EmojiArtModel?

@Published @MainActor private(set) var imageFeed: [ImageFile] = []
await MainActor.run {
  imageFeed.removeAll()
}
await MainActor.run {
  imageFeed = list
}
EfiyiObtJilun elluf PoahAxsep foisAloyag() fuuxOlosok() deecElatey() udvori utaseYeoc inzofi oliliLiaq

Fixing the Remaining Errors

Scroll to verifyImages() and find the error on the line that calls imageFeed.forEach { ... }. To access the actor, you need to call imageFeed.forEach { ... } asynchronously.

await imageFeed.forEach { file in
Actor-isolated property 'verifiedCount' can not be referenced from the main actor
Task {
  progress = await Double(model.verifiedCount) /
    Double(model.imageFeed.count)
}

Understanding Sendable

Sendable is a protocol that indicates that a given value is safe to use in concurrent code. “Use how?” you might ask. Dig in and see.

init(
  priority: TaskPriority? = nil, 
  operation: @escaping @Sendable () async -> Success
)
mutating func addTask(
  priority: TaskPriority? = nil, 
  operation: @escaping @Sendable () async -> ChildTaskResult
)

Making Safe Methods nonisolated

Now that you’ve moved imageFeed off your own actor and onto the main actor, the methods that work with the feed don’t actually work with your actor’s shared state directly.

nonisolated func loadImages() async throws
nonisolated func downloadImage(_ image: ImageFile) async throws -> Data

Designing More Complex Actors

Now that you’ve created a fairly simple actor, it’s time to try a more complex design. You’ll mix actors, tasks and async/await to solve one of the eternal problems in programming: image caching.

import UIKit

actor ImageLoader {
  enum DownloadState {
    case inProgress(Task<UIImage, Error>)
    case completed(UIImage)
    case failed
  }

  private(set) var cache: [String: DownloadState] = [:]
}

Filling up the Cache

Next, you’ll add a few methods to manage the cache: adding images, starting a new download and clearing the cache.

func add(_ image: UIImage, forKey key: String) {
  cache[key] = .completed(image)
}
func image(_ serverPath: String) async throws -> UIImage {
  if let cached = cache[serverPath] {
    switch cached {
    case .completed(let image):
      return image
    case .inProgress(let task):
      return try await task.value
    case .failed: throw "Download failed"
    }
  }
}
let download: Task<UIImage, Error> = Task.detached {
  guard let url = URL(string: "http://localhost:8080".appending(serverPath))
  else {
    throw "Could not create the download URL"
  }
  print("Download: \(url.absoluteString)")
  let data = try await URLSession.shared.data(from: url).0
  return try resize(data, to: CGSize(width: 200, height: 200))
}

cache[serverPath] = .inProgress(download)

Wrapping up the Image Download

Last but not least, you need to handle the result of the download. Append this last piece of code to image(_:):

do {
  let result = try await download.value
  add(result, forKey: serverPath)
  return result
} catch {
  cache[serverPath] = .failed
  throw error
}
func clear() {
  cache.removeAll()
}

Sharing the Actor

Since you’ll use ImageLoader in a few different views, your next step is to inject it directly into the SwiftUI environment, so you can easily access it throughout your view hierarchy.

actor ImageLoader: ObservableObject
.environmentObject(ImageLoader())
@EnvironmentObject var imageLoader: ImageLoader
.task {
  guard let image = try? await imageLoader.image(file.url) else {
    overlay = "camera.metering.unknown"
    return
  }
  updateImage(image)
}

Using the Cached Assets

The server image feed intentionally returns some duplicate assets so you can play around with the scenario of getting an already-cached asset and displaying it.

Download: http://localhost:8080/gallery/image?11
Download: http://localhost:8080/gallery/image?16
Download: http://localhost:8080/gallery/image?23
Download: http://localhost:8080/gallery/image?26
@EnvironmentObject var imageLoader: ImageLoader
.task {
  image = try? await imageLoader.image(file.url)
}

Key Points

  • The actor type is a type that protects its internals from concurrent access, supported by compile-time checks and diagnostics.
  • Actors allow “internal” synchronous access to their state while the compiler enforces asynchronous calls for access from the “outside”.
  • Actor methods prefixed with the nonisolated keyword behave as standard class methods and provide no isolation mechanics.
  • Actors use a runtime-managed serial executor to serialize calls to methods and access to properties.
  • The Sendable protocol indicates a value is safe to use in a concurrent context. The @Sendable attribute requires a sendable value for a method or a closure parameter.
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 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 Kodeco Personal Plan.

Unlock now