DEV Community

GoyesDev
GoyesDev

Posted on

[SC] Sendable

¿Qué diferencia hay entre pasar valores entre hilos en GCD versus en Swift Concurrency?

Igual que en GCD, en Swift Concurrency (SC) va a ser necesario pasar un dato de un hilo a otro. Sin embargo, en SC no se trabaja con hilos sino con "Dominios de Aislamiento" (del inglés "isolation domains").

Al pasar un dato de una función async a otra, se podría estar transfiriendo información entre dos dominios de aislamiento, para lo cual, SC necesita que el dato sea de tipo Sendable para garantizar que sea "thread-safe".

¿Por qué el compilador necesita saber si un valor es thread-safe?

¿Qué es un isolation domain y para qué sirve?

Un Dominio de Aislamiento ("Isolation Domain") define una frontera en la que se puede acceder a un valor o referencia sin el riesgo de tener una carrera de datos.

Hay tres tipos de dominio:

  • nonisolated.
  • actor.
  • Global-actor.

¿Qué restricciones tiene el código nonisolated respecto al estado de otros dominios?

nonisolated es el "Dominio de Aislamiento" por defecto que no tiene restricciones de concurrencia. El código nonisolated:

  • Puede modificar sin problema el estado de otro código del mismo tipo.
  • NO puede modificar el estado de otros Dominios de Aislamiento.

El siguiente método no tiene estado y por eso se puede llamar con seguridad desde cualquier hilo:

// ⚠️ Como nonisolated es el dominio de aislamiento por defecto, es redundante ponerlo.
nonisolated func add(a: Int, b: Int) -> Int {
  a + b
}
func add(a: Int, b: Int) -> Int {
  a + b
}
Enter fullscreen mode Exit fullscreen mode

¿Por qué acceder a propiedades de un actor desde afuera requiere await?

Un actor es un dominio de aislamiento que asegura que todas sus propiedades almacenadas y métodos se ejecutan en un ambiente seguro de un solo hilo, evitando carreras de datos.

Para acceder a los datos almacenados en un actor, se debe usar await para garantizar el acceso correcto. Básicamente sería como esperar hasta que el actor entregue o termine de modificar los datos.

// ⚠️ Se define la estructura de datos como "actor"
actor Library {

  // ⚠️ No se puede acceder directamente a books. Se puede leer
  // usando await, pero no escribir. Es ideal marcarlo private
  var books: [String] = []

  // ⚠️ Notar que los métodos no tienen async. Sin embargo, 
  // al estar definidos dentro del actor, están dentro de su
  // dominio de aislamiento y deben ser accedidos con await
  // desde otro dominio.
  func addBook(_ title: String) {
    books.append(title)
  }
  func getBookList() -> [String] {
    return books
  }

  // ⚠️ Un actor también puede tener métodos nonisolated. 
  // En este caso, para retornar un String escalar que puede
  // se accedido desde cualquier lado SIN await
  nonisolated func libraryName() -> String {
    "A library of books"
  }
}
Enter fullscreen mode Exit fullscreen mode
let library = Library()

// ⚠️ No se puede invocar un método del actor, fuera de este,
// de forma síncrona.
x.addBook("hola")
// ❌ Call to actor-isolated instance method 'addBook' in a synchronous main actor-isolated context. Calls to instance method 'addBook' from outside of its actor context are implicitly asynchronous

Task {
  await library.addBook("Dopamine Nation")
  // ⚠️ Se debe llamar el método con await
  let books = await library.getBookList()
  print(books)
  // Imprime: ["Dopamine Nation"]

  // ⚠️ Se puede leer books con await
  await library.books.forEach { print($0) }
  // Imprime: ["Dopamine Nation"]

  // ⚠️ No se puede escribir books fuera del actor
  x.books.append("Dopamine Nation")
  // ❌ Error en Swift 6: Actor-isolated property 'books' can not be mutated from the main actor. Consider declaring an isolated method on 'Library' to perform the mutation
}
Enter fullscreen mode Exit fullscreen mode

¿En qué se diferencia un global actor como @MainActor de un actor regular?

Mientras que un actor crea un dominio de aislamiento en una sola instancia, un global actor es un dominio de aislamiento compartido por varios tipos de datos, propiedades y funciones. Esto es útil cuando varias partes de la aplicación necesitan funcionar bajo las mismas restricciones.

¿Qué condiciones debe cumplir una API pública para considerarse thread-safe ("seguro entre hilos") según el protocolo Sendable?

El protocolo Sendable indica que cierta interfaz es thread-safe para el compilador. Se considera que una interfaz es segura para usar entre dominios de aislamiento cuando:

  1. No hay modificadores públicos.
  2. Cuenta con un sistema de bloqueo interno (e.g. proteger la escritura de un valor con ayuda de NSLock).
  3. Los modificadores implementan copy-on-write como los tipos de valor.

Muchos tipos de datos de la biblioteca estándar de Swift ya tienen soporte del protocolo Sendable. Luego, el compilador puede hacer que algunos tipos de datos también tengan soporte automático de Sendable:

Por ejemplo, dado que Int: Sendable:

// ✅ SomeStruct conforma implícitamente Sendable
struct SomeStruct {
  var someIntValue: Int
}
Enter fullscreen mode Exit fullscreen mode
// ❌ SomeClass NO conforma implícitamente Sendable
class SomeClass {
  var someIntValue: Int = 0
}
Enter fullscreen mode Exit fullscreen mode

Recite

  • ¿Por qué un struct con una propiedad Int conforma Sendable implícitamente, pero una class con la misma propiedad no?

Porque class es un tipo por referencia, lo que implica que puede ser modificado desde dos dominios de aislamiento diferentes.

  • ¿Puedes explicar con tus propias palabras qué ocurre cuando un valor viaja entre dos dominios de aislamiento?

Swift debe garantizar que el valor puede trasladarse desde un dominio a otro sin el riesgo de una carrera de datos.

  • ¿Cuándo tendría sentido marcar un método como nonisolated dentro de un actor?

Se puede marcar con nonisolated si no es necesario proteger el estado del actor. Esto ocurre, por ejemplo, si el método retorna algún valor escalar o constante.


Review

  • ¿Cuáles son las tres condiciones bajo las cuales una API pública es segura para usarse entre dominios de concurrencia?

  • ¿Qué cambia en Swift 6.2 respecto al comportamiento por defecto de Sendable?

Se introduce nonisolated(nonsending) que permite que las funciones y closures no sean Sendable a no ser de que atraviesen una frontera de aislamiento.


Bibliografía

Top comments (0)