Unexpected Deinit not on the MainActor

Hi,

I find it very surprising that code referenced inside a @MainActor closure can be deallocated on a different thread.

I have the following bare minimum reproducible example of the issue that I’m facing

import Foundation

final class A {
    func fetchData() async -> String {
        "Hello, World!"
    }

    func fetchData(completion: @escaping @MainActor (String) -> Void) {
        Task {
            let value = await fetchData()
            await completion(value)
        }
    }
}

@MainActor
final class MainThreadDeinit {
    deinit {
        assert(Thread.isMainThread)
    }
}

func doSomething() {
    let mainThreadDeinit = MainActor.assumeIsolated {
        MainThreadDeinit()
    }
    A().fetchData { _ in
        _ = mainThreadDeinit
    }
}

doSomething()

Running this snippet in a playground would crash with

__lldb_expr_84/MyPlayground.playground:19: Assertion failed

From what I can tell, once the completion is awaited inside the Task and executed on the MainActor, the execution resumes back inside the task, on a separate thread. since the task has finished its work, the completion is released on the Task’s current thread, hence the MainThreadDeinit.deinit is called not on the main thread.

My assumption was that, since the function is annotated with @MainActor, it would be released from memory on the main thread.

I expected the compiler to enforce this somehow, either by

  • freeing the objects from memory immediately, on the main thread, right after await completion(value) (since there are no more references to completion in the task)
  • implicitly destroying the object on the main thread, at the end of the task

In case this is actually not a bug, but how it is actually supposed to work, what can I do so that completion is indeed released on the main thread?