Skip to content

Commit f50feb9

Browse files
Crossing Isolation Boundaries #7 (#20)
* Crossing Isolation Boundaries #7 This includes content that covers: - Understanding when non-`Sendable` types can become a problem - Ways to add a `Sendable` conformance - Ways to avoid needing a `Sendable` conformance It does not include preconcurrency or any other incremental migration. * Section on preconcurrency import * Non-Isolated Initialization * Computed Value * Typos * Remove forthcoming links * Undo package changes * Fix description of implicit Sendable on public type * Update Guide.docc/CommonProblems.md Co-authored-by: Holly Borla <[email protected]> * Address feedback --------- Co-authored-by: Holly Borla <[email protected]>
1 parent 00fe5ad commit f50feb9

File tree

2 files changed

+337
-1
lines changed

2 files changed

+337
-1
lines changed

Guide.docc/CommonProblems.md

Lines changed: 329 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ Together, these could require significant structural changes to address.
297297
This may still be the right solution, but the side-effects should be carefully
298298
considered first, even if only a small number of types are involved.
299299

300-
#### Using Preconcurrency
300+
#### Preconcurrency Conformance
301301

302302
Swift has a number of mechanisms to help you adopt concurrency incrementally
303303
and interoperate with code that has not yet begun using concurrency at all.
@@ -391,3 +391,331 @@ struct CustomWindowStyle: Styler {
391391
Here, a new type has been created that can satisfy the needed inheritance.
392392
Incorporating will be easiest if the conformance is only used internally by
393393
`WindowStyler`.
394+
395+
## Crossing Isolation Boundaries
396+
397+
Any value that needs to move from one isolation domain to another
398+
must either be `Sendable` or must preserve mutually exclusive access.
399+
Using values with types that do not satisfy these requirements in contexts
400+
that require them is a very common problem.
401+
And because libraries and frameworks may be updated to use Swift's
402+
concurrency features, these issues can come up even when your code hasn't
403+
changed.
404+
405+
### Implicitly-Sendable Types
406+
407+
Many value types consist entirely of `Sendable` properties.
408+
The compiler will treat types like this as implicitly `Sendable`, but _only_
409+
when they are non-public.
410+
411+
```swift
412+
public struct ColorComponents {
413+
public let red: Float
414+
public let green: Float
415+
public let blue: Float
416+
}
417+
418+
@MainActor
419+
func applyBackground(_ color: ColorComponents) {
420+
}
421+
422+
func updateStyle(backgroundColor: ColorComponents) async {
423+
await applyBackground(backgroundColor)
424+
}
425+
```
426+
427+
A `Sendable` conformance is part of a type's public API contract,
428+
and that is up to you to define.
429+
Because `ColorComponents` is marked `public` it will not have an implicit
430+
conformance to `Sendable`.
431+
This will result in the following error:
432+
433+
```
434+
6 |
435+
7 | func updateStyle(backgroundColor: ColorComponents) async {
436+
8 | await applyBackground(backgroundColor)
437+
| |- error: sending 'backgroundColor' risks causing data races
438+
| `- note: sending task-isolated 'backgroundColor' to main actor-isolated global function 'applyBackground' risks causing data races between main actor-isolated and task-isolated uses
439+
9 | }
440+
10 |
441+
```
442+
443+
A very straightforward solution is just to make the type's `Sendable`
444+
conformance explicit.
445+
446+
```swift
447+
public struct ColorComponents: Sendable {
448+
// ...
449+
}
450+
```
451+
452+
Even when trivial, adding a `Sendable` conformance should always be
453+
done with care.
454+
Remember that `Sendable` is a guarantee of thread-safety, and part of a
455+
type's API contract.
456+
Removing the conformance is an API-breaking change.
457+
458+
### Preconcurrency Import
459+
460+
Even if the type in another module is actually `Sendable`, it is not always
461+
possible to modify its definition.
462+
In this case, you can use a `@preconcurrency import` to suppress errors until
463+
the library is updated.
464+
465+
```swift
466+
// ColorComponents defined here
467+
@preconcurrency import UnmigratedModule
468+
469+
func updateStyle(backgroundColor: ColorComponents) async {
470+
// crossing an isolation domain here
471+
await applyBackground(backgroundColor)
472+
}
473+
```
474+
475+
With the addition of this `@preconcurrency import`,
476+
`ColorComponents` remains non-`Sendable`.
477+
However, the compiler's behavior will be altered.
478+
When using the Swift 6 language mode, the produced here will be downgraded
479+
to a warning.
480+
The Swift 5 language mode will produce no diagnostics at all.
481+
482+
### Latent Isolation
483+
484+
Sometimes the _apparent_ need for a `Sendable` type can actually be the
485+
symptom of a more fundamental isolation problem.
486+
The only reason a type needs to be `Sendable` is to cross isolation boundaries.
487+
If you can avoid crossing boundaries altogether, the result can
488+
often be both simpler and a better reflection of the true nature of your
489+
system.
490+
491+
```swift
492+
@MainActor
493+
func applyBackground(_ color: ColorComponents) {
494+
}
495+
496+
func updateStyle(backgroundColor: ColorComponents) async {
497+
await applyBackground(backgroundColor)
498+
}
499+
```
500+
501+
The `updateStyle(backgroundColor:)` function is non-isolated.
502+
This means that its non-`Sendable` parameter is also non-isolated.
503+
But, it is immediately crossing from this non-isolated domain to the
504+
`MainActor` when `applyBackground(_:)` is called.
505+
506+
Since `updateStyle(backgroundColor:)` is working directly with
507+
`MainActor`-isolated functions and non-`Sendable` types,
508+
just applying `MainActor` isolation may be more appropriate.
509+
510+
```swift
511+
@MainActor
512+
func updateStyle(backgroundColor: ColorComponents) async {
513+
applyBackground(backgroundColor)
514+
}
515+
```
516+
517+
Now, there is no longer an isolation boundary for the non-`Sendable` type to
518+
cross.
519+
And in this case, not only does this resolve the problem, it also
520+
removes the need for an asynchronous call.
521+
Fixing latent isolation issues can also potentially make further API
522+
simplification possible.
523+
524+
Lack of `MainActor` isolation like this is, by far, the most common form of
525+
latent isolation.
526+
It is also very common for developers to hesitate to use this as a solution.
527+
It is completely normal for programs with a user interface to have a large
528+
set of `MainActor`-isolated state.
529+
Concerns around long-running _synchronous_ work can often be addressed with
530+
just a handful of targeted `nonisolated` functions.
531+
532+
### Computed Value
533+
534+
Instead of trying to pass a non-`Sendable` type across a boundary, it may be
535+
possible to use a `Sendable` function that creates the needed values.
536+
537+
```swift
538+
func updateStyle(backgroundColorProvider: @Sendable () -> ColorComponents) async {
539+
await applyBackground(using: backgroundColorProvider)
540+
}
541+
```
542+
543+
Here, it does not matter than `ColorComponents` is not `Sendable`.
544+
By using `@Sendable` function that can compute the value, the lack of
545+
sendability is side-stepped entirely.
546+
547+
### Sendable Conformance
548+
549+
When encountering problems related to crossing isolation domains, a very
550+
natural reaction is to just try to add a conformance to `Sendable`.
551+
You can make a type `Sendable` in four ways.
552+
553+
#### Global Isolation
554+
555+
Adding global isolation to any type will make it implicitly `Sendable`.
556+
557+
```swift
558+
@MainActor
559+
public struct ColorComponents {
560+
// ...
561+
}
562+
```
563+
564+
By isolating this type to the `MainActor`, any accesses from other isolation domains
565+
must be done asynchronously.
566+
This makes it possible to safely pass instances around across domains.
567+
568+
#### Actors
569+
570+
Actors have an implicit `Sendable` conformance because their properties are
571+
protected by actor isolation.
572+
573+
```swift
574+
actor Style {
575+
private var background: ColorComponents
576+
}
577+
```
578+
579+
In addition to gaining a `Sendable` conformance, actors have their own
580+
isolation domain.
581+
This allows them to freely work with other non-`Sendable` types internally.
582+
This can be a major advantage, but does come with trade-offs.
583+
584+
Because an actor's isolated methods all must be asynchronous,
585+
sites that access the type may now require an async context.
586+
This alone is a reason to make such a change with care.
587+
But further, data that is passed into or out of the actor may now itself
588+
need to cross the new isolation boundary.
589+
This can end up resulting in the need for yet more `Sendable` types.
590+
591+
#### Manual Synchronization
592+
593+
If you have a type that is already doing manual synchronization, you can
594+
express this to the compiler by marking your `Sendable` conformance as
595+
`unchecked`.
596+
597+
```swift
598+
class Style: @unchecked Sendable {
599+
private var background: ColorComponents
600+
private let queue: DispatchQueue
601+
}
602+
```
603+
604+
You should not feel compelled to remove use of queues, locks, or other
605+
forms of manual synchronization to integrate with Swift's concurrency system.
606+
However, most types are not inherently thread-safe.
607+
As a general rule, if a type isn't already thread-safe, attempting to make
608+
it `Sendable` should not be your first approach.
609+
It is often much easier to try other techniques first, falling back to
610+
manual synchronization only when truly necessary.
611+
612+
#### Sendable Reference Types
613+
614+
It is possible for reference types to be validated as `Sendable` without
615+
the `unchecked` qualifier.
616+
But, this can only be done under very narrow circumstances.
617+
618+
To allow a checked `Sendable` conformance a class:
619+
620+
- Must be `final`
621+
- Cannot inherit from another class other than `NSObject`
622+
- Cannot have any non-isolated mutable properties
623+
624+
```swift
625+
public struct ColorComponents: Sendable {
626+
// ...
627+
}
628+
629+
final class Style: Sendable {
630+
private let background: ColorComponents
631+
}
632+
```
633+
634+
Sometimes, this is a sign of a struct in disguise.
635+
But this can still be a useful technique when reference semantics need to be
636+
preserved, or for types that are part of a mixed Swift/Objective-C code base.
637+
638+
#### Using Composition
639+
640+
You do not need to select one single technique for making a reference type
641+
`Sendable.`
642+
One type can use many techniques internally.
643+
644+
```swift
645+
final class Style: Sendable {
646+
private nonisolated(unsafe) var background: ColorComponents
647+
private let queue: DispatchQueue
648+
649+
@MainActor
650+
private var foreground: ColorComponents
651+
}
652+
```
653+
654+
The `background` property is protected by manual synchronization,
655+
while the `foreground` property uses actor isolation.
656+
Combining these two techniques results in a type that better describes its
657+
internal semantics.
658+
And by doing this, the type can now continue to take advantage of the
659+
compiler's automated isolation checking.
660+
661+
### Non-Isolated Initialization
662+
663+
Actor-isolated types can present a problem when they have to be initialized in
664+
a non-isolated context.
665+
This occurs frequently when the type is used in a default value expression or
666+
as a property initializer.
667+
668+
> Note: These problems could also be a symptom of
669+
[latent isolation](#Latent-Isolation) or an
670+
[under-specified protocol](#Under-Specified-Protocol).
671+
672+
Here the non-isolated `Stylers` type is making a call to a
673+
`MainActor`-isolated initializer.
674+
675+
```swift
676+
@MainActor
677+
class WindowStyler {
678+
init() {
679+
}
680+
}
681+
682+
struct Stylers {
683+
static let window = WindowStyler()
684+
}
685+
```
686+
687+
This code results in the following error:
688+
689+
```
690+
7 |
691+
8 | struct Stylers {
692+
9 | static let window = WindowStyler()
693+
| `- error: main actor-isolated default value in a nonisolated context
694+
10 | }
695+
11 |
696+
```
697+
698+
Globally-isolated types sometimes don't actually need to reference any global
699+
actor state in their initializers.
700+
By making the `init` method `nonisolated`, it is free to be called from any
701+
isolation domain.
702+
This remains safe as the compiler still guarantees that any state that *is*
703+
isolated will only be accessible from the `MainActor`.
704+
705+
```swift
706+
@MainActor
707+
class WindowStyler {
708+
private var viewStyler = ViewStyler()
709+
private var primaryStyleName: String
710+
711+
nonisolated init(name: String) {
712+
self.primaryStyleName = name
713+
// type is fully-initialized here
714+
}
715+
}
716+
```
717+
718+
719+
All `Sendable` properties can still be safely accessed in this `init` method.
720+
And while any non-`Sendable` properties cannot,
721+
they can still be initialized by using default expressions.

Guide.docc/DataRaceSafety.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,14 @@ isolation boundary.
395395

396396
Values are only ever permitted to cross an isolation boundary where there
397397
is no potential for concurrent access to shared mutable state.
398+
Values can cross a boundary directly, via asychronous function calls.
399+
They can also cross boundaries indirectly when captured by closures.
400+
401+
When you call an asynchronous function with a _different_ isolation domain,
402+
the parameters and return value need to cross a boundary.
403+
Closures introduce many opportunities to cross isolation boundaries.
404+
They can be created in one domain and then executed in another.
405+
They can even be executed in multiple, different domains.
398406

399407
### Sendable Types
400408

0 commit comments

Comments
 (0)