How to Prevent Actor Reentrancy Resulting in Duplicative Requests

How to prevent actor reentrancy resulting in duplicative requests?

You can find the “better solution” code in the Developer app. Open the session in the Developer app, select the Code tab, and scroll to “11:59 - Check your assumptions after an await: A better solution”.

screen shot of Developer app

The screen shot is from my iPad, but the Developer app is also available on iPhone, Mac, and Apple TV. (I don't know if the Apple TV version gives you a way to view and copy the code, though…)

As far as I can tell, the code is not available on the developer.apple.com web site, either on the WWDC session's page or as part of a sample project.

For posterity, here is Apple's code. It is extremely similar to that of Andy Ibanez:

actor ImageDownloader {

private enum CacheEntry {
case inProgress(Task<Image, Error>)
case ready(Image)
}

private var cache: [URL: CacheEntry] = [:]

func image(from url: URL) async throws -> Image? {
if let cached = cache[url] {
switch cached {
case .ready(let image):
return image
case .inProgress(let task):
return try await task.value
}
}

let task = Task {
try await downloadImage(from: url)
}

cache[url] = .inProgress(task)

do {
let image = try await task.value
cache[url] = .ready(image)
return image
} catch {
cache[url] = nil
throw error
}
}
}

One method in many Tasks async/await

You said:

I want [the second attempt] to wait for the first refresh API finish

You can save a reference to your Task and, if found, await it. If not found, then start the task:

actor Refresh {
var task: Task<Void, Never>?

func refresh(value: Int) async {
try! await Task.sleep(nanoseconds: 100_000_000) // imitation API CALL
print("Try to refresh: \(value)")
}

func mockCallAPI(value: Int) async {
if let task = self.task {
_ = await task.result
return
}

task = Task {
await refresh(value: value)
task = nil
}
}
}

Apple showed example of this pattern in the code provided the with WWDC 2021 video, Protect mutable state with Swift actors (but this code is not on the web site; only provided in the Developer app). See How to prevent actor reentrancy resulting in duplicative requests?

Their example is more complicated (a pattern to avoid duplicate network requests from being initiated by some image cache/downloader), but the kernel of the idea is the same: Save and await the Task.

Why does a Task within a @MainActor not block the UI?

await is a yield point in Swift. It's where the current Task releases the queue and allows something else to run. So at this line:

        try await doNetworkRequest()

your Task will let go of the main queue, and let something else be scheduled. It won't block the queue waiting for it to finish.

This means that after the await returns, it's possible that other code has been run by the main actor, so you can't trust the values of properties or other preconditions you've cached before the await.

Currently there's no simple, built-in way to say "block this actor until this finishes." Actors are reentrant.

Should you design websites that require JavaScript in this day & age?

5% according to these statistics: http://www.w3schools.com/browsers/browsers_stats.asp



Related Topics



Leave a reply



Submit