swiftswift-concurrency

Difference between starting a detached task and calling a nonisolated func in main actor


Imagine I'm in a class annotated with @MainActor so that all the functions are tied to the main actor. I'm trying to understand what the difference is between doing the following:

func bar() {
  Task.detached(priority: .background) {
    await foo()
  }
}

func foo() async {
  ...
}

vs

func bar() {
  Task(priority: .background) {
    await foo()
  }
}

nonisolated func foo() async {
  ...
}

Are they the same?


Solution

  • You said:

    Imagine I'm in a class annotated with @MainActor so that all the functions are tied to the main actor. I'm trying to understand what the difference is between doing the following:

    func bar() {
        Task.detached(priority: .background) {
            await foo()
        }
    }
    
    func foo() async {
        …
    }
    

    The bar method creates a non-structured task, which, because it is a detached task, is not on the current actor. But that task will await the foo method, which will run on the main actor. The fact that foo is in a class bearing the @MainActor designation means that it is isolated to that actor, regardless of which Swift concurrency context you invoke it.

    But, in this case, there is little point in launching a detached task that only awaits an actor-isolated function.

    You continue:

    vs

    func bar() {
        Task(priority: .background) {
            await foo()
        }
    }
    
    nonisolated func foo() async {
        …
    }
    

    In this case, bar will launch a task on the current (main) actor, but it will await the result of foo, which is nonisolated (i.e., not isolated to the main actor).

    So, the difference is that the first example, foo is actor-isolated, but it is not in the second example.


    Effective Swift 5.7 (by virtue of SE-0338), Apple has formalized the behavior here, namely that “async functions that are not actor-isolated should formally run on a generic executor associated with no actor” (i.e., if invoked from the main actor, the nonisolated async function will not run on the main thread.

    Consider:

    func bar() {
        Task {
            await foo()
        }
    }
    
    nonisolated func foo() async {
        …
    }
    

    This will achieve what you want. In this case, even if bar is isolated to the main actor (and thus its Task is also isolated to the main actor), foo will not run on the main actor. This pattern is a nice simple way of getting work off the main thread. And you do not need to introduce detached tasks.

    Now, in the interest of full disclosure, in Swift 6, there is a further refinement to this rule (by virtue of SE-0444) where the caller can optionally specify a preferred executor for nonisolated async functions (avoiding unnecessary context switches in some edge cases), but that is not applicable in your case.

    But this is a digression: In general, if calling something from the main actor and you want to get it off the main thread, a nonisolated async function will do the job. (Or, obviously, you could create your own actor type, and its async functions will be isolated to that actor, and will not run on the main thread, either.)