DEV Community

GoyesDev
GoyesDev

Posted on

[SC] Aislamiento basado en regiones y la palabra clave "sending"

Preguntas

¿Qué es el aislamiento basado en regiones (region-based isolation) y por qué existe?

El sistema original de Sendable era demasiado estricto y bloqueaba patrones de código seguros que usaban código no-Sendable. Por esto se creó el "aislamiento basado en regiones", el cual es un mecanismo del compilador que verifica si un tipo no-Sendable se usa únicamente dentro de un scope.

¿En qué situaciones el compilador ignora errores de tipos no-Sendable?

Si se crea un valor localmente dentro de una función, no se muta y no se accede a él después de transferirlo a otro dominio de aislamiento, entonces el compilador puede garantizar que no hay acceso simultáneo.

¿Qué son los diagnósticos sensibles al flujo de control (control flow-sensitive diagnostics)?

El "diagnóstico sensible al flujo de control" es un análisis de código estático que hace el compilador para determinar que un tipo no-Sendable puede transferirse de forma segura a otro dominio de aislamiento.

El compilador revisa si el valor se usa o no después del punto de transferencia.

¿Cuándo deja de compilar el código aunque se use aislamiento basado en regiones?

Cuando se usa el valor después de haberlo transferido a otro dominio de aislamiento.

¿Qué problema resuelve la palabra clave sending?

sending declara explícitamente una transferencia de propiedad, por lo que compilador mueve las verificaciones hacia el interior del método receptor.

De esta forma se puede silenciar un posible error cuando el desarrollador sabe que no hay riesgo de data-race.

¿Cuál es la diferencia entre usar sending en parámetros y en valores de retorno?

  • sending como parámetro garantiza que el invocador del método no puede volver a utilizar la propiedad. Además, solo se puede transferir a un solo dominio de aislamiento.
  • sending como retorno indica que la función no conserva referencia al valor y lo transfiere hacia fuera.

¿Por qué un tipo no-Sendable dentro de un método local no genera error al pasarse a una Task?

El compilador es capaz de identificar que el valor nació en esa función, nadie más tiene referencia a él y, si no se accede después del Task, no puede haber un acceso simultáneo.

¿Qué ocurre si accedes a un valor después de haberlo transferido con sending?

El compilador indica que se está usando el valor transferido y los accesos posteriores podrían generar una condición de carrera.


Ejemplos con código

Article es una clase no-Sendable.

nonisolated class Article {
  var title: "String"

  init(title: "String) {"
    self.title = title
  }
}
Enter fullscreen mode Exit fullscreen mode

Puedo recibir un Article y pasárselo de forma segura UN SOLO dominio de aislamiento. Para ello, puedo modificar un parámetro con sending:

nonisolated final class ArticleConsumer {
  func execute(article: sending Article) {
    Task {
    // ❌ Sending value of non-Sendable type '@concurrent () async -> ()' risks causing data races
      print(article.title)
    }
    print(article.title)
  }
}
Enter fullscreen mode Exit fullscreen mode

En este caso, la clase ArticleConsumer vive en el dominio nonisolated. En el método execute(article:), se transfiere article desde nonisolated hasta la región sin estructura del Task. En este punto, el método que transfiere (i.e. execute(article:) ya no puede volver a usarlo (i.e. llamar print(article.title).

Con lo anterior quiero decir que al usar sending solo se puede transferir el dato a través de UN SOLO dominio de aislamiento.

Sucede lo mismo en el siguiente escenario, donde el dominio de aislamiento al que se transfiere article es un actor en lugar de un Task:

actor ArticleStore {
  func save(_ article: Article) {
    print(article.title)
  }
}
Enter fullscreen mode Exit fullscreen mode
nonisolated final class ArticleConsumerWithActor {
  func execute(article: sending Article) async {
    let store = ArticleStore()
    await store.save(article)
    // ❌ Sending 'article' risks causing data races
    print(article.title)
  }
}
Enter fullscreen mode Exit fullscreen mode

No obstante, todo este análisis funciona porque tanto ArticleConsumerWithActor como ArticleConsumer están marcados con nonisolated. Si no tuvieran ese modificador y tuviéramos las propiedades de configuración del proyecto SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor y SWIFT_VERSION = 6.0, entonces tanto ArticleConsumerWithActor como ArticleConsumer vivirían en @MainActor implícitamente.

Luego, cuando se crea un Task dentro del contexto aislado MainActor, la Task hereda el mismo aislamiento MainActor. De esta forma, tanto la Task como el cuerpo del método ArticleConsumer.execute(article:) vivirán en @MainActor. Más tarde, cuando el compilador hace el análisis de código estático para el dianóstico de regiones encontrará que no se está cruzando ninguna región y usar article después del Task es legal.

// ⚠️ hereda @MainActor implícitamente
final class ArticleConsumer {
  func execute(article: sending Article) {
    Task {
      // ✅ No saca ningún error
      print(article.title)
    }
    print(article.title)
  }
}
Enter fullscreen mode Exit fullscreen mode

Recitación

Explica con tus propias palabras cómo el compilador determina si hay riesgo de data race.

El compilador sigue el código y marca el punto donde un valor se transfiere de un dominio a otro. Si no se vuelve a transferir, no hay problema. Si, en cambio, se transfiere, entonces hay dos zonas con acceso potencial simultáneo y saca un error.

¿Por qué agregar un print después de la transferencia rompe la compilación?

La Task podría estar ejecutándose en paralelo al tiempo que el print. Esas dos zonas pueden acceder simultáneamente al mismo valor, y esto es lo que quiere prevenir Swift.


Revisión

¿En qué casos concretos usarías sending en lugar de conformar un tipo a Sendable?

La clase en cuestión no puede ser Sendable. Pueden tener estado mutable pero en cierto contexto pasa de forma controlada y sin acceso concurrente.

¿Cómo reduce el aislamiento basado en regiones la necesidad de usar Sendable?

El compilador analiza cada caso específicamente. Si en un scope es seguro usar un valor, no es necesario que conforme Sendable.


Bibliografía

Top comments (0)