Return async function value to synchronous context in background thread

160 views Asked by At

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?

1

There are 1 answers

0
Rob On BEST ANSWER

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:

  1. The use of semaphores with Swift concurrency.

    Given that you are only calling signal from the Task {…}, you’ll get away with the semaphore usage.

    FWIW, calling wait from within the Task {…} is not permitted across concurrency domains, as discussed in Swift concurrency: Behind the scenes, which says:

    [Primitives] like semaphores ... are unsafe to use with Swift concurrency. This is because they hide dependency information from the Swift runtime, but introduce a dependency in execution in your code. Since the runtime is unaware of this dependency, it cannot make the right scheduling decisions and resolve them. In particular, do not use primitives that create unstructured tasks and then retroactively introduce a dependency across task boundaries by using a semaphore or an unsafe primitive. Such a code pattern means that a thread can block indefinitely against the semaphore until another thread is able to unblock it. This violates the runtime contract of forward progress for threads.

  2. The updating of the Int ivar from within the Task {…}.

    This is not generally permitted. But you could use an unsafe pointer to take over and do this yourself, e.g.,

    class Foo: @unchecked Sendable {
        func asyncFunc() async -> Int {
            try? await Task.sleep(for: .seconds(1))
            return 42
        }
    
        // This func is running on BG thread
        func syncFunc() -> Int {
            dispatchPrecondition(condition: .notOnQueue(.main))
    
            let semaphore = DispatchSemaphore(value: 0)
    
            let pointer = UnsafeMutablePointer<Int>.allocate(capacity: 1)
            pointer.initialize(to: 0)
    
            Task { [self] in
                pointer.pointee = await asyncFunc()
                semaphore.signal()
            }
            semaphore.wait()
    
            let value = pointer.pointee
    
            pointer.deinitialize(count: 1)
            pointer.deallocate()
    
            return value
        }
    }
    

    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:

    class Foo: @unchecked Sendable {
        func asyncFunc() async -> Int {…}
    
        // This func is running on BG thread
        func syncFunc() -> Int {
            dispatchPrecondition(condition: .notOnQueue(.main))
    
            let semaphore = DispatchSemaphore(value: 0)
            let w = Wrapper()
            Task { [self] in
                w.value = await asyncFunc()
                semaphore.signal()
            }
            semaphore.wait()
            return w.value
        }
    }
    

    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.,

    final class Wrapper: @unchecked Sendable {
        private var _value: Int = 0
        private let lock = NSLock()
    
        var value: Int {
            get { lock.withLock { _value } }
            set { lock.withLock { _value = newValue } }
        }
    }
    

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.