Nonisolated/isolated when calling other async/await functions inside of an actor?

I have a Networking client that contains some shared mutable state. I decided to make this class an actor to synchronize access to this state. Since it's a networking client, it needs to make calls into URLSession's async data functions.

struct MiddlewareManager {
  var middleware: [Middleware] = []
  func finalRequest(for request: URLRequest) -> URLRequest { ... }
}

actor Networking {
  var middleware = MiddlewareManager()
  var session: URLSession

  func data(from request: URLRequest) async throws -> (Data, URLResponse) {
    let finalRequest = middleware.finalRequest(for: request)
    let (data, response) = try await session.data(for: finalRequest)
    let finalResponse = middleware.finalResponse(for: response)
    // ...
    return (data, finalResponse)
  }

}

Making a network request could be a long running operation. From my understanding long running or blocking operations should be avoided inside of actors. I'm pretty sure this is non blocking due to the await call, the task just becomes suspended and the thread continues on, but it will inherit the actor's context. As long URLSession creates a new child task then this is ok, since the request isn't actually running on the actor's task?

My alternative approach after watching the tagged WWDC 2022 videos was to add the nonisolated tag to the function call to allow the task to not inherit the actor context, and only go to the actor when needed:

  nonisolated func data(from request: URLRequest) async throws -> (Data, URLResponse) {
    let finalRequest = await middleware.finalRequest(for: request)
    let (data, response) = try await session.data(for: finalRequest)
    let finalResponse = await middleware.finalResponse(for: response)
    // ...
    return (data, finalResponse)
  }

Is this the proper or more correct way to solve this?

From my understanding long running or blocking operations should be avoided inside of actors.

That’s true.

I'm pretty sure this is non blocking due to the await call

That’s also true. Under the covers, URLSession uses a completion handler to implement its asynchronicity. The data(for:) method is basically a wrapper around the dataTask(with:completionHandler:) method, using a continuation to bridge back to the Swift concurrency world.

the task just becomes suspended

Correct.

and the thread continues on

I’m not sure what you mean by this so let’s be clear…

In Swift concurrency, tasks are run by threads from the Swift concurrency cooperative thread pool. When a task suspends at an await, the thread running that task returns to the thread pool and looks for more work to do.

but it will inherit the actor's context.

I don’t understand what you mean by this. When the thread returns to the thread pool, it gives up the actor’s context. At this point other threads are able to enter the actor.

When the URLSession work is complete, the task becomes available to run. At some point a thread from the thread pool will pick up that task and start running it. To do that it must enter the actor’s context again. That means that this code:

let finalResponse = middleware.finalResponse(for: response)
// ...
return (data, finalResponse)

is running in the actor’s context.

As long URLSession creates a new child task then this is ok, since the request isn't actually running on the actor's task?

Actor’s don’t have tasks. Rather, task enter actors. I recommend you have another watch of the WWDC session (WWDC 2022 Session 110351 Eliminate data races using Swift Concurrency). In that analogy, actors are islands and task are boats (-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

In Swift concurrency, tasks are run by threads from the Swift concurrency cooperative thread pool. When a task suspends at an await, the thread running that task returns to the thread pool and looks for more work to do.

Thanks for the clarity here. This is what I was attempting to say with "and the thread continues on" 😅

I don’t understand what you mean by this. When the thread returns to the thread pool, it gives up the actor’s context. At this point other threads are able to enter the actor.

I thought when you would do let (data, response) = await urlSession.data(for: request) the continuation wrapper around dataTask(with:completionHandler:) created a Task under the hood that would run on the actor. After reading the docs more it states that a continuation wrapper will suspend the current task and then call the closure with checked throwing continuation for the current task. So basically whatever is run inside the closure is done outside of the swift concurrency world until the continuation is called informing swift concurrency the task is ready to run again.

Are there any major tradeoffs to using the nonisoalted approach above? I was using the new Swift Concurrency instrument tool to test the Networking actor above using the following code:

func runTest() {
  Task {
    try! await withThrowingTaskGroup(of: Void.self) { group in 
      for i in 0..3 { 
        group.addTask {
          try await networking.data(from: URLRequest(url: URL(string: "https://www.google.com")!))
        } 
      }
      
      try await group.waitForAll()
    }
  }
}

I noticed if the networking.data(from:) function did not have the nonisolated keyword the created Tasks had more time spent in the enqueued state than if I added the nonisolated keyword. I was not sure why this occurred, or if this was even truly a problem. The Tasks still all ran in parallel.

Are there any major tradeoffs to using the nonisolated approach above?

It’s hard to say without more context but my general inclination is to view middleware as part of the internal state of the actor and thus run the data(from:) method, which relies on that, as isolated.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Nonisolated/isolated when calling other async/await functions inside of an actor?
 
 
Q