Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution

I'm keeping most information in an actor and I would like to save also a closure in it that I get from

func application(
        _ application: UIApplication,
        handleEventsForBackgroundURLSession identifier: String,
        completionHandler: @escaping () -> Void)

Task.init{
                await GeoreferenceQueue.shared.setBackgroundCompletionHandler(completionHandler)
            }
}

where GeoreferenceQueue is and actor, while the caller is a class. yet I receive error:

Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure

and

Sending task-isolated 'completionHandler' to actor-isolated instance method 'setBackgroundCompletionHandler' risks causing data races between actor-isolated and task-isolated uses

Answered by DTS Engineer in 887130022

So, yeah, the compiler is right to complain about this. That completion handler is intended to be called from the main thread. By passing it to a newly created task you allow it to be called from other contexts, which is Not Good™.

There isn’t one true way of resolving this issue. The best option is gonna vary based on how you’ve set up your code. For example, the following is a very expedient option that kinda matches your existing design:

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {

    …
    
    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        let runCompletionHandler = { @MainActor in
            completionHandler()
        }
        Task {
            await GeoreferenceQueue.shared.addCompletionHandler(runCompletionHandler)
        }
    }
}

actor GeoreferenceQueue {
    static let shared = GeoreferenceQueue()

    func addCompletionHandler(_ h: (() async -> Void)) {
        …
    }
}

It works by wrapping the completion handler sync function in an async function that’s also bound to the main actor. This function is sendable because, regardless of where it’s called from, it’ll bounce to the main actor before called completionHandler. Given that, it’s fine to pass the wrapper to the GeoreferenceQueue actor.

However, that’s just one example. If I were in your shoes I’d probably move the code that keeps track of these completion handlers to a main-actor-isolated type and then loosely couple that type to GeoreferenceQueue via some sort of notification.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

So, yeah, the compiler is right to complain about this. That completion handler is intended to be called from the main thread. By passing it to a newly created task you allow it to be called from other contexts, which is Not Good™.

There isn’t one true way of resolving this issue. The best option is gonna vary based on how you’ve set up your code. For example, the following is a very expedient option that kinda matches your existing design:

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {

    …
    
    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        let runCompletionHandler = { @MainActor in
            completionHandler()
        }
        Task {
            await GeoreferenceQueue.shared.addCompletionHandler(runCompletionHandler)
        }
    }
}

actor GeoreferenceQueue {
    static let shared = GeoreferenceQueue()

    func addCompletionHandler(_ h: (() async -> Void)) {
        …
    }
}

It works by wrapping the completion handler sync function in an async function that’s also bound to the main actor. This function is sendable because, regardless of where it’s called from, it’ll bounce to the main actor before called completionHandler. Given that, it’s fine to pass the wrapper to the GeoreferenceQueue actor.

However, that’s just one example. If I were in your shoes I’d probably move the code that keeps track of these completion handlers to a main-actor-isolated type and then loosely couple that type to GeoreferenceQueue via some sort of notification.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Unfortunately using your code as of:

func application(
        _ application: UIApplication,
        handleEventsForBackgroundURLSession identifier: String,
        completionHandler: @escaping () -> Void) {
            let runCompletionHandler = { @MainActor in
                    completionHandler()
            }
            Task.init{
                await GeoreferenceQueue.shared.addCompletionHandler(runCompletionHandler)
            }
}

produces:

Sending 'completionHandler' risks causing data races

And @MainActor in your code is blue as it were recognized, in my code it remain black. And moreover in the actor at:

func addCompletionHandler(_ h: @escaping (() async -> Void)) {
        backgroundCompletionHandler=h
   }

I have:

/Users/fbartolom/Documents/cocoa applications/iPuja/Classes/GeoreferenceQueue.swift:78:36 Invalid conversion from 'async' function of type '() async -> Void' to synchronous function type '() -> Void'

On the assignment of h to the variable

In practice previously I had an error message, now two...

Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution
 
 
Q