|
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() |
660 | 654 |
|
661 |
| - _ = await Gene(fromPieces: ns.getPieces()) |
| 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 |
| 658 | + |
| 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 |
|
@@ -773,7 +755,24 @@ var y = x + 2
|
773 | 755 | #### Removing Redundant Isolation
|
774 | 756 |
|
775 | 757 | Global-actor isolation on a stored property provides safe concurrent access to the storage occupied by that stored property in the type's instances.
|
776 |
| -For example, if `pid` is an actor-isolated stored property (i.e., one without an observer or property wrapper), then the access `p.pid.reset()` only protects the memory read of `pid` from `p`, and not the call to `reset` afterwards. Thus, for value types (enums and structs), global-actor isolation on those stored properties fundamentally serves no use: mutations of the storage occupied by the stored property in a value type are concurrency-safe by default, thanks to copy-on-write semantics. So, we propose to remove the requirement that access to those properties are protected by isolation. That is, reading or writing those stored properties do not require an `await`. |
| 758 | +For example, if `pid` is an actor-isolated stored property (i.e., one without an observer or property wrapper), then the access `p.pid.reset()` only protects the memory read of `pid` from `p`, and not the call to `reset` afterwards. Thus, for value types (enums and structs), global-actor isolation on those stored properties fundamentally serves no use: mutations of the storage occupied by the stored property in a value type are concurrency-safe by default, because mutable variables cannot be shared between tasks. For example, it is error when trying to capture a mutable var in a Sendable closure: |
| 759 | + |
| 760 | +```swift |
| 761 | +@MainActor |
| 762 | +struct StatTracker { |
| 763 | + var count = 0 |
| 764 | + |
| 765 | + mutating func update() { |
| 766 | + count += 1 |
| 767 | + } |
| 768 | +} |
| 769 | + |
| 770 | +var st = StatTracker() |
| 771 | +Task { await st.update() } // error: mutation of captured var 'st' in concurrently-executing code |
| 772 | +``` |
| 773 | + |
| 774 | +As a result, there is no way to concurrently mutate the memory of a struct, regardless of whether the stored properties of the struct are isolated to a global actor. Whether the instance can be shared only depends on whether it's var-bound or not, and the only kind of sharing permitted is via copying. Any mutations of reference types stored _within_ the struct require the usual actor-isolation applied to that reference type itself. In other words, applying global-actor isolation to a stored property containing a class type does _not_ protect the members of that class instance from concurrent access. |
| 775 | +So, we propose to remove the requirement that access to those properties are protected by isolation. That is, accessing those stored properties do not require an `await`. |
777 | 776 |
|
778 | 777 | The [global actors](0316-global-actors.md) proposal explicitly excludes actor types from having stored properties that are global-actor isolated. But in Swift 5.5, that is not enforced by the compiler. We feel that the rule should be enforced, i.e., the storage of an actor should uniformly be isolated to the actor instance. One benefit of this rule is that it reduces the possibility of [false sharing](https://en.wikipedia.org/wiki/False_sharing) among threads. Specifically, only one thread will have write access the memory occupied by an actor instance at any given time.
|
779 | 778 |
|
@@ -871,6 +870,21 @@ One idea for working around the inability to synchronize from a `deinit` with th
|
871 | 870 |
|
872 | 871 | 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 | 872 |
|
| 873 | +### Requiring Sendable arguments only for delegating initializers |
| 874 | + |
| 875 | +Delegating initializers are conceptually a good place to construct a fresh |
| 876 | +instance of a non-Sendable value to pass-along to the actor during initialization. |
| 877 | +This could only work by saying that only `nonisolated self` delegating initializers |
| 878 | +can accept a non-Sendable value from any context. But also, an initializer's |
| 879 | +delegation status must now be published in the interface of a type, i.e., |
| 880 | +some annotation like `convenience` is required. Eliminating the need for |
| 881 | +`convenience` was chosen over non-Sendable values for a specific kind of delegating |
| 882 | +initializer for a few reasons: |
| 883 | + |
| 884 | +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. |
| 885 | +2. Requiring `convenience` for just one narrow use-case is not worth it. |
| 886 | +3. Static functions, using a factory pattern, can replace the need for initializers with non-Sendable arguments callable from anywhere. |
| 887 | + |
874 | 888 | ## Effect on ABI stability
|
875 | 889 |
|
876 | 890 | This proposal does not affect ABI stability.
|
|
0 commit comments