DEV Community

GoyesDev
GoyesDev

Posted on

Resolviendo errores al conformar protocolos "actor-isolated".

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)
Enter fullscreen mode Exit fullscreen mode

¿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
  }
}
Enter fullscreen mode Exit fullscreen mode

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 {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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 {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode
// @MainActor (no tiene nonisolated)
struct Person: @MainActor Decodable {
  var name: String
  nonisolated enum CodingKeys: CodingKey {
    case name
  }
}
Enter fullscreen mode Exit fullscreen mode

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)


Referencia

Top comments (0)