I would like to convert a sync function to an async function, but I don't know what is the correct way to do it.
Let assume I have a sync function that take a long time to get data:
func syncLongTimeFunction() throws -> Data { Data() }
Then I called it in the below function, it is still a sync function.
func syncGetData() throws -> Data {
return try syncLongTimeFunction()
}
But now I would like to convert it to async function. What is the correct way below:
First way:
func asyncGetData() async throws -> Data {
return try syncLongTimeFunction()
}
Second way:
func asyncGetData2() async throws -> Data {
return try await withCheckedThrowingContinuation { continuation in
DispatchQueue.global().async {
do {
let data = try self.syncLongTimeFunction()
continuation.resume(returning: data)
} catch {
continuation.resume(throwing: error)
}
}
}
}
I thought the first way is enough, like Rob has answer below. But when I call the async function like below on main thread, the syncLongTimeFunction()
is processed on main thread as well. So it will block the main thread.
async {
let data = try? await asyncGetData()
}
In short, it depends upon the nature of the slow/synchronous function:
Is it some relatively short-lived synchronous task and you are trying to avoid a momentary hitch in the UI?
In this case, the common suggestion would often be a “detached” task:
func asyncGetData() async throws -> Data {
try await Task.detached {
try self.someSyncFunction()
}.value
}
But this is unstructured concurrency, so you bear the burden of supporting task cancelation, yourself. So something like the following might be more appropriate:
func asyncGetData() async throws -> Data {
let task = Task.detached {
try self.longTimeFunction()
}
return try await withTaskCancellationHandler {
try await task.value
} onCancel: {
task.cancel()
}
}
(Needless to say, that assumes that longTimeFunction
even supports cancelation. More on that below. It also assumes that longTimeFunction
is not isolated to the actor in question; you might want to make sure it is non-isolated.)
Alternatively, as of Swift 5.7 (thanks to SE-0338), you can enjoy structured concurrency and get it off the current actor with a non-isolated async
function:
nonisolated func asyncGetData() async throws -> Data {
try self.longTimeFunction()
}
Because that is structured concurrency, we get cancelation propagation for free. And because it is non-isolated async
function, it gets it off the current actor.
For the sake of completeness, there are other alternatives, too. But these are a few common ways to get a task off the current actor.
Is this slow function performing some computationally intensive calculation?
In order to ensure that the actor can always make “forward progress”, you would want to periodically Task.yield
. And in this case, you would also want to make sure that it periodically checks for cancelation, with checkCancellation
or isCancelled
, too.
For example:
func longTimeFunction() async throws -> Data {
…
for i in … {
try Task.checkCancellation()
await Task.yield()
…
}
return …
}
Now, the frequency with which you check for cancelation and yield is up to you. I generally strike some balance between speed of the calculations and the frequency with which I want to check (e.g., if i.isMultiple(of: 100) {…}
), but, hopefully, you get the idea.
Or is it really is a slow, synchronous function that you cannot easily refactor to support Swift concurrency?
Then, we must be aware that the Swift concurrency is predicated upon contract to ensure tasks can always make forward progress.
Specifically, SE-0296 - Async/await warns us that we either must offer some way to interleave within Swift concurrency (e.g., using Task.yield
as discussed in prior point) or that we would “generally run it in a separate context”:
Because potential suspension points can only appear at points explicitly marked within an asynchronous function, long computations can still block threads. This might happen when calling a synchronous function that just does a lot of work, or when encountering a particularly intense computational loop written directly in an asynchronous function. In either case, the thread cannot interleave code while these computations are running, which is usually the right choice for correctness, but can also become a scalability problem. Asynchronous programs that need to do intense computation should generally run it in a separate context.
That Swift evolution proposal does not explicitly define “run it in a separate context”. But in WWDC 2022 video Visualize and optimize Swift concurrency, they suggest GCD:
If you have code that needs to do these things, move that code outside of the concurrency thread pool – for example, by running it on a dispatch queue – and bridge it to the concurrency world using continuations. Whenever possible, use async APIs for blocking operations to keep the system operating smoothly.
In short, in this scenario, you would employ your option 2, with GCD code wrapped in withCheckedThrowingContinuation
. Clearly all the traditional issues that we have to worry with GCD (e.g., mitigating thread-explosion, manually ensuring thread-safety, avoiding deadlocks, etc.) still apply. And be aware that when you start tying up CPU cores with GCD API, this falls outside of the purview of the Swift concurrency cooperative thread pool, so you can still end up with CPU overcommits that this pool was designed to mitigate.
The other approach to keep work off of the Swift concurrency cooperative thread pool is to isolate the work to an actor
which has a custom executor:
actor Foo {
private let queue = DispatchSerialQueue(label: Bundle.main.bundleIdentifier! + ".Foo", qos: .utility)
nonisolated var unownedExecutor: UnownedSerialExecutor {
queue.asUnownedSerialExecutor()
}
…
}
Now you enjoy actor
isolation, but you will not tie up any threads from the cooperative thread pool.
So, bottom line, depends entirely upon the nature of the synchronous and/or long running routine. Unfortunately, it is impossible to be more specific without more information about syncLongTimeFunction
.