Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
364 changes: 364 additions & 0 deletions TSPL.docc/LanguageGuide/Concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -1591,6 +1591,370 @@ You can also use an unavailable conformance
to suppress implicit conformance to a protocol,
as discussed in <doc:Protocols#Implicit-Conformance-to-a-Protocol>.

## Isolated Protocol Conformances

Protocols that are nonisolated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's either define "nonisolated protocol" if it's a new concept being introduced, or find a way to refer to normal protocol conformance that doesn't sound like a new term.

can be used from anywhere in a concurrent program.
A conformance to a nonisolated protocol can be isolated
to a global actor, which allows the implementation to
access actor isolated state synchronously.
This is called an *isolated conformance*.
When a conformance is isolated,
Swift prevents data races by ensuring that
the conformance is only used on the actor
that the conformance is isolated to.

### Declaring an Isolated Conformance
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably don't need a heading here after just one paragraph.


You declare an isolated conformance
by writing the global actor attribute before the protocol name
when you implement the conformance.
The following code example declares
a main-actor isolated conformance to `Equatable` in an extension:

```swift
@MainActor
class Person {
var id: Int
}

extension Person: @MainActor Equatable {
static func ==(lhs: Person, rhs: Person) -> Bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To match TSPL style elsewhere:

Suggested change
static func ==(lhs: Person, rhs: Person) -> Bool {
static func == (lhs: Person, rhs: Person) -> Bool {

Likewise below; marking it just here.

return lhs.id == rhs.id
}
}
```

This allows the implementation of the conformance
to use global actor isolated state
while ensuring that state is only accessed
from within the actor.
Comment on lines +1628 to +1631
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's walk through the code listing more step-by-step in its explanation paragraph. For example:

  • Why is Person marked @MainActor?
  • Call out that writing @MainActor Equatable is how you mark the conformance as actor-isolated

Expand "This allows" so the reader doesn't have to guess what "this" refers to. Here, probably "This isolated conformance allows"?


### Inferring an Isolated Conformance
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The developer reading TSPL isn't doing the inference, the compiler is. Maybe "Inferred" or "Using Inferred" in the heading?


> Note:
> Isolated conformance inference is an upcoming language feature.
> To enable it in current language modes of Swift,
> use the feature identifier `InferIsolatedConformances`.

Isolated conformances are inferred
for global actor isolated types.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss "global actor isolated types" with an editor, find a phrasing that avoids the noun pile, and add an entry to the TSPL style guide. Hyphenating like you did below (global-actor-isolated type) works, but could be hard to read. Expanding it to "types that are isolated to a global actor" is likely to be too wordy when used more than once or twice.

The following code example declares a conformance to `Equatable`
for a main-actor isolated class,
and Swift infers main-actor isolation for the conformance:

```swift
@MainActor
class Person {
var id: Int
}

// Inferred to be a @MainActor conformance to Equatable
extension Person: Equatable {
static func ==(lhs: Person, rhs: Person) -> Bool {
return lhs.id == rhs.id
}
}
```

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the listing above just a shorter (implicit) way to spell the listing that came before it? Let's call out the difference, or lack of difference.

You can opt out of this inference for a global-actor-isolated type
by explicitly declaring that a protocol conformance is nonisolated.
The following code example declares
a nonisolated conformance to `Equatable` in an extension:

```swift
@MainActor
class Person {
let id: Int
}
Comment on lines +1666 to +1669
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Within a running example, each code listing doesn't need to repeat code that's unchanged. In most of the running examples, a single piece of code is built up across multiple code listings.

That style decision comes, in part, from the pre-DocC build system that literally concatenated code listings (whose names were the same) into one file when compiling and testing. The consequence of that style is that TSPL often calls out in prose things like "here's another version of SomeType that does x" to tell the reader that it's a replacement for something they just read, not more parts of the same code.


extension Person: nonisolated Equatable {
nonisolated static func ==(lhs: Person, rhs: Person) -> Bool {
return lhs.id == rhs.id
}
}
```

### Data-Race Safety for Isolated Conformances

Swift prevents data races for isolated conformances
by ensuring that protocol requirements are only used
on the global actor that the conformance is isolated to.
In generic code,
where the concrete conforming type is abstracted away,
protocol requirements can be used through type parameters or `any` types.
Comment on lines +1683 to +1685
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this sentence hard to understand until I started reading the code listing.

By type parameters, I think this is referring to generic type parameters, right? TSPL uses both terms, but mostly uses "type parameters" in the Generics chapter — in this context, it might be worth spelling out.

TSPL uses the term "boxed protocol types" rather than "any types" or "existential types". Would you consider code that uses any to be a form of generic code, or should we adjust the wording at the start of the sentence?


