Swift 6 introdujo varios cambios para promover un entorno concurrente seguro. Entre ellos está el ejecutar el código en el hilo principal por defecto y también conformar protocolos de forma aislada por concurrencia ("actor-isolated").
Contexto
- En Swift, un actor garantiza exclusión mutua: su estado interno solo puede ser accedido desde dentro de su propio aislamiento de concurrencia.
- Cuando un protocolo define una propiedad o método, por defecto se interpreta como aislada al contexto del tipo que la implementa. Eso quiere decir que si un actor conforma ese protocolo, esa propiedad o método estaría aislada al actor y para accederla desde fuera tendría que usar await.
¿Qué pasa con nonisolated?
Cuando cierto código está aislado por actor (actor-isolated) y va a ser invocado por otro actor, se necesita envolver el llamado con async/await. Si, en cambio, el código no está aislado por el actor (está marcado como nonisolated), estoy diciendo al compilador que no necesito garantizar que el cliente del código esté en el mismo actor.
Cuando marco una propiedad o método de un protocolo como nonisolated, le estoy diciendo al compilador:
"Este requisito no depende del aislamiento de concurrencia del tipo que lo implemente. Puede ser accedido de manera síncrona y sin
await, incluso si la conformidad la da un actor".
Por ejemplo:
protocol IdentifiableEntity {
var id: String { get } // aislada si lo implementa un actor
nonisolated var typeName: String { get } // no depende del aislamiento
}
actor User: IdentifiableEntity {
let id: String
init(id: String) { self.id = id }
nonisolated var typeName: String { "User" }
}
let u = User(id: "123")
// Para acceder al id necesito await porque está aislado al actor:
let id = await u.id
// Pero el typeName es nonisolated, así que puedo llamarlo directamente:
print(u.typeName)
¿Qué pasa cuando conformamos un protocolo en contextos aislados (actor-isolated)?
Podríamos crear un protocolo con un método o propiedad de tipo nonisolated:
protocol SomeProtocol {
nonisolated var name: String { get }
}
class SomeClass: SomeProtocol {
var name: String
init(name: String) {
self.name = name
}
}
Sin embargo, al tratar de conformar directamente este protocolo obtenemos el error:
Conformance of 'SomeClass' to protocol 'SomeProtocol' crosses into main actor-isolated code and can cause data races
Como en Swift 6 el código se ejecuta por defecto en el actor principal ("main-actor"), se asume que SomeClass está en el hilo principal. No obstante, la propiedad name del protocolo está en un contexto nonisolated, lo cual significa que podría ser accedido sin usar await desde otros hilos. Esto puede provocar una condición de carrera.
Solución 1: Que todo sea nonisolated
En este escenario, la clase SomeClass va a ser declarada como nonisolated. Se supone que originalmente la clase estaba aislada en un actor: ¿por qué querríamos alterar a TODA la clase cuando solo una pequeña parte requiere ser corregida? Además, muy probablemente los clientes de esta clase también estén asilados en el actor principal, así que no tiene sentido hacer que SomeClass ahora sea nonisolated.
nonisolated class SomeClass: SomeProtocol {
// ...
}
Solución 2: Conformar el protocolo en el Actor Principal
En este caso, podemos conformar el protocolo dentro del actor principal:
class SomeClass: @MainActor SomeProtocol {
// ...
}
Aquí, SomeClass sigue estando bajo el MainActor, junto con la conformidad al protocolo. Luego, cualquier acceso a name se entiende que ocurre siempre en el Main Actor, por lo que no hay que marcarla nonisolated.
No obstante, si el protocolo tenía un requisito que parecía necesitar nonisolated, al aislar la conformidad con @MainActor ya no hace falta así que: ¿De qué sirvió marcar nonisolated en primer lugar?
Resolviendo: Conformación de Encodable puede provocar condiciones de carrera cuando se relacione con código aislado en el hilo principal
El siguiente código trae un error de compilación:
struct Person: Codable {
var name: String
}
Conformance of 'Person' to protocol 'Encodable' crosses into main actor-isolated code and can cause data races
El error surge porque conformar este protocolo desde una estructura o clase aislada al actor principal genera que el método encode(to:) esté aislado en el actor principal. Sin embargo, el protocolo requiere que ese método sea nonisolated.
Como la tarea de decodificar el DTO puede ser computacionalmente costosa, es mejor hacerla en un hilo de segundo plano. Si la estructura es muy pequeña, entonces se puede considerar hacer la decodificación en primer plano. Con base en esto, podríamos tener estas dos soluciones:
// nonisolated (no tiene @MainActor)
nonisolated struct Sample: Decodable {
var name: String
nonisolated enum CodingKeys: CodingKey {
case name
}
}
// @MainActor (no tiene nonisolated)
struct Person: @MainActor Decodable {
var name: String
nonisolated enum CodingKeys: CodingKey {
case name
}
}
Notar que, en caso de necesitar tener CodingKeys, es necesario marcar la enumeración con nonisolated porque CodingKeys necesita conformar CustomDebugStringConvertable, que requiere que Self sea Sendable (cosa que no se puede si es @MainActor)
Top comments (0)