Living with default isolation controls

I have been playing with concurrency for a while now, after the default isolation controls were introduced. (even got myself a brand new, shiny Mac mini.)

I absolutely hate those controls!

Now, to understand the code, I have to go and find them first and then constantly remember what they are.

I have altogether abandoned them, embracing the practice of annotating the code explicitly.

Does anyone feel the same?

3 Likes

There's precedent already in other languages where build time configuration modifies semantics of the related code, so Swift is definitely not unique in that regard. You don't even really have to look further than TypeScript, where tsconfig.json changes how the type checker interprets your code.

IMO, the reason this even comes up is that (unlike most languages) Swift enforces isolation statically as part of the type system rather than leaving it to the runtime. Very few modern languages have such tradeoffs, so it's not that surprising to me that Swift surfaces such escape hatches. This does come at the cost you mention, where local reasoning about the code gets blurry.

2 Likes

Does this mean you are writing both @MainActor and nonisolated explicitly? And, also, what default did you settle on?

3 Likes

Right, I remember seeing precedents like whether char is signed or unsigned, or whether enums are the same size as int. But it feels like Swift has taken this to a whole new level..

2 Likes

Yes and yes. And nonisolated.

I am using the following to check when in doubt.

Details
@main
enum Driver {
    static func main() async throws {
        IsolationInspector.inspect()
        
        await hibernate (for: 1)
        IsolationInspector.inspect()
        
        await hibernate_nonisolated (for: 1)
        IsolationInspector.inspect()
        
        await hibernate_GA (for: 1)
        IsolationInspector.inspect()
        
        await hibernate_concurrent (for: 1)
        IsolationInspector.inspect()
        
        await ui ()
        IsolationInspector.inspect()
    }
}

private
func hibernate (for seconds: TimeInterval) async {
    IsolationInspector.inspect()
    try! await Task.sleep (until: .now + .seconds (seconds))
    IsolationInspector.inspect()
}

nonisolated(nonsending)
private
func hibernate_nonisolated (for seconds: TimeInterval) async {
    IsolationInspector.inspect()
    try! await Task.sleep (until: .now + .seconds (seconds))
    IsolationInspector.inspect()
}

@concurrent
private
func hibernate_concurrent (for seconds: TimeInterval) async {
    IsolationInspector.inspect()
    try! await Task.sleep (until: .now + .seconds (seconds))
    IsolationInspector.inspect()
}

@GA
private
func hibernate_GA (for seconds: TimeInterval) async {
    IsolationInspector.inspect()
    try! await Task.sleep (until: .now + .seconds (seconds))
    IsolationInspector.inspect()
}

@UI
func ui () async {
    IsolationInspector.inspect()
    await ui_update ()
    IsolationInspector.inspect()
}

@MainActor
func ui_update () async {
    IsolationInspector.inspect()
    await hibernate_nonisolated (for: 1)
    IsolationInspector.inspect()
}

@globalActor
actor GA {
    static let shared = GA()
}

@globalActor
actor UI {
    static let shared = UI()
}
//
//  IsolationInspector.swift

enum IsolationInspector {
    @inline(always)
    nonisolated
    static func inspect (f: String = #function, line: Int = #line, file: String = #file, _ u: (any Actor)?  = #isolation) {
        
        var file = file
        let x = file.startIndex
        let y = file.lastIndex (of: "/")
        if let y {
            file.removeSubrange (x...y)
        }
        let s = "\(file): \(line): \(f)"
        if let u {
            print ("\(s):", ": [\(u)]")
        }
        else {
            print ("\(s): [undefined]")
        }
    }
}
1 Like

In this example code, there are actually numerous declarations that do not have any explicit isolation and are relying on the default. Perhaps especially relevant for the explorations you are doing here, hibernate is picking up the default of nonisolated.

You are, of course, free to not want to rely on any implicit behaviors. That totally up to you. However, I do want to point out that, at least today, an explicit nonisolated is not identical to implicit in all cases. It is not a showstopper, just something to be aware of.

2 Likes

I use the defaults assumed by the bare swiftc compiler. The only thing I explicitly set is Swift 6 language mode (AFAIK the default is Swift 5). I understand that these defaults can change in the future. In this case I am planning to modify my source code if needed.

@MainActor - yes, nonisolated - no.

Please also note that I don't use Swift Concurrency at all. I have to write concurrency-related things in source code just to make it (correctly) compiled.

2 Likes