#### Using Isolated Conformances
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this heading here and at this level? Level 4 headings are allowed, but should be rare. They don't render especially well and often indicate there's a better, less deeply nested way, that we can organize the content.


A conformance requirement to `Sendable`
allows generic code to send parameter values to concurrently-executing code.
If generic code accepts non-`Sendable` types,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using code voice as an English word, or part of a word.

Suggested change
If generic code accepts non-`Sendable` types,
If generic code accepts nonsendable types,

then the generic code can only use the input values
from the current isolation domain.
These generic APIs can safely accept isolated conformances
and call protocol requirements
as long as the caller is on the same global actor
that the conformance is isolated to.
The following code has a protocol `Dancer`,
a class `Ballerina` with a main-actor isolated conformance to `Dancer`,
and calls to the `Dancer.perform` requirement
from a main-actor task and a concurrent task:

```swift
protocol Dancer {
func perform()
}

func startRecital(_ dancer: some Dancer) {
dancer.perform()
}

@MainActor class Ballerina: @MainActor Dancer { ... }

Task { @MainActor in
let ballerina = Ballerina()
startRecital(ballerina)

let dancer: any Dancer = ballerina
dancer.perform()
}

Task { @concurrent in
let ballerina = Ballerina()
startRecital(c) // Error

let dancer: any Dancer = ballerina // Error
Comment on lines +1724 to +1726
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's a way to briefly state the error, let's include that in the comment. Not all // Error comments in TSPL do this, but it's preferred when we can.

dancer.perform()
}
```

Calling `Dancer.perform` in generic code and on an `any Dancer` type
from a main actor task
is safe because it matches the isolation of the conformance.
Calling `Dancer.perform` in generic code and on an `any Dancer` type
from a concurrent task results in an error,
because it would allow calling the main actor isolated implementation
of `Dancer.perform` from outside the main actor.

Abstract code that uses type parameters and `any` types
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use the term "abstract code" elsewhere in TSPL — let's see if there's an existing term we can use here.

can check whether a value conforms to a protocol
through dynamic casting.
Comment on lines +1739 to +1741
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new part of the discussion needs a clearer connection to the current topic. How is this different from the existing discussion of as? in the Type Casting chapter? I think you're re-introducing that syntax as buildup for the next example, showing when that as? cast succeeds?

The following code has a protocol `Dancer`,
and a method `performIfDancer` that accepts a parameter of type `Any`
which is dynamically cast to `any Dancer` in the function body:
Comment on lines +1742 to +1744
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be clearer to introduce the code listing with what details I should watch out for while reading it. For example here, I think the salient point is the value as? any Dancer check, not (for example) the Dancer protocol. We can add a paragraph after the code listing that walks through the code in smaller steps.


```swift
protocol Dancer {
func perform()
}

func performIfDancer(_ value: Any) {
if let dancer = value as? any Dancer {
dancer.perform()
}
}
```

Isolated conformances are only safe to use
when the code is running on the global actor
that the conformance is isolated to,
so the dynamic cast only succeeds
if the dynamic cast occurs on that global actor.
Comment on lines +1761 to +1762
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be the critical point — let's lead with it.

For example, if you declare a main-actor isolated conformance to `Dancer`
and call `performIfDancer` with an instance of the conforming type,
the dynamic cast will succeed
when `performIfDancer` is called in a main actor task, and it
will fail when `performIfDancer` is called in a concurrent
task:

```swift
@MainActor class Ballerina: @MainActor Dancer {
func perform() {
print("Ballerina.perform")
}
}

Task { @MainActor in
let ballerina = Ballerina()
performIfDancer(ballerina) // Prints "Ballerina.perform"
}

Task { @concurrent in
let ballerina = Ballerina()
performIfDancer(ballerina) // Prints nothing
}
```

In the above code,
the call to `performIfDancer` from a main-actor isolated task
matches the isolation of the conformance,
so the dynamic cast succeeds.
The call to `performIfDancer` from a concurrent task
happens outside the main actor,
so the dynamic cast fails and `perform` is not called.

#### Restricting Isolated Conformances in Concurrent Code

Protocol requirements can be used
through instances of conforming types and through
instances of the conforming types themselves,
called *metatype values*.
In generic code,
a conformance requirement to `Sendable` or `SendableMetatype`
tells Swift that an instance or metatype value is safe to use concurrently.
Comment on lines +1802 to +1804
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads like a definition of Sendable but I think it's about the where D: Sendable clause in the code below? We can check terminology here too — I see "conformance requirement" only twice in TSPL, and in both places it refers to the requirements that a type must implement in order to conform to a protocol.

