swift 如何使用@MainActor初始化全局变量?

nbnkbykc  于 2022-12-10  发布在  Swift
关注(0)|答案(1)|浏览(160)

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.

4ngedf3f

4ngedf3f1#

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.6, 5.7

TL;DR: @MainActor sometimes won't call static let variables on the main thread. Use private(set) 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
func bar() {
  print("bar is main", Thread.isMainThread)
}

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

This is a bug (see issue 58270 ).
A workaround is to always use a private(set) static var instead of a static let , as the correct isolation behaviour is enforced in that case.

相关问题