|
31 | 31 | - [Syntactic Form](#syntactic-form)
|
32 | 32 | - [Isolation](#isolation)
|
33 | 33 | - [Sendability](#sendability)
|
34 |
| - - [Delegation and Sendable](#delegation-and-sendable) |
35 | 34 | - [Deinitializers](#deinitializers)
|
36 | 35 | - [Global-actor isolation and instance members](#global-actor-isolation-and-instance-members)
|
37 | 36 | - [Removing Redundant Isolation](#removing-redundant-isolation)
|
|
40 | 39 | - [Introducing `nonisolation` after `self` is fully-initialized](#introducing-nonisolation-after-self-is-fully-initialized)
|
41 | 40 | - [Permitting `await` for property access in `nonisolated self` initializers](#permitting-await-for-property-access-in-nonisolated-self-initializers)
|
42 | 41 | - [Async Actor Deinitializers](#async-actor-deinitializers)
|
| 42 | + - [Requiring Sendable arguments only for delegating initializers](#requiring-sendable-arguments-only-for-delegating-initializers) |
43 | 43 | - [Effect on ABI stability](#effect-on-abi-stability)
|
44 | 44 | - [Effect on API resilience](#effect-on-api-resilience)
|
45 | 45 | - [Acknowledgments](#acknowledgments)
|
@@ -598,114 +598,96 @@ But, the delegating initializers of an actor have simpler rules about what can a
|
598 | 598 |
|
599 | 599 | ### Sendability
|
600 | 600 |
|
601 |
| -The delegating initializers of an `actor`, and all initializers of a GAIT, follow the same rules about Sendable arguments as other functions. Namely, if the function is isolated, then cross-actor calls require that the arguments conform to the `Sendable` protocol. |
602 |
| - |
603 |
| -All non-delegating initializers of an actor, regardless of any flow-sensitive isolation applied to `self`, are considered "isolated" from the `Sendable` point-of-view. That's because these initializers are permitted to access the actor's isolated stored properties during bootstrapping. |
604 |
| - |
605 |
| -These two rules force programmers to correctly deal with `Sendable` values when creating a new actor instance. Fundamentally, programmers will have only two options for initializing a non-`Sendable` stored property of an actor: |
| 601 | +When passing values to any of an `actor`'s initializers, from outside of that actor, those values must be `Sendable`. |
| 602 | +Thus, during the initialization of a new instance, the actor's "boundary" in terms of Sendability begins at the initial call-site to one of its initializers. |
| 603 | +This rule forces programmers to correctly deal with `Sendable` values when creating a new actor instance. Fundamentally, programmers will have only two options for initializing a non-`Sendable` stored property of an actor: |
606 | 604 |
|
607 | 605 | ```swift
|
608 | 606 | class NotSendableType { /* ... */ }
|
609 | 607 | struct Piece: Sendable { /* ... */ }
|
610 | 608 |
|
611 | 609 | actor Greg {
|
612 |
| - var ns: NonSendableType |
| 610 | + var ns: NotSendableType |
613 | 611 |
|
614 |
| - // Option 1: a non-delegating init can only take |
615 |
| - // Sendable values and use them to construct |
616 |
| - // a new non-Sendable value. |
| 612 | + // Option 1: an initializer that can be called from anywhere, |
| 613 | + // because its arguments are Sendable. |
617 | 614 | init(fromPieces ps: (Piece, Piece)) {
|
618 |
| - self.ns = NonSendableType(ps) |
| 615 | + self.ns = NotSendableType(ps) |
619 | 616 | }
|
620 | 617 |
|
621 |
| - // Option 2: a delegating and nonisolated-self init |
622 |
| - // can take a non-Sendable value and allow you to |
623 |
| - // pass the Sendable pieces to a non-delegating init. |
624 |
| - init(with ns: NonSendableType) { |
| 618 | + // Option 2: an initializer that can only be delegated to, |
| 619 | + // because its arguments are not Sendable. |
| 620 | + init(with ns: NotSendableType) { |
625 | 621 | self.init(fromPieces: ns.getPieces())
|
626 | 622 | }
|
627 | 623 | }
|
628 | 624 | ```
|
629 | 625 |
|
630 |
| -As shown in the example above, you _can_ construct an actor that has a non-`Sendable` stored property. But, you must be able to create a new instance of that type from `Sendable` pieces of data. The two options above provide ways to either accept the pieces directly in a non-delegating initializer, or to rely on a delegating initializer to start with non-`Sendable` values. This effectively forces programmers to construct a new, fresh instance of the non-Sendable value within the non-delegating initializer. |
631 |
| - |
632 |
| -#### Delegation and Sendable |
633 |
| - |
634 |
| -It's tempting to think that all delegating initializers can accept non-Sendable values from any caller, but that's not true. Whether it is safe to pass a non-Sendable value to a delegating initializer still depends on the isolation of the caller. |
635 |
| - |
636 |
| -For example, an `async` delegating initializer has an `isolated self`, so it has access to the stored properties after delegating. If we were to allow non-Sendable values into this initializer _without_ paying attention to the isolation of its caller, then an invalid sharing of non-Sendable values can happen: |
| 626 | +As shown in the example above, you _can_ construct an actor that has a non-`Sendable` stored property. But, you should create a new instance of that type from `Sendable` pieces of data in order to store it in the actor instance. Once inside an actor's initializer, non-Sendable values can be freely passed when delegating to another initializer, or calling its methods, *etc*. The following example illustrates this rule: |
637 | 627 |
|
638 | 628 | ```swift
|
639 | 629 | class NotSendableType { /* ... */ }
|
640 | 630 | struct Piece: Sendable { /* ... */ }
|
641 | 631 |
|
642 | 632 | actor Gene {
|
643 |
| - var ns: NonSendableType? |
| 633 | + var ns: NotSendableType |
644 | 634 |
|
645 |
| - init(with ns: NonSendableType) async { |
646 |
| - self.init() |
| 635 | + init(_ ns: NotSendableType) { |
647 | 636 | self.ns = ns
|
648 | 637 | }
|
649 | 638 |
|
| 639 | + init(with ns: NotSendableType) async { |
| 640 | + self.init(ns) // ✅ non-Sendable is OK during initializer delegation... |
| 641 | + someMethod(ns) // ✅ and when calling a method from an initializer, etc. |
| 642 | + } |
| 643 | + |
650 | 644 | init(fromPieces ps: (Piece, Piece)) async {
|
651 |
| - let ns = NonSendableType(ps) |
652 |
| - await self.init(with: ns) // ✅ OK |
653 |
| - assert(self.ns == ns) |
| 645 | + let ns = NotSendableType(ps) |
| 646 | + await self.init(with: ns) // ✅ non-Sendable is OK during initializer delegation |
654 | 647 | }
|
| 648 | + |
| 649 | + func someMethod(_: NotSendableType) { /* ... */ } |
655 | 650 | }
|
656 | 651 |
|
657 |
| -func someFunc(ns: NonSendableType) async { |
658 |
| - let ns = NonSendableType() |
659 |
| - _ = await Gene(with: ns) // ❌ error: cannot pass non-Sendable value across actors |
| 652 | +func someFunc() async { |
| 653 | + let ns = NotSendableType() |
| 654 | + |
| 655 | + _ = Gene(ns) // ❌ error: cannot pass non-Sendable value across actor boundary |
| 656 | + _ = await Gene(with: ns) // ❌ error: cannot pass non-Sendable value across actor boundary |
| 657 | + _ = await Gene(fromPieces: ns.getPieces()) // ✅ OK because (Piece, Piece) is Sendable |
660 | 658 |
|
661 |
| - _ = await Gene(fromPieces: ns.getPieces()) |
| 659 | + _ = await SomeGAIT(isolated: ns) // ❌ error: cannot pass non-Sendable value across actor boundary |
| 660 | + _ = await SomeGAIT(secondNonIso: ns) // ❌ error: cannot pass non-Sendable value across actor boundary |
662 | 661 | }
|
663 | 662 | ```
|
664 | 663 |
|
665 |
| -In the example above, both `Gene.init(with:)` and `Gene.init(fromPieces:)` are both delegating initializers that are `isolated self`. The difference is that `init(with:)` takes a non-`Sendable` argument, whereas `init(fromPieces:)` only takes `Sendable` arguments. It would not be safe to permit a call from `someFunc` to `init(with:)` because that would mean passing a non-`Sendable` value across actors. But, it's OK to delegate from `init(fromPieces:)` to `init(with:)` because the isolation is matching! |
| 664 | +For a global-actor isolated type (GAIT), the same rule applies to its `nonisolated` initializers. Thus, upon entering such an initializer from outside of the actor, the values must be `Sendable`. The differences from an `actor` are that: |
666 | 665 |
|
667 |
| -<!-- |
668 |
| -Here's another more exhaustive example to show all the different corner-cases: |
| 666 | +1. The caller of the first initializer may already be isolated to the global-actor, so there is no `Sendable` barrier (as usual). |
| 667 | +2. When delegating from a `nonisolated` initializer to one that is isolated to the global actor, the value must be `Sendable`. |
669 | 668 |
|
670 |
| -```swift |
671 |
| -class NotSendableType { /* ... */ } |
| 669 | +The second difference only manifests when a `nonisolated` and `async` initializer delegates to an isolated initializer of the GAIT: |
672 | 670 |
|
673 |
| -actor George { |
674 |
| - var ns: NonSendableType |
| 671 | +```swift |
| 672 | +@MainActor |
| 673 | +class SomeGAIT { |
| 674 | + var ns: NotSendableType |
675 | 675 |
|
676 |
| - init(anyNonDelegating ns: NonSendableType) { |
| 676 | + init(isolated ns: NotSendableType) { |
677 | 677 | self.ns = ns
|
678 | 678 | }
|
679 | 679 |
|
680 |
| - init(delegatingAsync ns: NonSendableType) async { |
681 |
| - self.init(anyNonDelegating: ns) // ✅ OK |
682 |
| - self.ns = ns // ✅ OK |
683 |
| - } |
684 |
| -
|
685 |
| - // ^^^ the above can only be delegated to from another init |
686 |
| - // --- |
687 |
| - // vvv the below can be called from outside the actor |
688 |
| -
|
689 |
| - init(delegatingSync ns: NonSendableType) { |
690 |
| - self.init(anyNonDelegating: ns) // ❌ error: cannot pass non-Sendable value across actors |
691 |
| - self.ns = ns // ❌ error: cannot mutate isolated property from nonisolated context |
| 680 | + nonisolated init(firstNonIso ns: NotSendableType) async { |
| 681 | + await self.init(isolated: ns) // ❌ error: cannot pass non-Sendable value across actor boundary |
692 | 682 | }
|
693 | 683 |
|
694 |
| - nonisolated init(delegatingNonIsoAsync ns: NonSendableType) async { |
695 |
| - self.init(anyNonDelegating: ns) // ❌ error: cannot pass non-Sendable value across actors |
696 |
| - self.ns = ns // ❌ error: cannot mutate isolated property from nonisolated context |
| 684 | + nonisolated init(secondNonIso ns: NotSendableType) async { |
| 685 | + await self.init(firstNonIso: ns) // ✅ |
697 | 686 | }
|
698 | 687 | }
|
699 |
| -
|
700 |
| -func someUnrelatedCaller(ns: NonSendableType) async { |
701 |
| - _ = George(anyNonDelegating: ns) // ❌ error: cannot pass non-Sendable value across actors |
702 |
| - _ = await George(delegatingAsync: ns) // ❌ error: cannot pass non-Sendable value across actors |
703 |
| - _ = George(delegatingSync: ns) // ✅ OK |
704 |
| - _ = await George(delegatingNonIsoAsync: ns) // ✅ OK |
705 |
| -} |
706 | 688 | ```
|
707 |
| ---> |
708 | 689 |
|
| 690 | +The barrier in the example above can be resolved by removing the `nonisolated` attribute, so that the initializer has a matching isolation. |
709 | 691 |
|
710 | 692 | ### Deinitializers
|
711 | 693 |
|
@@ -871,6 +853,21 @@ One idea for working around the inability to synchronize from a `deinit` with th
|
871 | 853 |
|
872 | 854 | The primary danger here is that it is currently undefined behavior in Swift for a reference to `self` to escape a `deinit` and persist after the `deinit` has completed, which must be possible if the `deinit` were asynchronous. The only other option would be to have `deinit` be blocking, but Swift concurrency is designed to avoid blocking.
|
873 | 855 |
|
| 856 | +### Requiring Sendable arguments only for delegating initializers |
| 857 | + |
| 858 | +Delegating initializers are conceptually a good place to construct a fresh |
| 859 | +instance of a non-Sendable value to pass-along to the actor during initialization. |
| 860 | +This could only work by saying that only `nonisolated self` delegating initializers |
| 861 | +can accept a non-Sendable value from any context. But also, an initializer's |
| 862 | +delegation status must now be published in the interface of a type, i.e., |
| 863 | +some annotation like `convenience` is required. Eliminating the need for |
| 864 | +`convenience` was chosen over non-Sendable values for a specific kind of delegating |
| 865 | +initializer for a few reasons: |
| 866 | + |
| 867 | +1. The rules for Sendable values and initializers would become complex, being dependent on three factors: delegation status, isolation of `self`, and the caller's context. The usual Sendable rules only depend on two. |
| 868 | +2. Requiring `convenience` for just one narrow use-case is not worth it. |
| 869 | +3. Static functions, using a factory pattern, can replace the need for initializers with non-Sendable arguments callable from anywhere. |
| 870 | + |
874 | 871 | ## Effect on ABI stability
|
875 | 872 |
|
876 | 873 | This proposal does not affect ABI stability.
|
|
0 commit comments