Wil Gieseler
Wil Gieseler

Reputation: 2173

How do I initialize a global variable with @MainActor?

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.

Upvotes: 26

Views: 16405

Answers (1)

Bradley Mackey
Bradley Mackey

Reputation: 7708

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 var 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 var shared = 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.shared

Bug in Swift 5.5 - 5.9 (fixed in 5.10)

@MainActor won't initialize static let variables on the main thread. Use static var instead.

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

This is a bug (see issue 58270) that's been fixed in Swift 5.10.

Upvotes: 21

Related Questions