DEV Community

GoyesDev
GoyesDev

Posted on

[SC] Sendable y Tipos-por-referencia

Preguntas

¿Qué es un tipo por referencia y en qué se diferencia fundamentalmente de un tipo por valor?

Un tipo por referencia es una instancia que se manipula a través de punteros. Esto implica que cuando se modifica el valor a través un puntero, puede haber otro apuntando a la misma dirección de memoria que se vea "afectado".

Los siguientes son tipos de dato por referencia:

  • Clases (class)
  • Closures ((Input) -> (Output))
  • Actores (actor)
  • Metatipos (MyStruct.Type)
  • Protocolos con restricción a class o AnyObject (protocol P: AnyObject { })
  • NSObject
  • Referencias de tipo UnsafePointer & Unmanaged

¿Por qué las clases no pueden conformarse a Sendable tan fácilmente como las estructuras?

Puedo tener dos punteros apuntando a la misma dirección de memoria, y usar cada puntero desde un hilo diferente. En este caso podría haber un "data-race", si ambos hilos tratan de leer y modificar la misma posición de memoria simultáneamente.

¿Qué condiciones debe cumplir una clase para poder marcarse como Sendable?

// ❌ Non-final class 'Counter' cannot conform to 'Sendable'; use '@unchecked Sendable'
class Counter: Sendable {
  // ❌ Stored property 'value' of 'Sendable'-conforming class 'Counter' is mutable
  var value: Int = 0
}
Enter fullscreen mode Exit fullscreen mode

Hay dos problemas en la clase anterior:

  1. No es final.
  2. Tiene un atributo mutable.

El atributo mutable es un problema múltiples dominios de aislamiento potencialmente podrían tener acceso y modificar la misma dirección de memoria, con lo que se generaría un data-race.

Para que una clase sea Sendable, esta debe:

  1. Ser final.
  2. Solo contener propiedades inmutables y Sendable.
  3. No ser sub-class, o únicamente sub-class de NSObject.

Cualquier otro caso (no ser final, ser subclase, o tener propiedades mutables) también puede ser @unchecked Sendable, y debe tener acceso sincronizado vía NSLocking.

Convertir class en actor

Otra alternativa puede ser tener un actor, que automáticamente hace que el tipo de dato sea Sendable, gracias a que los datos se sincronizan a través del executor del actor.

Sin embargo, esto complica mucho la implementación cuando se quiere usar el actor en un contexto de concurrencia sin aislamiento:

actor X {
  var value: Int = 0
}

struct XTests {
  @Test
  func execute() {
    let sut1 = X()
    let sut2 = sut1

    // ❌ Actor-isolated property 'value' can not be mutated from a nonisolated context
    sut2.value += 1
    // ⚠️ Un actor es un dato por referencia
    #expect(sut1.value == 1)
    // ❌ Actor-isolated property 'value' can not be referenced from a nonisolated context
  }
}
Enter fullscreen mode Exit fullscreen mode

Antes de convertir la clase en actor, vale la pena preguntarse:

  1. ¿Este class puede ser un struct mejor?
  2. ¿Este class necesita ser mutable y no-final?
  3. ¿Este class va a ser modificado desde distintos dominios de aislamiento? ¿O basta con marcarlo con @MainActor?

¿Por qué las clases no finales (non-final) no pueden ser Sendable?

A pesar de que tenga una súper-clase que cumpla con las condiciones para ser Sendable, el día de mañana podría aparecer una sub-clase que (1) agregue un atributo mutable o que (2) no sea Sendable. En ese caso, el compilador ya no puede asegurar que no haya data-races. Si luego se referencian unas subclases no-Sendable a través de la interfaz de la super-clase (por polimorfismo) que supuéstamente es Sendable, entonces estaría incumpliendo con el contrato.

Técnicamente se podría re-compilar todo el proyecto revisando todas las subclases; sin embargo, esto impactaría las optimizaciones de compilación.

¿Cuál es la única superclase permitida en una clase Sendable, y por qué?

Solo se permite heredar de NSObject al conformar Sendable, porque puede haber protocolos de APIs basadas en Obj-C, que conforman NSObjectProtocol, que requieren heredar de NSObject.

¿Cómo puede la composición ayudar a crear tipos por referencia que sean Sendable?

Puede ser que nuestro API ya tenga un tipo de dato class que sea Sendable y nosotros necesitemos agregarle comportamiento. En este caso, debemos buscar añadirlo por composición.


Lectura, recitación y repaso

¿Qué alternativas existen antes de convertir una clase en un actor para resolver problemas de concurrencia?

  • ¿La clase puede ser struct?
  • ¿Necesita ser mutable? ¿Puede ser final?

¿Por qué el compilador no puede simplemente validar todos los casos de uso de una clase no final para determinar si es segura como Sendable?

Se pierden optimizaciones de compilación.

¿En qué situación tendría sentido usar @MainActor en lugar de convertir una clase en actor?

Convertir una clase en actor provocaría que tenga que acceder con await a los datos desde contextos de concurrencia sin aislamiento.

En ciertos escenarios sabemos que vamos a modificar la clase solo desde el hilo principal, así que podemos marcarla solo con @MainActor.


Bibliografía

Top comments (0)