Is it possible to access result of async function outside of task on background thread? We can access result from task on main thread using @MainActor But I need it on background thread which is actually waiting for the result of async func.
Consider following code:
func asyncFunc() async -> Int {
// some async code here
}
// This func is running on BG thread
func syncFunc() -> Int {
let semaphore = DispatchSemaphore(value: 0)
var value: Int
Task {
value = await asyncFunc() // Produces “Mutation of captured var 'value' in concurrently-executing code” error
semaphore.signal()
}
semaphore.wait()
return value
}
For this compiler shows error for the first line in Task:
Mutation of captured var 'value' in concurrently-executing code.
And yes, this error is clear and expected. Then I try to use Actor to wrap value:
actor ValueActor {
var value = 0
func setValue(_ newValue: Int) {
value = newValue
}
}
// This func is running on BG thread
func syncFunc() -> Int {
let semaphore = DispatchSemaphore(value: 0)
let valueActor = ValueActor()
Task {
let value = await asyncFunc()
await valueActor.setValue(value)
semaphore.signal()
}
semaphore.wait()
return valueActor.value // Produces “Actor-isolated property 'value' can not be referenced from a non-isolated context” error
}
In this case, the error is for the returning value:
Actor-isolated property 'value' can not be referenced from a non-isolated context
This error is also expected, but... may be some workaround can be found to return the value?
Bottom line, this is an anti-pattern to be avoided. You should refactor the library to adopt Swift concurrency more broadly, if possible. Or just do not adopt Swift concurrency until you are ready to do that.
But, let’s set that aside for a second. There are two questions:
The use of semaphores with Swift concurrency.
Given that you are only calling
signal
from theTask {…}
, you’ll get away with the semaphore usage.FWIW, calling
wait
from within theTask {…}
is not permitted across concurrency domains, as discussed in Swift concurrency: Behind the scenes, which says:The updating of the
Int
ivar from within theTask {…}
.This is not generally permitted. But you could use an unsafe pointer to take over and do this yourself, e.g.,
With an unsafe pointer (with a stable memory address), you can do whatever you want (and you bear responsibility to ensure the thread/address safety, yourself).
Or, as you pointed out, you can introduce some type class to manage this for you:
But if you turn on “Strict Concurrency Checking” build setting to “Complete”, it will warn you that you must make this
Sendable
(i.e., employ your own synchronization). E.g.,But, again, this whole idea is an antipattern. It is generally a mistake to adopt Swift concurrency (with whom we have a contract to never impede forward progress, i.e., to never block a thread), but then to turn around and start blocking threads. Admittedly, we are not doing this with the cooperative thread-pool, but it is still generally ill-advised. You should be careful to avoid the concomitant deadlock risks, and the like.