Para conservar la comparación e intercambio atómicos al migrar un modelo de Swift a un actor, mantén la comparación y la asignación dentro de un único método síncrono aislado al actor. Los llamadores usan await, pero el método en sí no debe suspenderse entre la comprobación y el reemplazo.
Este tutorial está dirigido a un desarrollador de Mac que migra un pequeño modelo de escritorio que antes estaba protegido por un bloqueo. La tarea concreta consiste en reemplazar una etiqueta de forma sintética solo si su valor actual sigue coincidiendo con un valor esperado. El ejemplo opera completamente en memoria y no usa solicitudes de red, registros externos ni persistencia.
Define el contrato de valores antes de cambiar el modelo
Para este ejemplo, compareAndReplace(expected:replacement:) tiene tres reglas: comparar la etiqueta actual con expected; si coinciden, asignar replacement y devolver true; de lo contrario, devolver false sin cambiar la etiqueta. La comparación usa la igualdad de valores de Swift. La documentación de Equatable de la biblioteca estándar define esa comparación mediante == y distingue la igualdad de valores de la identidad de las instancias de clase.
La comparación debe usar el valor vigente cuando se ejecuta la operación. Una instantánea anterior puede proporcionar el valor esperado, pero no puede determinar que el reemplazo deba tener éxito más adelante. Esa decisión pertenece a la operación que controla la mutación.
| Valor actual | Valor esperado | Reemplazo | Resultado | Valor posterior |
|---|---|---|---|---|
"Circle" |
"Circle" |
"Square" |
true |
"Square" |
"Square" |
"Circle" |
"Triangle" |
false |
"Square" |
"Square" |
"Square" |
"Square" |
true |
"Square" |
La última fila es intencional: el éxito significa que la comparación coincidió y se aceptó la asignación. No significa que el valor visible haya cambiado. Cambiar esa convención de retorno durante la migración alteraría el contrato.
En una implementación basada en bloqueos, toda la comparación y la asignación deben estar dentro de la misma sección crítica protegida. Apple documenta que Foundation NSLock permite proteger una sección crítica para que se ejecute de forma atómica. Para la migración al actor, conserva ese límite de la operación: traslada toda la decisión al actor, en lugar de repartir sus partes entre los llamadores.
Aquí, «atómico» significa que ninguna otra operación de este actor puede intercalarse entre la comparación y la asignación. Este ejemplo no afirma nada sobre instrucciones atómicas de hardware ni sobre la ejecución sin bloqueos.
Por qué el await del llamador no interrumpe la operación
Un método síncrono de un actor puede requerir await cuando se llama desde fuera de su dominio de aislamiento. El llamador puede tener que suspenderse mientras espera para entrar en el actor. Una vez que se ejecuta el método, su cuerpo síncrono habitual no tiene ningún punto de suspensión en el que otra tarea pueda entrar y cambiar el estado de ese mismo actor. Swift documenta esta distinción en SE-0306: Actores.
La guía del lenguaje recomienda encapsular las actualizaciones en métodos síncronos de actor porque estos no pueden contener posibles puntos de suspensión. La sesión de Apple sobre actores también explica que el código síncrono del actor termina sin que otra tarea interrumpa su ejecución aislada al actor. Estas son las garantías en las que se basa esta implementación. Consulta Concurrencia y Protege el estado mutable con actores de Swift.
Por tanto, el límite adecuado es una llamada al actor que contenga tanto la condición como la mutación resultante. Un llamador puede leer primero una instantánea, hacer un cálculo local y luego solicitar un reemplazo condicional. El actor debe volver a comprobar la condición al procesar esa solicitud.
¿Qué sale mal al separar las lecturas y escrituras?
Supongamos que una migración expone métodos separados para obtener una instantánea y realizar un reemplazo incondicional. Un llamador lee "Circle". Otro cambia la etiqueta a "Square". Entonces el primero escribe "Triangle" basándose en su lectura anterior. Cada método individual podría respetar el aislamiento del actor y, aun así, la secuencia completa incumpliría el contrato previsto de comparación e intercambio.
Este cronograma ilustrativo explica el defecto sin depender de un planificador de tareas específico. El aislamiento del actor protege cada operación; el programador aún debe elegir una operación lo bastante amplia para preservar la condición de la aplicación.
Crea un ejemplo local completo en tu Mac
Usa una cadena de herramientas de Swift 6 instalada. La guía oficial de instalación para macOS de Swift describe la instalación mediante Swiftly y el desarrollo con Xcode. En Terminal, comprueba el compilador seleccionado antes de continuar:
swiftc --version
Guarda el siguiente programa completo como CompareReplace.swift en una carpeta de trabajo. Solo usa la biblioteca estándar de Swift, disponible sin una importación explícita. El archivo define el actor, su estado inicial, todas las entradas de ejemplo y un punto de entrada asíncrono.
actor ShapeLabelModel {
private var label: String
init(initial: String) {
label = initial
}
func snapshot() -> String {
label
}
func compareAndReplace(
expected: String,
replacement: String
) -> Bool {
guard label == expected else {
return false
}
label = replacement
return true
}
}
@main
struct CompareReplaceDemo {
static func main() async {
let model = ShapeLabelModel(initial: "Circle")
let matched = await model.compareAndReplace(
expected: "Circle", replacement: "Square"
)
let afterMatch = await model.snapshot()
precondition(matched && afterMatch == "Square")
let stale = await model.compareAndReplace(
expected: "Circle", replacement: "Triangle"
)
let afterStale = await model.snapshot()
precondition(!stale && afterStale == "Square")
let unchanged = await model.compareAndReplace(
expected: "Square", replacement: "Square"
)
precondition(unchanged)
let contested = ShapeLabelModel(initial: "Circle")
async let first = contested.compareAndReplace(
expected: "Circle", replacement: "Square"
)
async let second = contested.compareAndReplace(
expected: "Circle", replacement: "Triangle"
)
let (firstWon, secondWon) = await (first, second)
let finalLabel = await contested.snapshot()
precondition(firstWon != secondWon)
precondition(
finalLabel == (firstWon ? "Square" : "Triangle")
)
let cycle = ShapeLabelModel(initial: "Circle")
let oldSnapshot = await cycle.snapshot()
let movedAway = await cycle.compareAndReplace(
expected: "Circle", replacement: "Square"
)
let movedBack = await cycle.compareAndReplace(
expected: "Square", replacement: "Circle"
)
let matchedAgain = await cycle.compareAndReplace(
expected: oldSnapshot, replacement: "Triangle"
)
let afterCycle = await cycle.snapshot()
precondition(movedAway && movedBack && matchedAgain)
precondition(afterCycle == "Triangle")
print("Matching replacement: \(matched)")
print("Stale replacement: \(stale)")
print("Equal replacement: \(unchanged)")
print("Exactly one competing replacement succeeded.")
print("A restored value matches the old snapshot.")
}
}
Compílalo y ejecútalo desde esa carpeta. La opción -parse-as-library admite el punto de entrada explícito @main, mientras que -swift-version 6 selecciona el modo de lenguaje Swift 6.
swiftc -swift-version 6 -parse-as-library CompareReplace.swift -o compare-demo
./compare-demo
La salida esperada, deducida de las comprobaciones del programa, es:
Matching replacement: true
Stale replacement: false
Equal replacement: true
Exactly one competing replacement succeeded.
A restored value matches the old snapshot.
El código se contrastó con la documentación, pero no se ejecutó aquí porque este entorno no tiene un compilador de Swift. Las precondiciones incluidas permiten hacer comprobaciones locales al ejecutarlo; no afirman que se haya ejecutado.
Qué demuestran las llamadas en competencia
Las dos declaraciones async let crean tareas secundarias sin esperar al primer resultado antes de iniciar la segunda. Ese uso de concurrencia estructurada sigue la explicación de llamadas asíncronas concurrentes de la guía del lenguaje. Ambos llamadores esperan "Circle" y cada uno propone un reemplazo distinto.
En esta configuración concreta, solo una llamada tiene éxito: la que se ejecute primero cambia el valor para que deje de ser "Circle"; la otra falla entonces su comparación. La prueba admite cualquiera de las dos como ganadora. Swift no garantiza que las tareas que esperan a un actor se ejecuten en el orden en que lo esperaron inicialmente, tal como se documenta en SE-0306.
El ejemplo también separa las llamadas con await de las expresiones precondition. Primero guarda cada resultado en una constante local y luego comprueba esa constante. Así, el trabajo asíncrono queda fuera de las expresiones de aserción síncronas.
Mantén la preparación asíncrona fuera de la comparación final
Una versión futura podría calcular una etiqueta propuesta de manera asíncrona mediante otro trabajo local sintético. Completa esa preparación antes de llamar al método síncrono de comparación e intercambio. Pasa juntos el valor esperado original y el reemplazo calculado; trata false como un intento rechazado.
Un método asíncrono del actor que comprueba la igualdad, espera a que termine la preparación y luego asigna incondicionalmente tiene una brecha. Durante la suspensión, otra tarea puede cambiar el estado del actor. El método reanudado se basaría en una condición que quizá ya no se cumpla. El modelo de reentrada de actores documentado por Swift permite explícitamente esta intercalación.
Si la preparación se realiza en un método asíncrono del actor, llama al método síncrono de comparación después de que termine la preparación. Ese método final debe realizar una nueva comparación. Una comprobación hecha antes de la suspensión puede ayudar a evitar trabajo innecesario, pero no puede autorizar la asignación posterior.
Si no hay coincidencia, conserva el resultado propuesto si la interfaz de escritorio necesita mostrarlo, obtén una instantánea nueva y decide si recalcular o informar del conflicto. Reemplazar automáticamente el valor esperado y reintentar cambiaría la solicitud de «reemplazar el valor que observé» a «seguir intentándolo con valores más recientes». Convierte ese comportamiento en una decisión explícita de producto.
No mantengas el antiguo bloqueo de Foundation durante un await. Apple exige que un NSLock se desbloquee en el mismo hilo que lo bloqueó. El diseño de actor que se muestra aquí logra el resultado necesario mediante una operación aislada, síncrona y breve, sin mantener un bloqueo durante el trabajo asíncrono.
Conserva la semántica de igualdad y reconoce sus límites
La comprobación del ciclo muestra un límite sutil. Un llamador toma una instantánea de "Circle"; el modelo cambia a "Square" y luego vuelve a "Circle". La comparación con la instantánea anterior tiene éxito porque el contrato compara el valor actual, no la secuencia de cambios que lo precedieron.
Si tu requisito real es «reemplazar solo si no se ha aceptado ninguna mutación desde mi instantánea», la comparación de valores por sí sola no basta. Un diseño compatible consiste en devolver una instantánea que contenga tanto un valor como un token de revisión y luego comprobar el token y actualizar el valor juntos dentro de un método síncrono del actor. Ese es un contrato distinto y más estricto. Define cómo avanzará el token y qué ocurrirá si se agota antes de implementarlo; no lo introduzcas silenciosamente durante una migración que conserva los valores.
Para modelos más grandes, usa un valor de instantánea cuya igualdad abarque todos los campos relevantes para el reemplazo. Swift puede sintetizar Equatable para una estructura cuando todas sus propiedades almacenadas cumplen ese protocolo, según la referencia de Equatable. Una implementación de igualdad que ignore un campo relevante permitiría reemplazos pese a los cambios en ese campo.
Mantén los valores que cruzan el límite del actor aptos para el uso concurrente. La documentación de Sendable explica que los valores que cumplen ese protocolo pueden pasar entre dominios de concurrencia y que las estructuras que lo cumplen deben contener miembros que también lo cumplan. Envolver una referencia mutable arbitraria en una estructura no establece esa propiedad.
Para este modelo local, la migración está completa cuando el actor es dueño de la etiqueta mutable, cada reemplazo condicional se realiza mediante la operación combinada y los llamadores interpretan su resultado booleano según el contrato original. Mantén juntas la comparación final y la asignación, y deja que el flujo de trabajo de escritorio decida qué significa un intento rechazado.
Fuentes
- NSLock — Documentación para desarrolladores de Apple.
- Equatable — Documentación para desarrolladores de Apple.
- Sendable — Documentación para desarrolladores de Apple.
- Concurrencia — The Swift Programming Language, proyecto Swift.
- Actores — SE-0306 — Swift Evolution, proyecto Swift.
- Protege el estado mutable con actores de Swift — Apple Developer, WWDC21.
- Instala Swift — macOS — Swift.org.