Is there any state that's being read/written to in the ImageProcessing actor? If not then I don't know if you should use an actor at all unless whatever processImageData does requires exclusive execution.
As for calling that code on the background from the main actor, you need to use asyncDetached because regular async will inherit the actor from the current execution context (the MainActor in this case).
I think this is the code you're aiming for:
@MainActor
class OsuImageCache {
init() { ... }
/// Grab an image on the main thread for use in collection view cells
func image(for url: URL) -> UIImage? { }
/// Cache an image on a background thread
func cacheImage(for url: URL) async throws -> (image: UIImage?, thumbnail: UIImage?) {
let (data, _) = try await session.data(from: url)
let processTask = asyncDetached(priority: /* use whatever you want like .userInitiated or .utility */) {
return processImageData(data, for url)
}
let processedImages = await processTask.get()
try saveInFileSystem(imageData: processedImages.imageData, thumbnailImageData: processedImages.thumbnailData, for url: URL)
return (UIImage(data: processedImages.imageData), UIImage(data: processedImages.thumbnailData))
}
/// Resize images and create thumbnails
/// NOT ASYNC
func processImageData(_ data: Data, url: URL) -> (UIImage, UIImage) {
// Code that resizes the image and the thumbnail goes here. It's all inline, no completion handlers nor async calls...
...
// Finally, return
return (finalProcessedImageData, finalProcessedThumbnailData)
}
}
Post
Replies
Boosts
Views
Activity
No problem. π Yes, concurrency will always be complex and devs will need to take care when using it. The real awesome benefits of async/await and actors are better syntax and compile time checks against the lower-level problems of data corruption that are super-hard to find. Trying to reason about the behavior you asked about would be 10x harder in a callback-based implementation. π΅
let handle = async {} isn't a suspension point (notice that you don't await the result) so the read and write are not interrupted.
It's a little confusing because that usage of async is actually a function call that has the same name as the async keyword.
First of all, I'd cache the prepared the thumbnail, possibly in a separate cache or make your asset store layered in a way that you just request a URL and it will supply a prepared image or load + prepare it for you. With separate caches, you can restrict the sizes separately (look into using NSCache for this).
Secondly, a call being async doesn't mean it will suspend 100% of the time when called. For instance, if your asset(forURL:) function doesn't have to await in the code path that returns .image, then the call is essentially synchronous. Similarly, you should mark this view controller and cell as @MainActor so you can remove the dispatch async to main within your async {}. The task will inherit the @MainActor from the context that creates the task.
To get the behavior you want, you need to hold onto the task handle that async {} returns so you can cancel it when the cell is reused.
Assuming you're referring to the code from this post: https://developer.apple.com/forums/thread/682032
This code works because of the checks at the beginning that are omitted in your quote. Only a call to this function that has read cache[url] as nil can start down the code path to write to the internal state. While a call is suspended at let image = try await handle.get(), any other call for the same URL will read .inProgress from the cache and return/throw the task handle's result without writing to the internal state.
There's no suspension points between reading the current state and writing something based on that state.
reading .inProgress or .ready will not cause that call to write to internal state.
reading nil and writing .inProgress happen within the same uninterrupted execution. After suspending, this context will overwrite its own earlier value of .inProgress with either .ready or nil. This is valid for the code as it is written, if we wanted to be extra-safe we could re-read and make sure the state for that URL is still .inProgress with the same handle. That's not necessary for this code, but adding other functions that manipulate the actor's state (e.g. adding a deleteCache(for: URL) method) would require careful updates to make sure this stays correct.
Async/await is specifically built to not allow what you're describing here.
async {} returns a Task.Handle<> that allows cancellation or waiting on the result, but you have to wait on the Task with other async code. This is a strength, not a weakness, of async/await.
Really hope the sample code is posted soon, I'm trying to implement it myself as an exercise but there's some advanced stuff with the shape transform handles that I have no clue how to implement in SwiftUI and would love to see how they achieved that.