[SOLVED] How do I initialize a global variable with @MainActor?

Issue

I would like to have some sort of global variable that is synchronized using @MainActor.

Here’s an example struct:

@MainActor
struct Foo {}

I’d like to have a global variable something like this:

let foo = Foo()

However, this does not compile and errors with Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context.

Fair enough. I’ve tried to construct it on the main thread like this:

let foo = DispatchQueue.main.sync {
    Foo()
}

This compiles! However, it crashes with EXC_BAD_INSTRUCTION, because DispatchQueue.main.sync cannot be run on the main thread.

I also tried to create a wrapper function like:

func syncMain<T>(_ closure: () -> T) -> T {
    if Thread.isMainThread {
        return closure()
    } else {
        return DispatchQueue.main.sync(execute: closure)
    }
}

and use

let foo = syncMain {
    Foo()
}

But the compiler does not recognize if Thread.isMainThread and throws the same error message again, Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context.

What’s the right way to do this? I need some kind of global variable that I can initialize before my application boots.

Solution

One way would be to store the variable within a container (like an enum acting as an abstract namespace) and also isolating this to the main actor.

@MainActor
enum Globals {
    static let foo = Foo()
}

An equally valid way would be to have a "singleton-like" static property on the object itself, which serves the same purpose but without the additional object.

@MainActor
struct Foo {
    static let global = Foo()
}

You now access the global object via Foo.global.

One thing to note is that this will now be lazily initialized (on the first invocation) rather than immediately initialized.
You can however force an initialization early on by making any access to the object.

// somewhere early on
_ = Foo.global

Bug in Swift 5.5 and Swift 5.6

TL;DR: @MainActor sometimes won’t call static let variables on the main thread. Use static var instead.

While this compiles and works, it appears that this may call the initializer off the main thread and subsequently any calls made inside the initializer.

@MainActor
struct Bar {
    init()  {
        print("bar init is main", Thread.isMainThread)
    }
    func barCall() {
        print("bar call is main", Thread.isMainThread)
    }
}


@MainActor
struct Foo {
    @MainActor
    static let global = Foo()
    
    init() {
        print("foo init is main", Thread.isMainThread)
        let b = Bar()
        b.barCall()
    }
    
    func fooCall() {
        print("foo call is main", Thread.isMainThread)
    }
}
Task.detached {
    await Foo.global.fooCall()
}
// prints:
// foo init is main false
// bar init is main false
// bar call is main false
// foo call is main true

This is a bug (see SR-16009).

A workaround is to always ensure initalization takes place in a @MainActor context, which we can do by annotating the closure where we first call.

Task.detached { @MainActor in
    await Foo.global.fooCall()
}

Alternatively, you can use a static var instead of a static let, as the correct isolation behaviour is enforced in that case.

Answered By – Bradley Mackey

Answer Checked By – Terry (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published.