To prevent isolated conformances from being used outside of their actor,
a type with an isolated conformance
can't be used as the concrete generic argument for a type
parameter that requires a conformance
to `Sendable` or `SendableMetatype`.

A conformance requirement to `Sendable` indicates
that instances may be passed across isolation boundaries and used concurrently:

```swift
protocol Dancer {
func perform()
}

func performConcurrently<D: Dancer>(_ dancer: D) where D: Sendable {
Task { @concurrent in
dancer.perform()
}
}
```

The above code would admit data races
if the conformance to `Dancer` was isolated,
because the implementation of `perform`
may access global actor isolated state.
Comment on lines +1826 to +1829
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having a hard time with this sentence, probably because of the irrealis mood on the verbs (would admit, was isolated, may access). Are you saying that the code above is safe, and then describing why by telling me about something that can't happen? Or are you describing a scenario where the code has possible data races?

To prevent data races,
Swift prohibits using an isolated conformance
when the type is also required to conform to `Sendable`:

```swift
@MainActor class Ballerina: @MainActor Dancer { ... }

let ballerina = Ballerina()
performConcurrently(ballerina) // Error
```

The above code results in an error
because the conformance of `Ballerina` to `Dancer` is main-actor isolated,
which can't satisfy the `Sendable` requirement of `performConcurrently`.

Static and initializer protocol requirements
can also be called through metatype values.
A conformance to Sendable on the metatype type,
such as `Int.Type`,
indicates that a metatype value is safe
to pass across isolation boundaries and used concurrently.
Metatype types can conform to `Sendable`
even when the type does not conform to `Sendable`;
this means that only metatype values are safe to share in concurrent code,
but instances of the type are not.

In generic code,
a conformance requirement to `SendableMetatype`
indicates that the metatype of a type conforms to `Sendable`,
which allows the implementation to share metatype values in concurrent code:

```swift
protocol Dancer {
init()
func perform()
}

func performConcurrently<D: Dancer>(n: Int, for: D.Type) async where T: SendableMetatype {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the T is just a leftover from a previous version that had different type names?

Suggested change
func performConcurrently<D: Dancer>(n: Int, for: D.Type) async where T: SendableMetatype {
func performConcurrently<D: Dancer>(n: Int, for: D.Type) async where D: SendableMetatype {

await withDiscardingTaskGroup { group in
for _ in 0..<n {
group.addTask {
let dancer = T.init()
dancer.perform()
}
}
}
}
```

Without a conformance to `SendableMetatype`,
generic code must only use metatype values from the current isolation domain.
The following code results in an error
because the non-`Sendable` metatype `D`
is used from concurrent child tasks:

```swift
protocol Dancer {
init()
func perform()
}

func performConcurrently<D: Dancer>(n: Int, for: D.Type) async {
await withDiscardingTaskGroup { group in
for _ in 0..<n {
group.addTask {
let dancer = D.init() // Error
dancer.perform()
}
}
}
}
```

Note that `Sendable` requires `SendableMetatype`,
so an explicit conformance to `SendableMetatype` is only necessary
if the type is non-`Sendable`.
Comment on lines +1903 to +1905
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This information seems like it should come earlier.


Types with isolated conformances can't satisfy
a `SendableMetatype` generic requirement.
Swift will prevent calling `createParallel`
with a type that has an isolated conformance to `Dancer`:

```swift
@MainActor class Ballerina: @MainActor Dancer {
init() { /* use main actor state */ }
func perform() { /* use main actor state */ }
}

let items = performConcurrently(n: 10, for: Ballerina.self) // Error
```

##### Protocols That Require `Sendable` or `SendableMetatype`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't use H5 or code voice in headings.


<!-- XXX: Can't use code voice in headings -->

Protocols can directly require that
conforming types also conform to `Sendable` or `SendableMetatype`:

```swift
public protocol Error: Sendable {}

public protocol ModelFactory: SendableMetatype {
static func create() -> Self
}
```

Note that the `Sendable` protocol requires `SendableMetatype`;
if an instance of a conforming type is safe to share across concurrent code,
its metatype must also be safe to share:

```swift
public protocol Sendable: SendableMetatype {}
```

If a protocol requires `Sendable`,
then any use of the protocol
can freely send instances across isolation boundaries.
If a protocol requires `SendableMetatype`,
then uses of metatypes in generic code can cross isolation boundaries.
In both cases,
Swift prevents declaring an isolated conformance,
because generic code can always call requirements concurrently.

```swift
@MainActor
enum MyError: @MainActor Error {} // Error
```

<!--
LEFTOVER OUTLINE BITS

Expand Down