I'm trying to understand the proper application of async-await
in Swift. Let's say an async
method, a non-IO method that doesn't make external calls, is called from a background thread to execute its process, such as some heavy image processing method.
func processImage(image: UIImage) async -> UIImage {
///
}
Task {
let result = await processImage(image: image)
}
What is happening when the code is paused and is waiting for the result? Since it's not making an external call, the process must be done somewhere from within the pool of threads. And since it's not being done in the very thread the method is called from, it must be being executed on another thread. Is a subtask created to execute the process? From what I understand,
Task
is a unit of concurrency and a single task contains no concurrency (except forasync let
), so this is a bit confusing to me. Is a task concurrent or not concurrent?I understand that if this method is called from the main thread, the non-blocking aspect of the
async
method frees up the thread for the UI elements to run, thereby providing a seamless visual experience. But, what is the benefit of calling anasync
method from a background thread? I'm not referring to the syntactic sugar of being able to return the results or throw errors. Are there any benefits to the non-blocking aspect as opposed to using a synchronous method if the method is a non-IO method called from the background? In other words, what is it not blocking? If it's a parallel process, it's utilizing more resources to process multiple things efficiently, but I'm not sure how a concurrent process in this case is beneficial.
CodePudding user response:
You need to stop thinking in terms of threads if you want to use async/await
. It is useful, to some extent and for obvious reasons, to keep using phrases like "on the main thread" and "on a background thread", but these are almost metaphors.
You just need to accept that, no matter what "thread" something runs on, await
has the magical power to say "hold my place" and to permit the computer to walk away and do something else entirely until the thing we're waiting for comes back to us. Nothing blocks, nothing spins. That is, indeed, a large part of the point of async/await
.
(If you want to understand how it works under the hood, you need to find out what a "continuation" is. But in general it's really not worth worrying about; it's just a matter of getting your internal belief system in order.)
The overall advantage of async/await
, however, is syntactic, not mechanical. Perhaps you could have done in effect everything you would do via async/await
by using some other mechanism (Combine, DispatchQueue, Operation, whatever). But experience has shown that, especially in the case of DispatchQueue, beginners (and not-so-beginners) have great difficulty reasoning about the order in which lines of code are executed when code is asynchronous. With async/await
, that problem goes away: code is executed in the order in which it appears, as if it were not asynchronous at all.
And not just programmers; the compiler can't reason about the correctness of your DispatchQueue code. It can't help catch you in your blunders (and I'm sure you've made a few in your time; I certainly have). But async/await
is not like that; just the opposite: the compiler can reason about your code and can help keep everything neat, safe, and correct.
As for the actual example that you pose, the correct implementation is to define an actor whose job it is to perform the time-consuming task. This, by definition, will not be the main actor; since you defined it, it will be what we may call a background actor; its methods will be called off the main thread, automatically, and everything else will follow thanks to the brilliance of the compiler.
Here is an example (from my book), doing just the sort of thing you ask about — a time-consuming calculation. This is a view which, when you call its public drawThatPuppy
method, calculates a crude image of the Mandelbrot set off the main thread and then portrays that image within itself. The key thing to notice, for your purposes, is that in the lines
self.bitmapContext = await self.calc.drawThatPuppy(center: center, bounds: bounds)
self.setNeedsDisplay()
the phrases self.bitmapContext =
and self.setNeedsDisplay
are executed on the main thread, but the call to self.calc.drawThatPuppy
is executed on a background thread, because calc
is an actor. Yet the main thread is not blocked while self.calc.drawThatPuppy
is executing; on the contrary, other main thread code is free to run during that time. It's a miracle!
// Mandelbrot drawing code based on https://github.com/ddeville/Mandelbrot-set-on-iPhone
import UIKit
extension CGRect {
init(_ x:CGFloat, _ y:CGFloat, _ w:CGFloat, _ h:CGFloat) {
self.init(x:x, y:y, width:w, height:h)
}
}
/// View that displays mandelbrot set
class MyMandelbrotView : UIView {
var bitmapContext: CGContext!
var odd = false
// the actor declaration puts us on the background thread
private actor MyMandelbrotCalculator {
private let MANDELBROT_STEPS = 200
func drawThatPuppy(center:CGPoint, bounds:CGRect) -> CGContext {
let bitmap = self.makeBitmapContext(size: bounds.size)
self.draw(center: center, bounds: bounds, zoom: 1, context: bitmap)
return bitmap
}
private func makeBitmapContext(size:CGSize) -> CGContext {
var bitmapBytesPerRow = Int(size.width * 4)
bitmapBytesPerRow = (16 - (bitmapBytesPerRow % 16)) % 16
let colorSpace = CGColorSpaceCreateDeviceRGB()
let prem = CGImageAlphaInfo.premultipliedLast.rawValue
let context = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: bitmapBytesPerRow, space: colorSpace, bitmapInfo: prem)
return context!
}
private func draw(center:CGPoint, bounds:CGRect, zoom:CGFloat, context:CGContext) {
func isInMandelbrotSet(_ re:Float, _ im:Float) -> Bool {
var fl = true
var (x, y, nx, ny) : (Float, Float, Float, Float) = (0,0,0,0)
for _ in 0 ..< MANDELBROT_STEPS {
nx = x*x - y*y re
ny = 2*x*y im
if nx*nx ny*ny > 4 {
fl = false
break
}
x = nx
y = ny
}
return fl
}
context.setAllowsAntialiasing(false)
context.setFillColor(red: 0, green: 0, blue: 0, alpha: 1)
var re : CGFloat
var im : CGFloat
let maxi = Int(bounds.size.width)
let maxj = Int(bounds.size.height)
for i in 0 ..< maxi {
for j in 0 ..< maxj {
re = (CGFloat(i) - 1.33 * center.x) / 160
im = (CGFloat(j) - 1.0 * center.y) / 160
re /= zoom
im /= zoom
if (isInMandelbrotSet(Float(re), Float(im))) {
context.fill (CGRect(CGFloat(i), CGFloat(j), 1.0, 1.0))
}
}
}
}
}
private let calc = MyMandelbrotCalculator()
// jumping-off point: draw the Mandelbrot set
func drawThatPuppy() async {
let bounds = self.bounds
let center = CGPoint(x: bounds.midX, y: bounds.midY)
self.bitmapContext =
await self.calc.drawThatPuppy(center: center, bounds: bounds)
self.setNeedsDisplay()
}
// turn pixels of self.bitmapContext into CGImage, draw into ourselves
override func draw(_ rect: CGRect) {
if self.bitmapContext != nil {
let context = UIGraphicsGetCurrentContext()!
context.setFillColor(self.odd ? UIColor.red.cgColor : UIColor.green.cgColor)
self.odd.toggle()
context.fill(self.bounds)
let im = self.bitmapContext.makeImage()
context.draw(im!, in: self.bounds)
}
}
}
CodePudding user response:
The Swift Programming Language: Concurrency defines an asynchronous function as “a special kind of function or method that can be suspended while it’s partway through execution.”
So, this async
designation on a function is designed for truly asynchronous routines, where the function will suspend/await the execution while the asynchronous process is underway. A typical example of this is the fetching of data with URLSession
.
But this computationally intensive image processing is not an asynchronous task. It is inherently synchronous. So, it does not make sense to mark it as async
. Furthermore, Task {…}
is probably not the right pattern, either, as that creates a “new top-level task on behalf of the current actor”. But you probably do not want that slow, synchronous process running on the current actor (certainly, if that is the main actor). You may want a detached task. Or put it on its own actor.
The below code snippet illustrates how truly asynchronous methods (like the network request to fetch the data, fetchImage
) differ from slow, synchronous methods (the processing of the image in processImage
):
func processedImage(from url: URL) async throws -> UIImage {
// fetch from network (calling `async` function)
let image = try await fetchImage(from: url)
// process synchronously, but do so off the current actor, so
// we don’t block this actor
return try await Task.detached {
await self.processImage(image)
}.value
}
// asynchronous method to fetch image
func fetchImage(from url: URL) async throws -> UIImage {
let (data, response) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else { throw ImageError.notImage }
return image
}
// slow, synchronous method to process image
func processImage(_ image: UIImage) -> UIImage {
…
}
enum ImageError: Error {
case notImage
}
For more information, see WWDC 2021 video Meet async/await in Swift. For insights about what await
(i.e., a suspension point) really means within the broader threading model, Swift concurrency: Behind the scenes might be an interesting watch.