- Proposal: SE-0302
- Authors: Chris Lattner, Doug Gregor
- Review Manager: John McCall
- Status: Implemented (Swift 5.7)
- Implementation: apple/swift#35264
- Major Contributors: Dave Abrahams, Paul Cantrell, Matthew Johnson, John McCall
- Review: (first review) (revision announcement) (second review) (acceptance)
- Introduction
- Motivation
- Proposed Solution Detailed Design
- Future Work / Follow-on Projects
- Source Compatibility
- Effect on API resilience
- Alternatives Considered
- Conclusion
- Revision history
A key goal of the Swift Concurrency effort is to “provide a mechanism for isolating state in concurrent programs to eliminate data races.” Such a mechanism will be a major progression for widely used programming languages — most of them provide concurrent programming abstractions in a way that subjects programmers to a wide range of bugs, including race conditions, deadlocks and other problems.
This proposal describes an approach to address one of the challenging problems in this space — how to type check value passing between structured concurrency constructs and actors messages. As such, this is a unifying theory that provides some of the underlying type system mechanics that make them both safe and work well together.
This implementation approach involves a marker protocol named Sendable
, as well as a @Sendable
attribute that may be applied to functions.
Each actor instance and structured concurrency task in a program represents an “island of single threaded-ness”, which makes them a natural synchronization point that holds a bag of mutable state. These perform computation in parallel with other tasks, but we want the vast majority of code in such a system to be synchronization free — building on the logical independence of the actor, and using its mailbox as a synchronization point for its data.
As such, a key question is: “when and how do we allow data to be transferred between concurrency domains?” Such transfers occur in arguments and results of actor method calls and tasks created by structured concurrency, for example.
The Swift Concurrency features aspire to build a safe and powerful programming model. We want to achieve three things:
- We want Swift programmers to get a static compiler error when they try to pass across concurrency domains that could introduce unprotected shared mutable state.
- We want advanced programmers to be able to implement libraries with sophisticated techniques (e.g. a concurrent hash table) that can be used in a safe way by others.
- We need to embrace the existing world, which contains a lot of code that wasn’t designed with the Swift Concurrency model in mind. We need a smooth and incremental migration story.
Before we jump into the proposed solution, let’s take a look at some common cases that we would like to be able to model along with the opportunities and challenges of each. This will help us reason about the design space we need to cover.
The first kind of type we need to support are simple values like integers. These can be trivially passed across concurrency domains because they do not contain pointers.
Going beyond this, Swift has a strong emphasis on types with value semantics, which are safe to transfer across concurrent boundaries. Except for classes, Swift’s mechanisms for type composition provide value semantics when their elements do. This includes generic structs, as well as its core collections: for example, Dictionary<Int, String>
can be directly shared across concurrency domains. Swift’s Copy on Write approach means that collections can be transferred without proactive data copying of their representations — an extremely powerful fact that I believe will make the Swift concurrency model more efficient than other systems in practice.
However, everything isn’t simple here: the core collections can not be safely transferred across concurrency domains when they contain general class references, closures that capture mutable state, and other non-value types. We need a way to differentiate between the cases that are safe to transfer and those that are not.
Structs, enums and tuples are the primary mode for composition of values in Swift. These are all safe to transfer across concurrency domains — so long as the data they contain is itself safe to transfer.
It is common in Swift and other languages with functional programming roots to use higher-order programming, where you pass functions to other functions. Functions in Swift are reference types, but many functions are perfectly safe to pass across concurrency domains — for example, those with an empty capture list.
There are many useful reasons why you’d want to send bits of computation between concurrency domains in the form of a function — even trivial algorithms like parallelMap
need this. This occurs at larger scale as well — for example, consider an actor example like this:
actor MyContactList {
func filteredElements(_ fn: (ContactElement) -> Bool) async -> [ContactElement] { … }
}
Which could then be used like so:
// Closures with no captures are ok!
list = await contactList.filteredElements { $0.firstName != "Max" }
// Capturing a 'searchName' string by value is ok, because strings are
// ok to pass across concurrency domains.
list = await contactList.filteredElements {
[searchName] in $0.firstName == searchName
}
We feel that it is important to enable functions to be passed across concurrency domains, but we are also concerned that we should not allow capturing local state by reference in these functions, and we should not allow capturing unsafe things by value. Both would introduce memory safety problems.
One common and efficient design pattern in concurrent programming is to build immutable data structures — it is perfectly safe to transfer a reference to a class across concurrency domains if the state within it never mutates. This design pattern is extremely efficient (no synchronization beyond ARC is required), can be used to build advanced data structures, and is widely explored by the pure-functional language community.
A common design pattern in concurrent systems is for a class to provide a “thread-safe” API: they protect their state with explicit synchronization (mutexes, atomics, etc). Because the public API to the class is safe to use from multiple concurrency domains, the reference to the class can be directly transferred safely.
References to actor instances themselves are an example of this: they are safe to pass between concurrency domains by passing a pointer, since the mutable state within an actor is implicitly protected by the actor mailbox.
A fairly common pattern in concurrent systems is for one concurrency domain to build up a data structure containing unsynchronized mutable state, then “hand it off” to a different concurrency domain to use by transferring the raw pointer. This is correct without synchronization if (and only if) the sender stops using the data that it built up — the result is that only the sender or receiver dynamically accesses the mutable state at a time.
There are both safe and unsafe ways to achieve this, e.g. see the discussion about “exotic” type systems in the Alternatives Considered section at the end.
One safe way to transfer reference types is to make a deep copy of the data structures, ensuring that the source and destination concurrency domains each have their own copy of mutable state. This can be expensive for large structures, but is/was commonly used in some Objective-C frameworks. General consensus is that this should be explicit, not something implicit in the definition of a type.
This is just a sampling of patterns, but as we can see, there are a wide range of different concurrent design patterns in widespread use. The design center of Swift around value types and encouraging use of structs is a very powerful and useful starting point, but we need to be able to reason about the complex cases as well — both for communities that want to be able express high performance APIs for a given domain but also because we need to work with legacy code that won’t get rewritten overnight.
As such, it is important to consider approaches that allow library authors to express the intent of their types, it is important for app programmers to be able to work with uncooperative libraries retroactively, and it is also important that we provide safety as well as unsafe escape hatches so we can all just “get stuff done” in the face of an imperfect world that is in a process of transition.
Finally, our goal is for Swift (in general and in this specific case) to be a highly principled system that is sound and easy to use. In 20 years, many new libraries will be built for Swift and its ultimate concurrency model. These libraries will be built around value semantic types, but should also allow expert programmers to deploy state of the art techniques like lock-free algorithms, use immutable types, or whatever other design pattern makes sense for their domain. We want users of these APIs to not have to care how they are implemented internally.
The high level design of this proposal revolves around a Sendable
marker protocol, adoption of Sendable
by standard library types, and a new @Sendable
attribute for functions.
Beyond the basic proposal, in the future it could make sense to add a set of adapter types to handle legacy compatibility cases, and first class support for Objective-C frameworks. These are described in the following section.
This proposal introduces the concept of a “marker” protocol, which indicates that the protocol has some semantic property but is entirely a compile-time notion that does not have any impact at runtime. Marker protocols have the following restrictions:
- They cannot have requirements of any kind.
- They cannot inherit from non-marker protocols.
- A marker protocol cannot be named as the type in an
is
oras?
check (e.g.,x as? Sendable
is an error). - A marker protocol cannot be used in a generic constraint for a conditional protocol conformance to a non-marker protocol.
We think this is a generally useful feature, but believe it should be a compiler-internal feature at this point. As such, we explain it and use this concept with the “@_marker
” attribute syntax below.
The core of this proposal is a marker protocol defined in the Swift standard library , which has special conformance checking rules:
@_marker
protocol Sendable {}
It is a good idea for types to conform to the Sendable
protocol when they are designed so all of their public API is safe to use across concurrency domains. This is true for example, when there are no public mutators, if public mutators are implemented with COW, or if they are implemented with internal locking or some other mechanism. Types may of course have internal implementation details based on local mutation if they have locking or COW as part of their public API.
The compiler rejects any attempts to pass data across concurrency domains, e.g. rejecting cases where the argument or result of an actor message send or structured concurrency call does not conform to the Sendable
protocol:
actor SomeActor {
// async functions are usable *within* the actor, so this
// is ok to declare.
func doThing(string: NSMutableString) async {...}
}
// ... but they cannot be called by other code not protected
// by the actor's mailbox:
func f(a: SomeActor, myString: NSMutableString) async {
// error: 'NSMutableString' may not be passed across actors;
// it does not conform to 'Sendable'
await a.doThing(string: myString)
}
The Sendable
protocol models types that are allowed to be safely passed across concurrency domains by copying the value. This includes value-semantic types, references to immutable reference types, internally synchronized reference types, @Sendable
closures, and potentially other future type system extensions for unique ownership etc.
Note that incorrect conformance to this protocol can introduce bugs in your program (just as an incorrect implementation of Hashable
can break invariants), which is why the compiler checks conformance (see below).
Swift has hard coded conformances for tuples to specific protocols, and this should be extended to Sendable
, when the tuples elements all conform to Sendable
.
Metatypes (such as Int.Type
, the type produced by the expression Int.self
) always conform to Sendable
, because they are immutable.
Sendable
types are extremely common in Swift and aggregates of them are also safe to transfer across concurrency domains. As such, the Swift compiler allows direct conformance to Sendable
for structs and enums that are compositions of other Sendable
types:
struct MyPerson : Sendable { var name: String, age: Int }
struct MyNSPerson { var name: NSMutableString, age: Int }
actor SomeActor {
// Structs and tuples are ok to send and receive!
public func doThing(x: MyPerson, y: (Int, Float)) async {..}
// error if called across actor boundaries: MyNSPerson doesn't conform to Sendable!
public func doThing(x: MyNSPerson) async {..}
}
While this is convenient, we would like to slightly increase friction of protocol adoption for cases that require more thought. As such, the compiler rejects conformance of structs and enums to the Sendable
protocol when one of their members (or associated values) does not itself conform to Sendable
(or is not known to conform to Sendable
through a generic constraint):
// error: MyNSPerson cannot conform to Sendable due to NSMutableString member.
// note: add '@unchecked' if you know what you're doing.
struct MyNSPerson : Sendable {
var name: NSMutableString
var age: Int
}
// error: MyPair cannot conform to Sendable due to 'T' member which may not itself be a Sendable
// note: see below for use of conditional conformance to model this
struct MyPair<T> : Sendable {
var a, b: T
}
// use conditional conformance to model generic types
struct MyCorrectPair<T> {
var a, b: T
}
extension MyCorrectPair: Sendable where T: Sendable { }
As mentioned in the compiler diagnostic, any type can override this checking behavior by annotating the conformance to Sendable
with @unchecked
. This indicates that the type can safely be passed across concurrency domains, but requires the author of the type to ensure that this is safe.
A struct
or enum
can only be made to conform to Sendable
within the same source file in which the type was defined. This ensures that the stored properties in a struct and associated values in an enum are visible so that their types can be checked for Sendable
conformance. For example:
// MySneakyNSPerson.swift
struct MySneakyNSPerson {
private var name: NSMutableString
public var age: Int
}
// in another source file or module...
// error: cannot declare conformance to Sendable outside of
// the source file defined MySneakyNSPerson
extension MySneakyNSPerson: Sendable { }
Without this restriction, another source file or module, which cannot see the private stored property name, would conclude that MySneakyNSPerson
is properly a Sendable
. One can declare conformance to Sendable
as @unchecked
to disable this check as well:
// in another source file or module...
// okay: unchecked conformances in a different source file are permitted
extension MySneakyNSPerson: @unchecked Sendable { }
Many structs and enums satisfy the requirements of Sendable
, and having to explicitly write out ": Sendable
" for every such type can feel like boilerplate.
For non-public structs and enums that are also not @usableFromInline
, and for frozen public structs and enums, the Sendable
conformance is implicitly provided when conformance checking (described in the previous section) succeeds:
struct MyPerson2 { // Implicitly conforms to Sendable!
var name: String, age: Int
}
class NotConcurrent { } // Does not conform to Sendable
struct MyPerson3 { // Does not conform to Sendable because nc is of non-Sendable type
var nc: NotConcurrent
}
Public non-frozen structs and enums do not get an implicit conformance, because doing so would present a problem for API resilience: the implicit conformance to Sendable
would become part of the contract with clients of the API, even if it was not intended to be. Moreover, this contract could easily be broken by extending the struct or enum with storage that does not conform to Sendable
.
Rationale: Existing precedent from
Hashable
,Equatable
, andCodable
is to require explicit conformance, even when the details are synthesized. We break from that precedent forSendable
because (1)Sendable
is likely to be even more common, (2) there is no impact on code size (or the binary at all) forSendable
, unlike with the other protocols, and (3)Sendable
does not introduce any additional API beyond allowing the use of the type across concurrency domains.
Note that implicit conformance to Sendable
is only available for non-generic types and for generic types whose instance data is guaranteed to be of Sendable
type. For example:
struct X<T: Sendable> { // implicitly conforms to Sendable
var value: T
}
struct Y<T> { // does not implicitly conform to Sendable because T does not conform to Sendable
var value: T
}
Swift will not implicitly introduce a conditional conformance. It is possible that this could be introduced in a future proposal.
Any class may be declared to conform to Sendable
with an @unchecked
conformance, allowing them to be passed between actors without semantic checks. This is appropriate for classes that use access control and internal synchronization to provide memory safety — these mechanisms cannot generally be checked by the compiler.
In addition, a class may conform to Sendable
and be checked for memory safety by the compiler in a specific limited case: when the class is a final class containing only immutable stored properties of types that conform to Sendable:
final class MyClass : Sendable {
let state: String
}
Such classes may not inherit from classes other than NSObject (for Objective-C interoperability). Sendable
classes have the same restriction as structs and enums that requires the Sendable
conformance to occur in the same source file.
This behavior makes it possible to safely create and pass around immutable bags of shared state between actors. There are several ways to generalize this in the future, but there are non-obvious cases to nail down. As such, this proposal intentionally keeps safety checking for classes limited to ensure we make progress on other aspects of the concurrency design.
Actor types provide their own internal synchronization, so they implicitly conform to Sendable
. The actors proposal provides more detail.
Key paths themselves conform to the Sendable
protocol. However, to ensure that it is safe to share key paths, key path literals can only capture values of types that conform to the Sendable
protocol. This affects uses of subscripts in key paths:
class SomeClass: Hashable {
var value: Int
}
class SomeContainer {
var dict: [SomeClass : String]
}
let sc = SomeClass(...)
// error: capture of 'sc' in key path requires 'SomeClass' to conform
// to 'Sendable'
let keyPath = \SomeContainer.dict[sc]
While the Sendable
protocol directly addresses value types and allows classes to opt-in to participation with the concurrency system, function types are also important reference types that cannot currently conform to protocols. Functions in Swift occur in several forms, including global func declarations, nested functions, accessors (getters, setters, subscripts, etc), and closures. It is useful and important to allow functions to be passed across concurrency domains where possible to allow higher order functional programming techniques in the Swift Concurrency model, for example to allow definition of parallelMap
and other obvious concurrency constructs.
We propose defining a new attribute on function types named @Sendable
. A @Sendable
function type is safe to transfer across concurrency domains (and thus, it implicitly conforms to the Sendable
protocol). To ensure memory safety, the compiler checks several things about values (e.g. closures and functions) that have @Sendable
function type:
-
A function can be marked
@Sendable
. Any captures must also conform toSendable
. -
Closures that have
@Sendable
function type can only use by-value captures. Captures of immutable values introduced bylet
are implicitly by-value; any other capture must be specified via a capture list:let prefix: String = ... var suffix: String = ... strings.parallelMap { [suffix] in prefix + $0 + suffix }
The types of all captured values must conform to
Sendable
. -
Accessors are not currently allowed to participate with the
@Sendable
system as of this proposal. It would be straight-forward to allow getters to do so in a future proposal if there was demand for this.
The @Sendable
attribute to function types is orthogonal to the existing @escaping
attribute, but it works the same way. @Sendable
functions are always subtypes of non-@Sendable
functions, and implicitly convert when needed. Similarly, closure expressions infer the @Sendable
bit from context just like @escaping
closures do.
We can revisit the example from the motivation section — it may be declared like this:
actor MyContactList {
func filteredElements(_ fn: @Sendable (ContactElement) -> Bool) async -> [ContactElement] { … }
}
Which could then be used like so:
// Closures with no captures are ok!
list = await contactList.filteredElements { $0.firstName != "Max" }
// Capturing a 'searchName' string is ok, because String conforms
// to Sendable. searchName is captured by value implicitly.
list = await contactList.filteredElements { $0.firstName == searchName }
// @Sendable is part of the type, so passing a compatible
// function declaration works as well.
list = await contactList.filteredElements(dynamicPredicate)
// Error: cannot capture NSMutableString in a @Sendable closure!
list = await contactList.filteredElements {
$0.firstName == nsMutableName
}
// Error: someLocalInt cannot be captured by reference in a
// @Sendable closure!
var someLocalInt = 1
list = await contactList.filteredElements {
someLocalInt += 1
return $0.firstName == searchName
}
The combination of @Sendable
closures and Sendable
types allows type safe concurrency that is library extensible, while still being easy to use and understand. Both of these concepts are key foundations that actors and structured concurrency builds on top of.
The inference rule for @Sendable
attribute for closure expressions is similar to closure @escaping
inference. A closure expression is inferred to be @Sendable
if either:
- it is used in a context that expects a
@Sendable
function type (e.g.parallelMap
orTask.runDetached
) or @Sendable
is in the closure'sin
specification.
The difference from @escaping
is that a context-less closure defaults to be non-@Sendable
, but defaults to being @escaping
:
// defaults to @escaping but not @Sendable
let fn = { (x: Int, y: Int) -> Int in x+y }
Nested functions are also an important consideration, because they can also capture values just like a closure expression. The @Sendable
attribute is used on nested function declarations to opt-into concurrency checking:
func globalFunction(arr: [Int]) {
var state = 42
// Error, 'state' is captured immutably because closure is @Sendable.
arr.parallelForEach { state += $0 }
// Ok, function captures 'state' by reference.
func mutateLocalState1(value: Int) {
state += value
}
// Error: non-@Sendable function isn't convertible to @Sendable function type.
arr.parallelForEach(mutateLocalState1)
@Sendable
func mutateLocalState2(value: Int) {
// Error: 'state' is captured as a let because of @Sendable
state += value
}
// Ok, mutateLocalState2 is @Sendable.
arr.parallelForEach(mutateLocalState2)
}
This composes cleanly for both structured concurrency and actors.
A function or closure that throws
can effectively return a value of any type that conforms to the Error
protocol. If the function is called from a different concurrency domain, the thrown value can be passed across it.
class MutableStorage {
var counter: Int
}
struct ProblematicError: Error {
var storage: MutableStorage
}
actor MyActor {
var storage: MutableStorage
func doSomethingRisky() throws -> String {
throw ProblematicError(storage: storage)
}
}
A call to myActor.doSomethingRisky()
from another concurrency domain would throw the problematic error, capturing part of the mutable state of myActor
, then provide it to another concurrency domain, breaking actor isolation. Because there is no information in the signature of doSomethingRisky()
about the types of errors thrown, and an error that propagates out from doSomethingRisky()
could come from any code that the function invokes, there is no place at which we could check that only Sendable
-conforming errors are thrown.
To close this safety hole, we alter the definition of the Error
protocol to require that all error types conform to Sendable
:
protocol Error: Sendable { … }
Now, the ProblematicError
type will be rejected with an error because it conforms to Sendable
but contains a stored property of non-Sendable
type MutableStorage
.
Generally speaking, one cannot add a new inherited protocol to an existing protocol without breaking both source and binary compatibility. However, marker protocols have no impact on the ABI and no requirements, so binary compatibility is maintained.
Source compatibility requires more care, however. ProblematicError
is well-formed in today’s Swift, but will be rejected with the introduction of Sendable
. To ease the transition, errors about types that get their Sendable
conformances through Error
will be downgraded to warnings in Swift < 6.
It is important for standard library types to be passed across concurrency domains. The vast majority of standard library types provide value semantics, and therefore should conform to Sendable
, e.g.:
extension Int: Sendable {}
extension String: Sendable {}
Generic value-semantic types are safe to be passed across concurrency domains so long as any element types are safe to be passed across concurrency domains. This dependency can be modeled by conditional conformances:
extension Optional: Sendable where Wrapped: Sendable {}
extension Array: Sendable where Element: Sendable {}
extension Dictionary: Sendable
where Key: Sendable, Value: Sendable {}
Except for the cases listed below, all struct, enum, and class types in the standard library conform to the Sendable
protocol. Generic types conditionally conform to the Sendable
protocol when all of their generic arguments conform to Sendable
. The exceptions to these rules follow:
ManagedBuffer
: this class is meant to provide mutable reference semantics for a buffer. It must not conform toSendable
(even unsafely).Unsafe(Mutable)(Buffer)Pointer
: these generic types unconditionally conform to theSendable
protocol. This means that an unsafe pointer to a non-concurrent value can potentially be used to share such values between concurrency domains. Unsafe pointer types provide fundamentally unsafe access to memory, and the programmer must be trusted to use them correctly; enforcing a strict safety rule for one narrow dimension of their otherwise completely unsafe use seems inconsistent with that design.- Lazy algorithm adapter types: the types returned by lazy algorithms (e.g., as the result of
array.lazy.map
{ … }) never conform toSendable
. Many of these algorithms (like the lazymap
) take non-@Sendable
closure values, and therefore cannot safely conform toSendable
.
The standard library protocols Error
and CodingKey
inherit from the Sendable
protocol:
Error
inherits fromSendable
to ensure that thrown errors can safely be passed across concurrency domains, as discussed in the previous section.CodingKey
inherits fromSendable
so that types likeEncodingError
andDecodingError
, which storeCodingKey
instances, can correctly conform toSendable
.
Interoperability with C and Objective-C is an important part of Swift. C code will always be implicitly unsafe for concurrency, because Swift cannot enforce correct behavior of C APIs. However, we still define some basic interactions with the concurrency model by providing implicit Sendable
conformances for many C types:
- C enum types always conform to the
Sendable
protocol. - C struct types conform to the
Sendable
protocol if all of their stored properties conform toSendable
. - C function pointers conform to the
Sendable
protocol. This is safe because they cannot capture values.
In addition to the base proposal, there are several follow-on things that could be explored as follow-on proposals.
NOTE: This section is NOT considered part of the proposal — it is included just to illustrate aspects of the design.
The proposal above provides good support for composition and Swift types that are updated to support concurrency. Further, Swift’s support for retroactive conformance of protocols makes it possible for users to work with codebases that haven’t been updated yet.
However, there is an additional important aspect of compatibility with existing frameworks that is important to confront: frameworks are sometimes designed around dense graphs of mutable objects with ad hoc structures. While it would be nice to “rewrite the world” eventually, practical Swift programmers will need support to “get things done” in the meantime. By analogy, when Swift first came out, most Objective-C frameworks were not audited for nullability. We introduced “ImplicitlyUnwrappedOptional
” to handle the transition period, which gracefully faded from use over the years.
To illustrate how we can do this with Swift concurrency, consider a pattern that is common in Objective-C frameworks: passing an object graph across threads by “transferring” the reference across threads — this is useful but not memory safe! Programmers will want to be able to express these things as part of their actor APIs within their apps.
This can be achieved by the introduction of a generic helper struct:
@propertyWrapper
struct UnsafeTransfer<Wrapped> : @unchecked Sendable {
var wrappedValue: Wrapped
init(wrappedValue: Wrapped) {
self.wrappedValue = wrappedValue
}
}
For example, NSMutableDictionary
isn’t safe to pass across concurrency domains, so it isn’t safe to conform to Sendable
. The struct above allows you (as an app programmer) to write an actor API in your application like this:
actor MyAppActor {
// The caller *promises* that it won't use the transferred object.
public func doStuff(dict: UnsafeTransfer<NSMutableDictionary>) async
}
While this isn’t particularly pretty, it is effective at getting things done on the caller side when you need to work with unaudited and unsafe code. This can also be sugared into a parameter attribute using the recently proposed extension to property wrappers for arguments, allowing a prettier declaration and caller-side syntax:
actor MyAppActor {
// The caller *promises* that it won't use the transferred object.
public func doStuff(@UnsafeTransfer dict: NSMutableDictionary) async
}
NOTE: This section is NOT considered part of the proposal — it is included just to illustrate aspects of the design.
Objective-C has established patterns that would make sense to pull into this framework en-masse, e.g. the NSCopying
protocol is one important and widely adopted protocol that should be onboarded into this framework.
General consensus is that it is important to make copies explicit in the model, so we can implement an NSCopied
helper like so:
@propertyWrapper
struct NSCopied<Wrapped: NSCopying>: @unchecked Sendable {
let wrappedValue: Wrapped
init(wrappedValue: Wrapped) {
self.wrappedValue = wrappedValue.copy() as! Wrapped
}
}
This would allow individual arguments and results of actor methods to opt-into a copy like this:
actor MyAppActor {
// The string is implicitly copied each time you invoke this.
public func lookup(@NSCopied name: NSString) -> Int async
}
One random note: the Objective-C static type system is not very helpful to us with immutability here: statically typed NSString
’s may actually be dynamically NSMutableString
’s due to their subclass relationships. Because of this, it isn’t safe to assume that values of NSString
type are dynamically immutable — they should be implemented to invoke the copy()
method.
Actors are a proposal that is conceptually layered on top of this one, but it is important to be aware of the actor design to make sure that this proposal addresses its needs. As described above, actor method sends across concurrency boundaries naturally require that arguments and results conform to Sendable
, and thus implicitly require that closures passed across such boundaries are @Sendable
.
One additional detail that needs to be addressed is “when is something a cross actor call?”. For example, we would like these calls to be synchronous and not require an await:
extension SomeActor {
public func oneSyncFunction(x: Int) {... }
public func otherSyncFunction() {
// No await needed: stays in concurrency domain of self actor.
self.oneSyncFunction(x: 42)
oneSyncFunction(x: 7) // Implicit self is fine.
}
}
However, we also need to consider the case when ‘self’ is captured into a closure within an actor method. For example:
extension SomeActor {
public func thing(arr: [Int]) {
// This should obviously be allowed!
arr.forEach { self.oneSyncFunction(x: $0) }
// Error: await required because it hops concurrency domains.
arr.parallelMap { self.oneSyncFunction(x: $0) }
// Is this ok?
someHigherOrderFunction {
self.oneSyncFunction(x: 7) // ok or not?
}
}
}
We need the compiler to know whether there is a possible concurrency domain hop or not — if so, an await is required. Fortunately, this works out through straight-forward composition of the basic type system rules above: It is perfectly safe to use actor self
in a non-@Sendable
closure in an actor method, but using it in a @Sendable
closure is treated as being from a different concurrency domain, and thus requires an await
.
The marker protocol Sendable
and the function attribute @Sendable
are intentionally given the same name. There is a potential future direction here where @Sendable
could move from a special attribute recognized by the compiler (as in this proposal), to having marker protocols like Sendable
be custom attributes like property wrappers and result builders. Such a change would have very little effect on existing code that uses @Sendable
so long as users don't declare their own Sendable
type that shadows the one from the standard library. However, it would make @Sendable
less special and allow other marker protocols to be used similarly.
This is almost completely source compatible with existing code bases. The introduction of the Sendable
marker protocol and @Sendable
functions are additive features that have no impact when not used and therefore do not affect existing code.
There are a few new restrictions that could cause source breakage in exotic cases:
- The change to keypath literals subscripts will break exotic keypaths that are indexed with non-standard types.
Error
andCodingKey
inherit fromSendable
and thus require that custom errors and keys conform toSendable
.
Because of these changes, the new restrictions will only be enforced in Swift 6 mode, but will be warnings for Swift 5 and earlier.
This proposal has no effect on API resilience!
There are several alternatives that make sense to discuss w.r.t. this proposal. Here we capture some of the bigger ones.
The Swift Concurrency Roadmap mentions that a future iteration of the feature set could introduce new type system features like “mutableIfUnique
” classes, and it is easy to imagine that move semantics and unique ownership could get introduced into Swift someday.
While it is difficult to understand the detailed interaction without knowing the full specification of future proposals, we believe that the checking machinery that enforces Sendable
checking is simple and composable. It should work with any types that are safe to pass across concurrency boundaries.
The first revision of this proposal allowed types to define custom behavior when they are sent across concurrency domains, through the implementation of an unsafeSend
protocol requirement. This increased the complexity of the proposal, admitted undesired functionality (explicitly implemented copy behavior), made the recursive aggregate case more expensive, and would result in larger code size.
This proposal defines a very simple approach for defining types that are safe to transfer across concurrency domains. It requires minimal compiler/language support that is consistent with existing Swift features, is extensible by users, works with legacy code bases, and provides a simple model that we can feel good about even 20 years from now.
Because the feature is mostly a library feature that builds on existing language support, it is easy to define wrapper types that extend it for domain specific concerns (along the lines of the NSCopied
example above), and retroactive conformance makes it easy for users to work with older libraries that haven’t been updated to know about the Swift Concurrency model yet.
- Changes from the second review:
- Renamed
@sendable
to@Sendable
, per review feedback and Core Team decision. - Add a future direction on marker protocols as custom attributes.
- Removed "Swift Concurrency 1.0" and "2.0" discussion in Alternatives Considered.
- Renamed
- Changes from the first review
- Renamed
ConcurrentValue
toSendable
and@concurrent
to@sendable
. - Replaced
UnsafeConcurrentValue
with@unchecked Sendable
conformances. - Add implicit conformance to
Sendable
for non-public, non-frozenstruct
andenum
types.
- Renamed