0

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?

5
  • 1
    I hope you don’t take this the wrong way, but this idea of blocking a thread until some asynchronous process returns is an antipattern (and predates Swift concurrency). S.O. is littered with questions about how to make some inherently asynchronous process behave synchronously. It’s such a natural and appealing intuition (“all I want is to wait for this asynchronous work”), but the answer is almost always that it is a mistake. The whole m.o. of asynchronous patterns is to avoid blocking a thread, and it is antithetical to this to turn around and block a thread waiting for the result. Commented Nov 18, 2023 at 23:57
  • @Rob Of course I understand this is an antipattern, but unfortunately I need it because I have to use some legacy library code, with sync functions on background thread Commented Nov 19, 2023 at 15:58
  • @Rob I have to use existing sync functions on background thread returning some result, and I have a new async func obtaining this result. So I try to find a way to place this new func into old code, preserving old behavior. Commented Nov 19, 2023 at 16:04
  • 1
    No, semaphore works correctly, problem is with returning result. I've updated my question to provide more context and actual errors. Commented Nov 19, 2023 at 18:32
  • Let us continue this discussion in chat. Commented Nov 19, 2023 at 18:52

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.