How to deal with/silence this strict concurrency warning?

// in my library code:
extension Util {
    static public var enableRemoteDebugging = false

    static public func remoteDebug(msg: String) {
        guard enableRemoteDebugging else { return }
        ...
    }
}

// In my main program, in some code that runs very early in the program's life cycle:
     Util.enableRemoteDebugging = true

Ok, I'm going to get warnings about mutating and accessing global state, obviously it's unsafe.

  1. In actual practice, the state will only be mutated from its default at most once, early in the program's lifetime before I would have started tasks.

  2. Is there any way to formalize (1) given our current language constructs so that it's not just "trust me, it'll be ok, I won't do bad things."

  3. If the answer to 2 is "nope", what's the easiest way to tell the compiler not to warn me? I don't want to isolate this to an actor or lock, because I do want to allow remoteDebug() to be called from any Task and why pay the price for forcing a hop to some particular actor or grabbing a lock, simply to satisfy the compiler?

1 Like

nonisolated(unsafe) - this article is a decent overview of the issue

(edit: re-reading your Q2 above maybe you already know this and are asking for something safer; leaving this post anyway)

You probably want something like Rust's sync::Once container (maybe there's a library that implements an analogue for Swift) (edit: this is precisely the behaviour of static let), or, if your value is Sendable, a generally viable solution that avoids actor hops, locks etc., while also allowing you to mutate the variable more than once is using atomics — particularly atomically swapping pointers to boxed values.

Very strictly speaking, the problem with such code is not only in ensuring that you're mutating the value once (this really shouldn't be an issue especially in the case that your value is a Bool), but that there's, in the general case, no way to ensure that the code which depends on this "happens-before" relationship actually observes your flag being set before it runs.

I can't think of a good way to model this situation in the general case, aside from using nonisolated(unsafe), though you could at least use access control to the setter to limit where the mutation occurs like

static public private(set) nonisolated(unsafe) var enableRemoteDebugging = false

so that only code in the same declaration has write access to the variable.

If the initialization is simple enough, and doesn't have time constraints on when or what context it can occur in, you could take advantage of lazy one-initialization of the global and move the initialization into the global directly:

static public nonisolated let enableRemoteDebugging = isRemoteDebuggingEnabled()

which will ensure that the value is only computed once and that the variable is otherwise immutable.

5 Likes

Thanks for your suggestions, and the general education on nonisolated(unsafe).

I realized that it can be made entirely safe, without needing to lock in remoteDebug() (which is is likely to be called a lot), by the following pattern:

extension RemoteDebugging {
    static private var enabledDefaultValue = false
    static private var enabledDefaultValueRead = false
    
    static private let enableDebugging = readEnabledDefaultValue()
    
    static public func remoteDebug(msg: String) {
        guard enableDebugging else { return }       // no lock needed here!
        /* code */
    }
    
    // Documentation: Call this function early in your program's life cycle.
    // If it returns false, it has no effect because someone issued a call
    // to remoteDebug() prior to your calling this function.
    static public func enableRemoteDebugging() -> Bool {
        return withSomeLock {
            guard !enabledDefaultValueRead else {
                print("*** DIRE WARNING: YOU CALLED THIS TOO LATE AND IT HAS NO EFFECT NOW!!! ***")
                return false
            }
            enabledDefaultValue = true
            return true
        }
    }

    static private func readEnabledDefaultValue() -> Bool {
        return withSomeLock {
            enabledDefaultValueRead = true
            return enabledDefaultValue
        }
    }
}

While this is silly in the case of remote debugging (it's not so bad to take a lock for each call, given how much code will likely execute), a better example might be some bounds-checking logic, where you cannot afford a lock and just want to look at a global variable. If we ensure that the call to access the default and the call to set the default are locked, and we make use of lazy initialization, we do indeed get a guarantee of lock protection around the mutation, and no lock at runtime when we call our routine a lot.

Edit: um, well, if global initialization is lazy, does that mean that the function remoteDebug() has a lock in it anyway that I can't see?

In a sense, though it's one heavily optimized for the one-time initialization use case. It uses dispatch_once under the hood, which does shenanigans to ensure that synchronization only occurs in the case where the initialization hasn't happened yet; once the initialization has completed, the check is just a non-atomic load and comparison against zero before accessing the value.

For a single boolean flag, it might still ultimately be more efficient to use an Atomic<Bool> global once that type is accepted and shipped in a release of the Swift runtime.

2 Likes