Skip to content

Commit 2831b72

Browse files
committed
[SE-0470] Add a section on isolated conformances to the Concurrency
chapter of the language guide.
1 parent f365934 commit 2831b72

File tree

1 file changed

+301
-0
lines changed

1 file changed

+301
-0
lines changed

TSPL.docc/LanguageGuide/Concurrency.md

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1591,6 +1591,307 @@ You can also use an unavailable conformance
15911591
to suppress implicit conformance to a protocol,
15921592
as discussed in <doc:Protocols#Implicit-Conformance-to-a-Protocol>.
15931593

1594+
## Isolated Protocol Conformances
1595+
1596+
Protocols that are nonisolated can be used from anywhere in a concurrent
1597+
program. An implementation of a nonisolated protocol conformance can still
1598+
use global actor isolated state. A conformance that needs global actor
1599+
isolated state is called an *isolated* conformance. When a conformance is
1600+
isolated, Swift prevents data races by ensuring that the conformance is only
1601+
used on the global actor that the conformance is isolated to.
1602+
1603+
### Declaring an isolated conformance
1604+
1605+
You declare an isolated conformance by writing the global actor attribute
1606+
before the protocol name when you implement the conformance. The following
1607+
code example declares a main-actor isolated conformance to `Equatable` in
1608+
an extension:
1609+
1610+
```swift
1611+
@MainActor
1612+
class Person {
1613+
var id: Int
1614+
}
1615+
1616+
extension Person: @MainActor Equatable {
1617+
static func ==(lhs: Person, rhs: Person) -> Bool {
1618+
return lhs.id == rhs.id
1619+
}
1620+
}
1621+
```
1622+
1623+
This allows the implementation of the conformance to use global actor
1624+
isolated state while ensuring that state is only accessed from within the
1625+
actor. Isolated conformances are also inferred for global actor isolated
1626+
types. The following code example declares a conformance to `Equatable` for
1627+
a main-actor isolated class, and Swift infers main-actor isolation for the
1628+
conformance:
1629+
1630+
```swift
1631+
@MainActor
1632+
class Person {
1633+
var id: Int
1634+
}
1635+
1636+
// Inferred to be a @MainActor conformance to Equatable
1637+
extension Person: Equatable {
1638+
static func ==(lhs: Person, rhs: Person) -> Bool {
1639+
return lhs.id == rhs.id
1640+
}
1641+
}
1642+
```
1643+
1644+
You can opt out of this inference for a global-actor-isolated type by
1645+
explicitly declaring that a protocol conformance is nonisolated. The
1646+
following code example declares a nonisolated isolated conformance to
1647+
`Equatable` in an extension:
1648+
1649+
```swift
1650+
@MainActor
1651+
class Person {
1652+
let id: Int
1653+
}
1654+
1655+
extension Person: nonisolated Equatable {
1656+
nonisolated static func ==(lhs: Person, rhs: Person) -> Bool {
1657+
return lhs.id == rhs.id
1658+
}
1659+
}
1660+
```
1661+
1662+
### Data-race safety for isolated conformances
1663+
1664+
Swift prevents data races for isolated conformances by ensuring that protocol
1665+
requirements are only called on the global actor that the conformance is
1666+
isolated to. In generic code, where the concrete conforming type is
1667+
abstracted away, protocol requirements can be called through type parameters
1668+
or `any` types.
1669+
1670+
#### Using isolated conformances
1671+
1672+
##### Generic code
1673+
1674+
A conformance requirement to `Sendable` allows generic code to send parameter
1675+
values to concurrently-executing code. If generic code accepts non-`Sendable`
1676+
types, then the generic code can only use the input values from the current
1677+
isolation domain. These generic APIs can safely accept isolated conformances
1678+
and call protocol requirement as long as the caller is on the same global
1679+
actor that the conformance is isolated to. The following code has a protocol
1680+
`P`, a class `C` with a main-actor isolated conformance to `P`, and two
1681+
call-sites to a generic method that accepts `some P`:
1682+
1683+
```swift
1684+
protocol P {
1685+
func perform()
1686+
}
1687+
1688+
func perform(_ p: some P) {
1689+
p.perform()
1690+
}
1691+
1692+
@MainActor class C: P { ... }
1693+
1694+
Task { @MainActor in
1695+
let c = C()
1696+
perform(c)
1697+
}
1698+
1699+
Task { @concurrent in
1700+
let c = C()
1701+
perform(c) // Error
1702+
}
1703+
```
1704+
1705+
The above code calls `perform` and provides an argument with a main-actor
1706+
isolated conformance to `P`. Calling `perform` from a main actor task is safe
1707+
because it matches the isolation of the conformance. Calling `perform` from a
1708+
concurrent task results in an error, because it would allow calling the main
1709+
actor isolated implementation of `perform` from outside the main actor.
1710+
1711+
##### Dynamic casting
1712+
1713+
Generic code can check whether a value conforms to a protocol through dynamic
1714+
casting. The following code has a protocol `P`, and a method `performIfP`
1715+
that accepts a parameter of type `Any` which is dynamic cast to `any P` in
1716+
the function body:
1717+
1718+
```swift
1719+
protocol P {
1720+
func perform()
1721+
}
1722+
1723+
func performIfP(_ value: Any) {
1724+
if let p = value as? any P {
1725+
p.perform()
1726+
}
1727+
}
1728+
```
1729+
1730+
Isolated conformances are only safe to use when the code is running on the
1731+
global actor that the conformance is isolated to, so the dynamic cast only
1732+
succeeds if the dynamic cast occurs on the global actor. If you declare a
1733+
main-actor isolated conformance to `P` and call `performIfP` with an instance
1734+
of the conforming type, the dynamic cast will only succeed when `performIfP`
1735+
is called from the main actor:
1736+
1737+
```swift
1738+
@MainActor class C: P {
1739+
func perform() {
1740+
print("C.perform")
1741+
}
1742+
}
1743+
1744+
Task { @MainActor in
1745+
let c = C()
1746+
performIfP(c) // Prints "C.perform"
1747+
}
1748+
1749+
Task { @concurrent in
1750+
let c = C()
1751+
performIfP(c) // Prints nothing
1752+
}
1753+
```
1754+
1755+
In the above code, the call to `performIfP` from a main-actor isolated task
1756+
matches the isolation of the conformance, so the dynamic cast succeeds. The
1757+
call to `performIfP` from a concurrent task happens outside the main actor,
1758+
so the dynamic cast fails and `perform` is not called.
1759+
1760+
#### Restricting isolated conformances in concurrent code
1761+
1762+
Protocol requirements can be called through instances of conforming types
1763+
and through metatype values. In generic code, a conformance requirement to
1764+
`Sendable` or `SendableMetatype` tells Swift that an instance or metatype
1765+
value is safe to use concurrently. To prevent isolated conformances from
1766+
being used outside of their actor, a type with an isolated conformance
1767+
cannot satisfy a conformance requirement to `Sendable` or `SendableMetatype`.
1768+
1769+
A conformance requirement to `Sendable` indicates that instances may be passed
1770+
across isolation boundaries and used concurrently:
1771+
1772+
```swift
1773+
protocol P {
1774+
func perform()
1775+
}
1776+
1777+
func performConcurrently<T: P>(_ t: T) where T: Sendable {
1778+
Task { @concurrent in
1779+
t.perform()
1780+
}
1781+
}
1782+
```
1783+
1784+
The above code admits data races if the conformance to `P` is isolated,
1785+
because the implementation of `perform` may access global actor isolated
1786+
state. To prevent data races, Swift prohibits using an isolated conformance
1787+
when the type is also required to conform to `Sendable`:
1788+
1789+
```swift
1790+
@MainActor class C: P { ... }
1791+
1792+
let c = C()
1793+
performConcurrently(c) // Error
1794+
```
1795+
1796+
The above code results in an error because the conformance of `C` to `P`
1797+
is main-actor isolated, which cannot satisfy the `Sendable` requirement
1798+
of `performConcurrently`.
1799+
1800+
Protocol requirements can also be called through metatype values. A
1801+
conformance to Sendable on the metatype type, such as `Int.Type`, indicates
1802+
that a metatype value is safe to pass across isolation boundaries and used
1803+
concurrently. Metatype types can conform to `Sendable` even when the type
1804+
does not conform to `Sendable`; this means that only metatype values are safe
1805+
to share in concurrent code, but instances of the type are not.
1806+
1807+
In generic code, a conformance requirement to `SendableMetatype` indicates
1808+
that the metatype of a type conforms to `Sendable`, which allows the
1809+
implementation to share metatype values in concurrent code:
1810+
1811+
```swift
1812+
protocol P {
1813+
static func perform()
1814+
}
1815+
1816+
func performConcurrently<T: P>(n: Int, for: T.Type) async where T: SendableMetatype {
1817+
await withDiscardingTaskGroup { group in
1818+
for _ in 0..<n {
1819+
group.addTask {
1820+
T.perform()
1821+
}
1822+
}
1823+
}
1824+
}
1825+
```
1826+
1827+
Without a conformance to `SendableMetatype`, generic code must only use
1828+
metatype values in the current isolation domain. The following code
1829+
results in an error because the non-`Sendable` metatype `T` is used from
1830+
concurrent child tasks:
1831+
1832+
```swift
1833+
protocol P {
1834+
static func perform()
1835+
}
1836+
1837+
func performConcurrently<T: P>(n: Int, for: T.Type) async {
1838+
await withDiscardingTaskGroup { group in
1839+
for _ in 0..<n {
1840+
group.addTask {
1841+
T.perform() // Error
1842+
}
1843+
}
1844+
}
1845+
}
1846+
```
1847+
1848+
Note that Sendable requires `SendableMetatype`, so an explicit conformance to
1849+
`SendableMetatype` is only necessary if the type is non-`Sendable`.
1850+
1851+
Types with isolated conformances cannot satisfy a `SendableMetatype` generic
1852+
requirement. Swift will prevent calling `createParallel` with a type that has
1853+
an isolated conformance to `P`:
1854+
1855+
```swift
1856+
@MainActor class C: P {
1857+
static func perform() { /* use main actor state */ }
1858+
}
1859+
1860+
let items = performConcurrently(n: 10, for: C.self) // Error
1861+
```
1862+
1863+
##### Protocols that require `Sendable` or `SendableMetatype`
1864+
1865+
Protocols can directly require that conforming types also conform to
1866+
`Sendable` or `SendableMetatype`:
1867+
1868+
```swift
1869+
public protocol Error: Sendable {}
1870+
1871+
public protocol ModelFactory: SendableMetatype {
1872+
func create() -> Self
1873+
}
1874+
```
1875+
1876+
Note that the `Sendable` protocol requires `SendableMetatype`; if an instance
1877+
of a conforming type is safe to share across concurrent code, its metatype
1878+
must also be safe to share:
1879+
1880+
```swift
1881+
public protocol Sendable: SendableMetatype {}
1882+
```
1883+
1884+
If a protocol requires `Sendable`, then any use of the protocol can freely
1885+
send instances across isolation boundaries. If a protocol requires
1886+
`SendableMetatype`, then uses of metatypes in generic code can cross
1887+
isolation boundaries. In both cases, Swift prevents declaring an isolated
1888+
conformance, because generic code can always call requirements concurrently.
1889+
1890+
```swift
1891+
@MainActor
1892+
enum MyError: @MainActor Error {} // Error
1893+
```
1894+
15941895
<!--
15951896
LEFTOVER OUTLINE BITS
15961897

0 commit comments

Comments
 (0)