Bug in MainActor?

Hi All.

I think this is a possible bug in MainActor, but I can't tell for sure. I have a class marked @MainActor that executes a method in not the Main Thread. However, I have read that actors only work when you play properly with them. Maybe I've broken one of the rules.

Here is a mini example that will run in a Playground:

import UIKit

@MainActor class MiyagiDo {
    func block() {
        print("block \(Thread.current)")
    }
}

class CobraKai:NSObject, UIApplicationDelegate {
    func kick(_ other:MiyagiDo) async throws {
        print("kick \(Thread.current)")
        try await Task.sleep(nanoseconds:1000000000)
        print("After kick \(Thread.current)")
        other.block()
    }
}

Task {
    let sam = await MiyagiDo()
    let robbie = CobraKai()
    try await robbie.kick(sam)
}

When you run this, the output looks like:

kick <NSThread: 0x6000018b4740>{number = 5, name = (null)}
After kick <NSThread: 0x6000018b0600>{number = 7, name = (null)}
block <NSThread: 0x6000018b0600>{number = 7, name = (null)}

Note that block() is running on thread 7 even though MiyagiDo is marked as @MainActor.

Can someone confirm my suspicions? From what I know, the call to other.block() should require an await, but the compiler does not complain, and if I put in an await, it doesn't work anyway.

Calling other.block() doesn't require await because your class CobraKai is implicitly @MainActor since it conforms to UIApplicationDelegate, which is declared a @MainActor protocol. If you delete that conformance, you will see that the compiler will require you to write await other.block().

However, despite the implicit global actor isolation inferred for CobraKai, the behavior here is different than if you explicitly write @MainActor class CobraKai. I suspect this is some oversight; certainly, it'd be a confusing language rule otherwise.

cc @John_McCall

Thanks for the answer.

Since you can write an extension to add protocol conformance to already compiled code, how would that work in regard to @MainActor?

Only code written in the same file where the protocol conformance is declared implicitly inherits that protocol’s global actor isolation, according to the approved proposal.

Hi. We ran into problem with @MainActor as well. I managed to reduce the problem to a small example project that can be found here:

I also opened a bug on bugs.swift.org
[SR-15789] MainActor isolation is not ensured · Issue #58066 · apple/swift · GitHub

@Douglas_Gregor @ktoso can you please take a look? Maybe there's something wrong in how we use @MainActor?

With a help from @zavsby I further simplified test project, all it takes is this code:

class ViewController: UIViewController {
  
  private let presenter: Presenter
  
  required init?(coder: NSCoder) {
    let presenter = Presenter()
    self.presenter = presenter
    super.init(coder: coder)
    presenter.view = self
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
   
    presenter.doSmth()
  }

}

extension ViewController {
  
  @MainActor
  func doSmthOnMain() {
    dispatchPrecondition(condition: .onQueue(.main))
  }
  
}

final class Presenter {
  
  @MainActor weak var view: ViewController?
  
  func doSmth() {
    Task {
      await view?.doSmthOnMain()
    }
  }
  
}

Interestingly, if we remove "final" from Presenter declaration, everything works fine. :thinking: So it seems because of some optimisation main actor isolation is not ensured.