Instruction

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

Actors

Working with concurrent code can become complex very quickly, especially if you have the state of an object being accessed and changed across different tasks. This accessing and updating of object state across tasks is known as Shared Mutable State.

// 1
public struct Trip {
  public let id: String
  public let directions: [String]
  public let duration: Int
}

public enum TripPart {
  case directions([String])
  case duration(Int)
}

// 2
class TripStore {
  typealias TripTask = Task<Trip, Error>
  private var taskLookup = [String: TripTask]()
  private var tripLookup = [String: Trip]()
  
  func task(for id: String) -> TripTask? {
    return taskLookup[id]
  }
    
  func setTask(_ task: TripTask, for id: String) {
    taskLookup[id] = task
  }
  
  func trip(for id: String) -> Trip? {
    return tripLookup[id]
  }
    
  func setTrip(_ trip: Trip, for id: String) {
    tripLookup[id] = trip
  }
}

// 3
class TripClient {
  
  private var tripStore = TripStore()
  
  func getTrip(for id: String) async throws -> Trip {
    if let trip = tripStore.trip(for: id) {
      return trip
    }
    if let task = tripStore.task(for: id) {
      return try await task.value
    }
    tripStore.setTask(Task {
      return try await withThrowingTaskGroup(of: TripPart.self, returning: Trip.self) { taskGroup in
        var directions: [String]!
        var duration: Int!
        taskGroup.addTask { [unowned self] in
          return try await getDirections(for: id)
        }
        taskGroup.addTask { [unowned self] in
          return try await getDuration(for: id)
        }
        for try await tripPart in taskGroup {
          switch tripPart {
          case .directions(let retrievedDirections):
            directions = retrievedDirections
          case .duration(let retrievedDuration):
            duration = retrievedDuration
          }
        }
        let newId = id + "-" + String(Int.random(in: 0...1000))
        let trip = Trip(id: newId, directions: directions, duration: duration)
        tripStore.setTrip(trip, for: id)
        tripStore.removeTask(for: id)
        return trip
      }
    }, for: id)
    return try await tripStore.task(for: id)!.value
  }
  
  // 5
  private func getDirections(for tripId: String) async throws -> [String] {
    try await simulateNetworkCall()
    return [
      "Turn left in 500 feet",
      "Turn right at the stop light",
      "Destination is on your left"
    ]
  }
  
  public func getDuration(for tripId: String) async throws -> Int {
    try await simulateNetworkCall()
    return 42
  }
  
  private func simulateNetworkCall() async throws {
    try await Task.sleep(nanoseconds: 1_000_000)
  }
}
let client = TripClient()
for _ in 0...1000 {
  Task {
    let trip = try await client.getTrip(for: "id1")
    print(trip)
  }
}
Trip(id: "id1-638", directions: ...)
Trip(id: "id1-602", directions: ...)
Trip(id: "id1-638", directions: ...)
Trip(id: "id1-638", directions: ...)
Trip(id: "id1-716", directions: ...)
Trip(id: "id1-638", directions: ...)
actor TripStore {
  typealias TripTask = Task<Trip, Error>
  // ...
func getTrip(for id: String) async throws -> Trip {
  if let trip = await tripStore.trip(for: id) {
    return trip
  }
  if let task = await tripStore.task(for: id) {
    return try await task.value
  }
  await tripStore.setTask(Task {
    return try await withThrowingTaskGroup(of: TripPart.self, returning: Trip.self) { taskGroup in
      var directions: [String]!
      var duration: Int!
      taskGroup.addTask { [unowned self] in
        return try await getDirections(for: id)
      }
      taskGroup.addTask { [unowned self] in
        return try await getDuration(for: id)
      }
      for try await tripPart in taskGroup {
        switch tripPart {
        case .directions(let retrievedDirections):
          directions = retrievedDirections
        case .duration(let retrievedDuration):
          duration = retrievedDuration
        }
      }
      let newId = id + "-" + String(Int.random(in: 0...1000))
      let trip = Trip(id: newId, directions: directions, duration: duration)
      await tripStore.setTrip(trip, for: id)
      await tripStore.removeTask(for: id)
      return trip
    }
  }, for: id)
  return try await tripStore.task(for: id)!.value
}

Non-Isolation

Sometimes, you may want to add code to an actor that’s Non Isolated if it doesn’t interact with the actor’s isolated state. You can do that using the non-isolated keyword. Take a look at adding a non-isolated function to TripIdStore:

actor TripStore {
  typealias TripTask = Task<Trip, Error>
  private var taskLookup = [String: TripTask]()
  private var tripLookup = [String: Trip]()

  private var pastTrips = [String: Trip]()
  
  func task(for id: String) -> TripTask? {
    return taskLookup[id]
  }
    
  func setTask(_ task: TripTask, for id: String) {
    taskLookup[id] = task
  }
  
  func trip(for id: String) -> Trip? {
    return tripLookup[id]
  }
    
  func setTrip(_ trip: Trip, for id: String) {
    tripLookup[id] = trip
  }

  nonisolated func pastTrip(for id:String) -> Trip? {
   return pastTrips[id]
  }
}

Global Actors

You don’t always have to apply actors at the type level. You can also use annotations to apply Data Isolation across a whole group of objects, known as Global Actors. One useful annotation is the @MainActor annotation. This makes any object you annotate with @MainActor safe to use on the main thread. Useful if you’re expecting to use an object as part of updating the UI.

@MainActor class TripsViewModel {
  
  // All the properties and functions in this class are safe for the main thread to access.
}
class TripsViewModel {
  
  @MainActor var trips: [Trip] // This property is safe to access on the main thread

  @MainActor func addTrip() {
    ...
    // This function is also safe to work on the main thread
  }
}
See forum comments
Download course materials from Github
Previous: Introduction Next: Demo