swiftlang / swift

The Swift Programming Language
https://swift.org
Apache License 2.0
67.66k stars 10.38k forks source link

Surprising actor deinitialization #77365

Open ABridoux opened 3 weeks ago

ABridoux commented 3 weeks ago

Description

I have been using Xcode 16 for a few weeks now and noticed a behavior regarding actor deinitialization that surprises me.

In our app, we use a few actors that are stored as constant static properties and thus live as long as the app is not killed. Compiling with Xcode 16, we have noticed that capturing the actor as unowned lead to a crash in several cases, like a Task initialization (even though for a Task, capturing self as unowned is unneeded most of the time).

Reproduction

To exemplify this, here's a block of code for a command-line tool.

// MARK: - Static

enum Storage {
    static let shared = SharedActor()
}

// MARK: - SharedActor

actor SharedActor {

    // MARK: Properties

    var task: Task<Void, Error>?
    var value = 1

    // MARK: Init

    deinit {
        print ("DEINIT")
    }

    // MARK: Test

    func test() {
        task = Task { [unowned self] in
            print ("Task execution")
            print (value)
        }
    }
}

// MARK: - Run

await Storage.shared.test()
try await Task.sleep(for: .seconds(1))
print("Bye-Bye")

Here's what is printed in the console.

Task execution
1
DEINIT
Bye-Bye

Expected behavior

The deinit block should not be called, as the actor is stored as a static constant property and thus should live as long as the application or command-line tool lives.

Environment

swift-driver version: 1.115 Apple Swift version 6.0.2 (swiftlang-6.0.2.1.2 clang-1600.0.26.4) Target: arm64-apple-macosx15.0

[!Note] I never had the issue before using Xcode 16.

Additional information

The behavior differs when a class is used instead of an actor, which lead me to think this might be a bug. The issue is mentioned in the Swift Forums.

jamieQ commented 3 weeks ago

here's a slightly reduced reproduction that can be run as a command line tool:

@main
struct S {
  static func main() async {
    await SharedActor.shared.test()
  }

  actor SharedActor {
    static let shared = SharedActor()

    deinit {
      print("DEINIT")
    }

    func test() {
      Task { [unowned self] in
        _ = self
      }
    }
  }
}

facts of note:

  1. self must actually be used in the Task closure or the issue appears to go away
  2. the specific capture list format [unowned self] also appears needed. aliasing self in various ways and capturing the alias seems to prevent the issue from arising.
  3. i was wondering if the changes in https://github.com/swiftlang/swift/pull/77031 may have had some effect on this behavior, but the latest nightly compiler snapshots appear to now crash on this example (2024-10-28, 6.0.2 snapshot). reported here.