DEV Community

GoyesDev
GoyesDev

Posted on

[SC] Valores globales seguros para concurrencia

Preguntas

¿Qué es una variable global y por qué su uso implica riesgos en contextos de concurrencia?

Una variable global es un valor que puede ser accedido y modificado desde cualquier parte del sistema, esto incluye cualquier contexto asíncrono.

¿Qué error genera el compilador de Swift 6 cuando una variable global es mutable y no está aislada?

Tomemos como ejemplo la clase no aislada ImageCache, que tiene una variable global mutable no aislada llamada shared. En este caso, el compilador saca un error indicando que no es seguro acceder a un estado global mutable desde un entorno concurrente.

nonisolated class ImageCache {
  static var shared = ImageCache()
  // ❌ Static property 'shared' is not concurrency-safe because it is nonisolated global shared mutable state
}
Enter fullscreen mode Exit fullscreen mode

¿Cuáles son las tres estrategias principales para hacer una variable global segura para la concurrencia?

1. Aislar el acceso a la clase con un actor global como @MainActor.

  • Ventaja: @MainActor garantiza que el acceso a ImageCache es serializado.
  • Desventaja: Complica el acceso desde otros dominios de aislamiento o contextos no-concurrentes

Por ejemplo:

@MainActor
class ImageCache {
  static var shared = ImageCache()
  // ✅ No hay error
}
Enter fullscreen mode Exit fullscreen mode
nonisolated class NonConcurrentContext {
  func execute() {
    let sut = ImageCache.shared
    // ❌ Main actor-isolated static property 'shared' can not be referenced from a nonisolated context
  }
}
Enter fullscreen mode Exit fullscreen mode
fileprivate actor ActorContext {
  func execute() {
    let sut = ImageCache.shared
    // ❌ Main actor-isolated static property 'shared' can not be referenced on a nonisolated actor instance
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Conformar Sendable

Esto es:

  • Hacer que la clase sea inmutable.
  • Marcar con final
  • Mutar las variables por medio de locks.

Recordar que el protocolo Sendable indica que el API del tipo en cuestión es thread-safe y se puede usar sin problema entre dominios de aislamiento.

// ✅ final
nonisolated final class ImageCache: Sendable {

  static let shared = ImageCache()
  // ✅ let, no var

  func clearCache() {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode
fileprivate actor ActorContext {
  func execute() {
    let sut = ImageCache.shared
    // ✅ No hay error
  }
}
Enter fullscreen mode Exit fullscreen mode
fileprivate nonisolated class NonConcurrentContext {
  func execute() {
    let sut = ImageCache.shared
    // ✅ No hay error
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Marcar el tipo como Sendable y la propiedad mutable como nonisolated(unsafe)

Si el API no se puede modificar con facilidad (para conformar Sendable) y tiene partes mutables, se puede marcar a estas últimas como nonisolated(unsafe), que es el equivalente a usar @unchecked Sendable. Básicamente se le está diciendo al compilador que el desarrollador se hace responsable de sincronizar el acceso a ese valor global, sin introducir data-races.

Lo ideal es solo modificar el valor global en el arranque de la aplicación y luego leerlo desde otros contextos de concurrencia, para lo cual se puede usar el modificador private(set).

¿Cuándo y por qué se usaría nonisolated(unsafe) en lugar de las otras soluciones?

Cuando el código tiene un patrón de inicialización tardía que no puedo reescribir fácilmente.

¿Qué condiciones debe cumplir una clase para conformar al protocolo Sendable?

  • marcada como final.
  • miembros inmutables.
  • acceso a miembros mutables por medio de locks.

Lectura

¿Por qué aislar una clase con @MainActor puede ser problemático en algunos casos?

El acceso desde otros dominios de aislamiento o nonisolated tendrá que ser usando await.

¿Qué papel juega private(set) cuando se usa junto con nonisolated(unsafe)?

El objetivo es que solo la clase que define el singleton pueda modificar la variable estática y que ningún cliente lo haga.

¿En qué se parece nonisolated(unsafe) al uso de @unchecked Sendable?

Se le dice al compilador que el desarrollador se compromete a garantizar que no hay carreras de datos.


Revisión

Sin volver atrás ¿puedes explicar con tus propias palabras qué hace que una variable global sea peligrosa en Swift 6?

¿Puedes describir de memoria los pasos para convertir ImageCache en una clase segura para la concurrencia?

¿Cuál de las tres soluciones presentadas consideras más adecuada para un proyecto en producción y por qué?

¿Qué riesgos asumes al marcar una variable como nonisolated(unsafe), y cómo podrías mitigarlos?


Bibliografía

Top comments (0)