Search code examples
swiftcore-dataasync-awaitconcurrencytask

Is it possible to pass an async function to a Core Data Managed Object Context in Swift? If not, then why?


I've looked around for a way to pass an async function to a Core Data managed object context but can't find anyway yet. I suspect there is some part of the new async/await concurrency model that I am not understanding but I have no idea what it is.

What I'd like to do:

// Grab an moc
let moc = container.newBackgroundContext()

// Enter an async context
Task {

    await moc.perform {

        // Get some object
        let obj = moc.object(with: anObjectID)
        
        // This is not possible because NSManagedObjectContext.perform only accepts
        // a synchronous block
        await obj.doSomeLongRunningProcess()

    }
}

It seems odd to me that this is not possible. I'm not sure if it's just not there yet in the Core Data api because async/await is so new, or if there is a very good reason it's not possible?

Wrapping the doSomeLongRunningProcess in a Task like so

await moc.perform {
    Task {
        let obj = moc.object(with: anObjectID)
        await obj.doSomeLongRunningProcess()
    }
}

Doesn't work because the inner Task gets run on a different thread and you end up with CoreData inconsistencies. I was kinda hoping it would inherit the context's thread but this is not the case.

I'd love a way to pass async functions to an ManagedObjectContext, but failing that, I'd like to know why it doesn't/can't work?


Solution

  • You cannot directly call an async method from within perform. It needs to be synchronous method, which you would not await.

    You asked:

    ... but what if I want to call some other async function from within my doSomeLongRunningProcess?

    The problem is that if you hit a hypothetical await suspension point inside perform, while path of execution would suspend, the thread would be free to execute other code pending on that executor, and the “continuation” (the code after the suspension point) would run later. (An await call is not like a GCD sync call, but rather more like a dispatch group notify of the continuation.) If that happens inside perform, the integrity of this task theoretically could be undermined by other tasks that could slip in before the continuation has a chance to run. For details on these suspension points, continuations, etc., see Swift concurrency: Behind the scenes

    I would suggest changing doSomeLongRunningProcess to not be async, and, if possible, instead initiate any asynchronous tasks with Task { … } from within doSomeLongRunningProcess. But you can’t have this method be async and have await suspension points.

    For more information about using the new async-await patterns in conjunction with Swift concurrency, see WWDC 2021 video Bring Core Data concurrency to Swift and SwiftUI.