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
}
¿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:
@MainActorgarantiza que el acceso aImageCachees 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
}
nonisolated class NonConcurrentContext {
func execute() {
let sut = ImageCache.shared
// ❌ Main actor-isolated static property 'shared' can not be referenced from a nonisolated context
}
}
fileprivate actor ActorContext {
func execute() {
let sut = ImageCache.shared
// ❌ Main actor-isolated static property 'shared' can not be referenced on a nonisolated actor instance
}
}
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() {
// ...
}
}
fileprivate actor ActorContext {
func execute() {
let sut = ImageCache.shared
// ✅ No hay error
}
}
fileprivate nonisolated class NonConcurrentContext {
func execute() {
let sut = ImageCache.shared
// ✅ No hay error
}
}
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.
Top comments (0)