"Approachable concurrency" es la unión de las siguientes propuestas:
5.9 SE-401: Disable Outward Actor Isolation Inference
6.0 SE-418: Infer Sendable for methods & key path literals
6.0 SE-434: Usability of global-actor-isolated types
6.2 SE-461: nonisolated(nonsending) by Default
6.2 SE-466: Control default actor isolatioin inference
6.2 SE-470: Global-actor isolated conformances
6.2 SE-486: Migration tooling for Swift features
Enables upcoming features that aim to provide a more approachable path to Swift Concurrency: DisableOutwardActorInference, GlobalActorIsolatedTypesUsability, InferIsolatedConformances, InferSendableFromCaptures, and NonisolatedNonsendingByDefault.
En el proyecto de iOS se busca SWIFT_APPROACHABLE_CONCURRENCY.
Revisión de las propuestas:
Disable Outward Actor Isolation Inference
Declarations that are not explicitly annotated with either a global actor or nonisolated can infer global actor isolation from several different places:
A struct or class containing a wrapped instance property with a global actor-qualified wrappedValue infers actor isolation from that property wrapper:
@propertyWrapper
struct UIUpdating<Wrapped> {
@MainActor var wrappedValue: Wrapped
}
struct CounterView { // infers @MainActor from use of @UIUpdating
@UIUpdating var intValue: Int = 0
}
El código anterior era confuso. Por esta razón:
This proposal advocates for removing this inference rule when compiling in the Swift 6 language mode. Given the example above,
CounterViewwould no longer infer@MainActorisolation in Swift 6.
Usability of global-actor-isolated types
Esta propuesta de Swift 6.0 resuelve tres problemas de usabilidad relacionados con tipos que están aislados a actores globales (como @MainActor).
Problema 1: Propiedades var en structs aislados
Antes: Si tenías un struct con @MainActor, las propiedades var no podían usarse fuera del actor sin marcarlas nonisolated(unsafe), lo cual era innecesariamente alarmante:
@MainActor struct S {
nonisolated(unsafe) var x: Int = 0 // feo e innecesario
}
Después: Si la propiedad es de tipo Sendable (como Int), Swift infiere automáticamente nonisolated dentro del mismo módulo, porque acceder a un valor Sendable nunca puede causar una carrera de datos:
@MainActor struct S {
var x: Int = 0 // Swift lo trata como nonisolated dentro del módulo
}
La clave es que esto solo aplica dentro del módulo, porque desde afuera alguien podría convertir esa propiedad en "computed" (con lógica aislada al actor) y eso rompería compatibilidad.
Problema 2: Closures aislados y @Sendable
Antes: Un closure @MainActor no era automáticamente @Sendable, lo que causaba errores absurdos al pasarlo a un Task:
func test(fn: @escaping @MainActor () -> Void) {
Task {
await fn() // ❌ error: tipo no-Sendable capturado
}
}
Después: @Sendable se infiere automáticamente en closures aislados a un actor global. Tiene sentido porque ese closure siempre corre en el mismo actor, así que nunca habrá acceso concurrente. Incluso puede capturar valores non-Sendable de forma segura:
class NonSendable {}
func test() {
let ns = NonSendable()
let closure = { @MainActor in
print(ns) // ✅ seguro, siempre corre en MainActor
}
Task { await closure() }
}
Problema 3: Subclases aisladas de superclases no-Sendable
Antes: Esto daba un error aunque en muchos casos sería perfectamente válido:
class Base {} // no-Sendable
@MainActor
class Sub: Base {} // ❌ error en Swift 5.10
El problema real era que @MainActor implica Sendable, lo que permitía pasar Sub entre contextos de concurrencia heredando estado mutable no protegido de Base.
Después: Se permite la subclase aislada, pero sin conformancia a Sendable. Así se evita el peligro sin bloquear el patrón por completo:
@MainActor
class Sub: Base {} // ✅ permitido, pero Sub no es Sendable
Global-actor isolated conformances
Tipos de datos aislados en MainActor pueden conformar protocolos en @MainActor.
Inferring Sendable for methods and key path literals
Problema 1: Referencias a métodos y @Sendable
En Swift puedes tomar una referencia a un método sin llamarlo. Esto se llama referencia parcial o unapplied method reference:
struct Counter: Sendable {
func increment() { ... }
}
// Referencia al método sin llamarlo
let fn = Counter.increment // tipo: (Counter) -> () -> Void
El problema era que al pasar esa referencia a un contexto concurrente, Swift se quejaba de que no era @Sendable, aunque el tipo Counter sí lo fuera:
// ❌ Antes: error aunque Counter es Sendable
Task {
let fn = Counter.increment // 'Counter.increment' no es @Sendable
}
Esto era inconsistente: si Counter es Sendable, cualquier referencia a sus métodos también debería serlo, porque no capturan estado no-Sendable.
Después: el compilador infiere @Sendable automáticamente:
// ✅ Ahora: funciona sin anotaciones
Task {
let fn = Counter.increment // @Sendable inferido
}
Problema 2: Key paths y Sendable
Un key path es una referencia a una propiedad que puedes almacenar y usar después:
struct Person {
var name: String
}
let kp = \Person.name // KeyPath<Person, String>
El problema era que Swift marcaba los key paths como Sendable de forma conservadora, lo que causaba advertencias confusas incluso cuando no había ningún riesgo real:
// ❌ Antes: advertencia innecesaria en contextos no concurrentes
let kp = \Person.name // ⚠️ advertencia: key path debería ser Sendable
Y al revés, si necesitabas que fuera Sendable explícitamente, no había forma de anotarlo.
Después: dos mejoras:
// ✅ Sin advertencia si no se usa en contexto concurrente
let kp = \Person.name // Sendable solo se infiere si hace falta
// ✅ Puedes forzarlo explícitamente cuando lo necesitas
let kp: any KeyPath<Person, String> & Sendable = \Person.name
nonisolated(nonsending) by Default
Las funciones async no aisladas (nonisolated) siempre saltaban a un executor genérico al ejecutarse. Con esta propuesta, en cambio, heredan el actor del contexto que las llama, a menos que se marquen explícitamente con @concurrent.
Para activar manualmente los Upcoming features en un paquete de SPM
Usar .swiftLanguageMode(.v5)
.target(
name: "YourPackageTarget",
swiftSettings: [
.swiftLanguageMode(.v5)
.enableUpcomingFeature("DisableOutwardActorInference"),
.enableUpcomingFeature("GlobalActorIsolatedTypesUsability"),
.enableUpcomingFeature("InferIsolatedConformances"),
.enableUpcomingFeature("InferSendableFromCaptures"),
.enableUpcomingFeature("NonisolatedNonsendingByDefault")
]
)
Notas del video
Pasos para implementar concurrency:
- Escribir código secuencial, de un solo hilo.
- Suspender la ejecución, sin paralelismo.
- Introducir paralelismo para mejorar el desempeño.
¿Cuáles son los nuevos cambios?
-
NonisolatedNonsendingByDefault.- `nonisolated(nonsending) no envía el objeto/estado a otro dominio de aislamiento.
Si se activa Migrate sobre NonisolatedNonsendingByDefault entonces aparece un warning sobre el código:
swift
// ⚠️ Feature 'NonisolatedNonsendingByDefault' will cause nonisolated async instance method 'performAsync' to run on the calle's actor; use '@concurrency' to preserve behavior.
En el warning aparece un signo de interrogación para aprender más y también un botón "Fix" para aplicar el cambio.
Es importante apoyarse en el "Migration tooling" porque la migración manual puede ser peligrosa.
Control default actor isolation inference
- Originalmente todo es
nonisolated. Sin embargo, se puede ponerMainActorpor defecto en todo. - Puede ser peligroso usar
MainActorpor defecto. - Para nuevos proyectos, sirve tener
MainActorpor defecto. Cuando se requiera mejor desempeño, se puede cambiar anonisolatedo@concurrent. Sin embargo, en código legado, es mejor hacer la migración manual.
Global-actor isolated conformances
swift
@MainActor
final class PersonViewModel {
let id: UUID
var name: String
init(id: UUID, name: String) {
self.id = id
self.name = name
}
}
// ❌ Conformance of 'PersonViewModel' to protocol 'Equatable' crosses into main actor-isolated code and can cause data races
extension PersonViewModel: Equatable {
static func == (lhs: PersonViewModel, rhs: PersonViewModel) -> Bool {
lhs.id == rhs.id
}
}
Soluciones:
1. Marcar con nonisolated el método de Equatable (i.e. nonisolated static func ==).
Esto permite usar el método desde cualquier dominio de aislamiento. Sin embargo, al hacer esto, es obligatorio asegurarse que los datos a usar son Sendable.
swift
extension PersonViewModel: Equatable {
nonisolated static func == (lhs: PersonViewModel, rhs: PersonViewModel) -> Bool {
lhs.id == rhs.id && lhs.name == rhs.name
// ❌ Main actor-isolated property 'name' can not be referenced from a nonisolated autoclosure
}
}
2. Conformar un protocolo solo en un actor global (i.e. extension PersonViewModel: @MainActor Equatable).
Con esto se puede comparar sin problema (usando Equatable) siempre que se esté en MainActor. Si se está en otro dominio de aislamiento, habrá un error.
swift
extension PersonViewModel: @MainActor Equatable {
static func == (lhs: PersonViewModel, rhs: PersonViewModel) -> Bool {
lhs.id == rhs.id && lhs.name == rhs.name
}
}
Estrategias de migración
No activar "Approachable Concurrency" de forma grupal. En su lugar, activar las funcionalidades una por una. Luego, hacer un PR por cada "upcoming feature". Esto hará más entendible los cambios.
Al final de todo el proceso sí se activa "Approachable Concurrency".
Algunos consejos:
- Hacerlo de forma iterativa.
-
Sendablepor defecto. - Evitar refactorizar todo durante la marcha. El foco es concurrency.
- Hacer cambios pequeños en un PR que pueda ser revisado rápidaemtne.
- En nuevos proyectos: activar 1) Swift 6.2, 2) aislamiento por defecto en
MainActory 3) "Approachable concurrency". - No aislar todo en
MainActor.
¿Cómo migrar código existente?
- Encontrar código aislado. Posiblemente un solo archivo o clase pequeña.
- Incrementar la validación estricta de concurrencia. Pasar primero a
Targetedy luegoComplete. - Habilitar "Upcoming Features" uno a uno.
- Deshabilitar/reiniciar los settings modificados.
- Someter PR a revisión.
- Cuando todo esté listo, activar Swift 6.2.
Error en tiempo de ejecución
"Block was expected to execute on queue []"
- El
sinkde combine no funciona bien con atributos marcados conMainActor. Nada impide llamarsinkdesde un hilo de segundo plano. Artículo.
for await no puede tener return en su interior.
En lugar de usar return, debe haber continue al interior de for await.
Top comments (0)