Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MainActor isolation is broken by conforming to an async protocol #68487

Closed
amarantedaniel opened this issue Sep 13, 2023 · 2 comments
Closed
Labels
actor isolation Feature → concurrency: Actor isolation bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler itself concurrency Feature: umbrella label for concurrency language features conformances Feature → protocol: protocol conformances duplicate Resolution: Duplicates another issue SILGen Area → compiler: The SIL generation stage swift 5.8 unexpected behavior Bug: Unexpected behavior or incorrect output

Comments

@amarantedaniel
Copy link

Description
When a protocol with async functions is marked with the MainActor annotation, any implementation of this protocol that doesn't mark the functions as async as well when conforming to the protocol, will not execute on the MainActor.

Steps to reproduce
Given the following protocol and implementation:

@MainActor
protocol MyMainActorProtocol {
    func doStuff() async
}

struct MyMainActorStruct: MyMainActorProtocol {
    nonisolated init() {}
    func doStuff()  {
        print(">>> \(Thread.isMainThread)")
    }
}

When the following code is executed:

Task {
    let dependency: MyMainActorProtocol = MyMainActorStruct()
    print(">>> \(Thread.isMainThread)")
    await dependency.doStuff()
}

Both print statements run in a background thread.

But with the following adjustments of either

@MainActor
protocol MyMainActorProtocol {
    func doStuff()
}

struct MyMainActorStruct: MyMainActorProtocol {
    nonisolated init() {}
    func doStuff()  {
        print(">>> \(Thread.isMainThread)")
    }
}

or

@MainActor
protocol MyMainActorProtocol {
    func doStuff() async
}

struct MyMainActorStruct: MyMainActorProtocol {
    nonisolated init() {}
    func doStuff() async  {
        print(">>> \(Thread.isMainThread)")
    }
}

We get the correct result, where the body of the function inside the struct runs in the main thread.

Expected behavior
The expected behavior would be that any code that runs on the doStuff function in MyMainActorStruct would run on the main thread, as per the global actor inference rules

A non-actor type that conforms to a global-actor-qualified protocol within the same source file as its primary definition infers actor isolation from that protocol

Environment

  • Swift compiler version info swift-driver version: 1.75.2 Apple Swift version 5.8 (swiftlang-5.8.0.124.2 clang-1403.0.22.11.100)
    Target: arm64-apple-macosx13.0
  • Xcode version info Build version 14E222b
  • Deployment target: iOS 16.4

Additional context
I have posted this question on both stackoverflow and the swift foruns and was asked to create an issue here (there's more information there).
https://forums.swift.org/t/weird-behavior-in-mainactor/67243
https://stackoverflow.com/questions/77089023/why-is-the-following-code-not-running-on-the-main-thread

@amarantedaniel amarantedaniel added bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. triage needed This issue needs more specific labels labels Sep 13, 2023
@ktoso ktoso added concurrency Feature: umbrella label for concurrency language features and removed triage needed This issue needs more specific labels labels Sep 13, 2023
@hborla
Copy link
Member

hborla commented Jan 23, 2024

This is the only way I can reproduce the issue now (and this example eliminates all dependencies except the standard library):

@MainActor
protocol MyMainActorProtocol {
  func doStuff() async
}

struct MyMainActorStruct: MyMainActorProtocol {
  nonisolated init() {}
  func doStuff()  {
    MainActor.assertIsolated()
  }
}

await Task.detached {
  let dependency: MyMainActorProtocol = MyMainActorStruct()

  await (MyMainActorStruct() as any MyMainActorProtocol).doStuff() // Trips the `MainActor.assertIsolated()` call

  await (MyMainActorStruct()).doStuff() // This line is fine
}.value

I think the the type checking here is fine. The issue here seems to be that the witness thunk for MyMainActorProtocol.doStuff does not hop to the main actor before calling MyMainActorStruct.doStuff.

@hborla
Copy link
Member

hborla commented Jan 23, 2024

Duplicate of #62394

@hborla hborla marked this as a duplicate of #62394 Jan 23, 2024
@hborla hborla closed this as completed Jan 23, 2024
@AnthonyLatsis AnthonyLatsis added duplicate Resolution: Duplicates another issue compiler The Swift compiler itself SILGen Area → compiler: The SIL generation stage conformances Feature → protocol: protocol conformances actor isolation Feature → concurrency: Actor isolation unexpected behavior Bug: Unexpected behavior or incorrect output swift 5.8 labels Jan 24, 2024
@AnthonyLatsis AnthonyLatsis closed this as not planned Won't fix, can't repro, duplicate, stale Jan 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
actor isolation Feature → concurrency: Actor isolation bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler itself concurrency Feature: umbrella label for concurrency language features conformances Feature → protocol: protocol conformances duplicate Resolution: Duplicates another issue SILGen Area → compiler: The SIL generation stage swift 5.8 unexpected behavior Bug: Unexpected behavior or incorrect output
Projects
None yet
Development

No branches or pull requests

4 participants