Isolation domain of a Task

I read that for classes without any declared isolations (eg MainActor), the default domain is non isolated. by isolation inference, all its functions are also non isolated. So, im assuming that through isolation inheritance, any Task initialised in these functions would also receive the non-isolated domain. However the docs state that "non-isolated entities cannot access the mutable state of other domains." However, inside Task it is always possible in xcode to access functions from other domains by prepending the call with "await", (eg await UIApplication.shared.open(url)). UiApplication is isolated to the MainActor domain.

Is the Task in a different isolation domain than non isolated, or does the data isolation rules not apply when accessing mutable state asynchronously?

You seems to mixing up a lot of different concepts together. Let's try structuring them a bit.

First of all, non-isolated functions can be different as if they are async or not. Synchronous functions inherit caller isolation by default, asynchronous nonisolated functions run on a generic executor.

Unstructured tasks (Task { /* ... */ }) inherit isolation of a caller, so if you run it inside isolated (say, global actor) context, it will have same isolation. Exception as for right now is isolated parameter, but let's put it aside for now.

Now, inside the task (unstructured in that case, but generally any) you can call anything from other isolation domains, as they ensure execution in that isolation domain, and await make this explicit. So if you create task in non-isolated context and call from there main actor isolated function, it is does not change isolation at all, and required await here highlight that this task is in different domain. If you have task inside same isolated domain, it won't require await:

// Task {} omitted as async functions basically do the same here:
nonisolated func runNonisolated() async {
    await UIApplication.shared.open(url)
}

@MainActor
func runOnMainActor() {
    UIApplication.shared.open(url)
}

Finally, as for mutable state, in your example with open method there is no mutable state involved you access and mutate. What it says, is that, if you have some state (say, progress bar state) in isolation (e.g. on main actor), you cannot mutate it outside this isolation:

@MainActor
final class LoadingViewModel {
    var progress: Double?
}

nonisolated func callNonisolated(viewModel: LoadingViewModel) async {
    viewModel.progress = 10.0  // error: main actor-isolated property 'progress' can not be mutated from a non-isolated context
}

But you can allow mutation inside isolation domain:

extension LoadingViewModel {
    func update(progress: Double?) { 
        self.progress = progress
    }
}

nonisolated func callNonisolated(viewModel: LoadingViewModel) async {
    await viewModel.update(progress: 10.0)
}

Now isolation of LoadingViewModel to MainActor ensures that update of progress happens on main actor, and enforces await on the call side from different isolation. That is true if we have simply different isolation:

actor X {
    func notify(vm: LoadingViewModel) async {
        await vm.update(progress: nil)
    }

    // or

    func notifyAndForget(vm: LoadingViewModel) {
        Task { await vm.update(progress: nil) }
    }

    // or even explicitly isolate Task to main actor

    func notifyWithExplicitMainActor(vm: LoadingViewModel) {
        Task { @MainActor in 
            vm.update(progress: nil)  // note that here we don't need await anymore
        }
    }
}
2 Likes

Thank you!!
Am I correct to say that the await keyword is the magic that does the job of ensuring that the execution of isolated methods is being done in its respective domains?
Is the keyword strictly required for the Task? or is it just for the coders benefit to know that the call is happening in a different domain? (ie are Tasks equivalent to using await keyword)
As well, is it possible to do something like await viewModel.progress = 10.0 or would the compiler raise an error like the one in your example?

I see you having hard time understand basics of concurrency for now, so I suggest start with book chapter on concurrency and WWDC sessions starting few prior years to current on this topic as well. There such a lot of things to write here to get proper explanation on this questions, that these resources will give you much more. WWDC sessions particularly do a great job describing isolation domains and tasks, and how all this work together. If you still have questions after that, reach out here for the help, but as for now any explanation here will be incomplete.

The only thing I have read so far is the Data Race Safety Chapter in Swift. And there was not anything in it about async/await, so im not sure how the isolated domains and the async work together. But if async/await is important for data race safety, which it appears to be, why did they not mention it in that chapter?

Data Race Safety is a bad place to start. It greatly covers more advance topics, and is written with assumption that the reader has general understanding of how Swift Concurrency works and aims to provide more concrete cases that arise during migration to Swift 6 (as it is part of migration guide). It provides valuable insights when you feel confident with main concepts and has used Swift Concurrency already, and now prepare codebase for new version. Start with the link in my previous post above to the Concurrency chapter in the book, and then reach out for WWDC, you'll have all these topics covered there.

2 Likes