How to capture local variable inside an async closure in Swift?
Asked Answered
A

3

31

I have the following code in Swift 5.5 and iOS 15

func getReviewIds() {
    
    var reviewIds: [Int] = []
    
    Task {
        let ids = await getReviewIdsFromGoogle()
        reviewIds.append(contentsOf: ids)
    }
    
    print("outside")
}

func getReviewIdsFromGoogle() async -> [Int] {
    await withUnsafeContinuation { continuation in
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            continuation.resume(returning: [1,2,3])
        }
    }
}

I get an error in getReviewIdsFromGoogle function on the following line:

reviewIds.append(contentsOf: ids)

Mutation of captured var 'reviewIds' in concurrently-executing code

I know that I can make the getReviewIdsFromGoogle an async function instead of using the async closure, but how can I solve this using the closure.

Andryc answered 16/6, 2021 at 4:22 Comment(2)
Your code doesn't make much sense. What do you plan on doing with reviewIds?Ballyhoo
Once I get all the reviewIds I will pass to an API and get the actual reviews OR even update the database table using those review idsAndryc
A
22

To prevent data races you must use synchronized access to variables from concurrent operations and the compiler doesn't allow you to change your array directly. To avoid the issue you can implement isolated access to your data with an actor instance e.g.:

actor Store {
    var reviewIds: [Int] = []
    func append(ids: [Int]) {
        reviewIds.append(contentsOf: ids)
    }
}

func getReviewIds() {
    
    let store = Store()
    
    Task {
        let ids = await getReviewIdsFromGoogle()
        await store.append(ids: ids)
        print(await store.reviewIds)
    }
}
Analemma answered 10/8, 2021 at 21:57 Comment(0)
V
12

You can't pass data back to the original synchronous context once you have started an async context (such as creating a new Task), as this would require the original context to "block" while it waits for the asynchronous results. Swift does not allow blocking in its concurrency model as this could lead to a thread deadlock. Each thread need to be able to make "forward progress".

You will have to just call another function with the results from the Task context to process the returned values. It's up to you if this process is another async function or not, depending on what you need to do.

func getReviewIDs() {
    Task {
        let result = await getReviewIdsFromGoogle()
        process(ids: result)
    }
}

func process(ids: [Int]) {
    print("now process ids: \(ids)")
}

func getReviewIdsFromGoogle() async -> [Int] {
    await withUnsafeContinuation { continuation in
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            continuation.resume(returning: [1,2,3])
        }
    }
}
Volk answered 17/6, 2021 at 6:47 Comment(0)
T
0

I encountered this when writing some async code with GRDB, since my types self-updated with ID when saved:

struct MyType: MutablePersistableRecord {
    var id: Int64?
    // ...

    // Update auto-incremented id upon successful insertion
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

In this case, I didn't really care about accessing the updated ID after the async write, so I just duplicated the entity :). Obviously this is inefficient, since it literally copies the instance, but that impact is negligible in my case, and I didn't need to write an actor:

func perform(db: DatabaseQueue) async throws {
    var item = MyType()
    // item.foo = "bar"

    let dupe1 = item
    try! await db.write { db in
        var dupe2 = dupe1
        dupe2.save(db)
    }

    // Note: `item` will not have an ID,
    // since I cloned it into dupe1 AND dupe2 :)
}

Done!

Tasia answered 7/1 at 1:52 Comment(3)
You're correct. In the specific case of GRDB, your other option is item = try await db.write { [item] db in try item.saved(db) }. Now item will have an id when the save has completed.Brackish
Ah, because item.saved(db) is non-mutating & returns a new instance, which we capture back into item =?Tasia
Exactly :-) The methods saved and inserted where actually introduced in order to help dealing with async/await warnings on captured mutable values. This does not invalidate your sample code, of course, which is perfectly valid. But you might be happy to use saved or inserted eventually.Brackish

© 2022 - 2024 — McMap. All rights reserved.