Guarda la tarea del cálculo dentro de un actor antes de la primera suspensión y luego permite que las llamadas concurrentes esperen esa misma tarea. Este recorrido crea un ejemplo local de suma de cuadrados para una utilidad de Mac, con reglas explícitas para la cancelación, los errores y la limpieza.
¿Qué deberían compartir las llamadas concurrentes?
Supongamos que dos partes de una utilidad de escritorio para Mac necesitan el mismo resumen calculado: una vista previa y un panel de detalles. Cada una solicita la suma de los cuadrados del arreglo sintético [1, 2, 3, 4]. Ambas deberían recibir 30, y las solicitudes que se unan a la misma ejecución activa deberían compartir una sola ejecución.
La implementación que sigue proporciona a cada instancia del calculador un arreglo de entrada inmutable. Comparte la tarea actual mientras esta ocupa el espacio de ejecución del actor y luego lo libera cuando una llamada observa que terminó. Una solicitud posterior puede iniciar otro cálculo. Esto permite compartir trabajo en curso; conservar indefinidamente las respuestas ya calculadas requeriría una política de almacenamiento en caché independiente.
El contrato de valores es deliberadamente acotado:
- Acepta como máximo 100 000 enteros, cada uno entre
0y1,000. - Devuelve la suma de sus cuadrados; un arreglo vacío devuelve
0. - Asigna un identificador de ejecución para que las llamadas puedan distinguir una ejecución compartida de totales simplemente iguales.
- Propaga los errores del cálculo a las llamadas que esperan esa ejecución.
- Permite que una llamada individual cancele su espera sin cancelar el proceso compartido.
El total máximo permitido es 100,000 × 1,000² = 100,000,000,000, que cabe en el Int de 64 bits de Mac. Estos límites forman parte del contrato de este ejemplo. Cambiarlos requiere reconsiderar el desbordamiento aritmético y el uso de recursos.
¿Por qué un actor sigue necesitando una tarea almacenada?
Un actor protege su estado mutable, pero un método asíncrono de un actor puede suspenderse. Durante esa suspensión, otra llamada puede entrar en el actor. La guía del lenguaje de Apple explica el aislamiento de actores y los puntos de suspensión, mientras que la sesión de Apple sobre actores muestra cómo las suposiciones hechas antes de un await pueden quedar obsoletas. Concurrency — The Swift Programming Language, Protect mutable state with Swift actors.
Por lo tanto, comprobar si ya existe una respuesta y luego esperar un cálculo nuevo no impide el trabajo duplicado. Otra llamada puede llegar antes de que exista la respuesta. En su lugar, publica el identificador de la tarea de forma síncrona: inspecciona el espacio de ejecución, crea un proceso solo si está vacío, lo almacena y luego espera su resultado.
Apple documenta que una tarea puede comenzar inmediatamente después de crearse. Acceder a su value asíncrono espera a que termine y propaga cualquier error generado. Por eso, el actor coordina la propiedad de un solo identificador; las llamadas obtienen sus resultados a través de él. Task, value.
Crea el ejemplo local completo
Usa un compilador de Swift 6 en macOS 13 o posterior. El requisito de macOS aquí se debe a la llamada demostrativa Task.sleep(for:). Esa llamada se suspende sin bloquear el hilo subyacente; la breve pausa facilita observar solicitudes simultáneas. No realiza ninguna parte del cálculo aritmético y puede eliminarse después. sleep(for:tolerance:clock:).
Guarda el siguiente programa completo como SharedCalculation.swift. Usa la configuración predeterminada normal, no aislada, para la compilación de línea de comandos que se muestra a continuación. Foundation proporciona UUID, cuyas conformidades documentadas incluyen Sendable y Equatable; el identificador etiqueta cada ejecución y permite una limpieza condicional. UUID.
import Foundation
struct CalculationResult: Sendable {
let total: Int
let runID: UUID
}
enum CalculationError: Error {
case invalidInput
}
actor SharedCalculation {
private struct Flight {
let id: UUID
let task: Task<CalculationResult, Error>
}
private let samples: [Int]
private var flight: Flight?
init(samples: [Int]) {
self.samples = samples
}
func value() async throws -> CalculationResult {
try Task.checkCancellation()
let active: Flight
if let existing = flight {
active = existing
} else {
let id = UUID()
let snapshot = samples
let task = Task.detached(priority: .userInitiated) {
[snapshot, id] () async throws -> CalculationResult in
guard snapshot.count <= 100_000,
snapshot.allSatisfy({ (0...1_000).contains($0) })
else {
throw CalculationError.invalidInput
}
// Demonstration only: allow overlap to be observed.
try await Task.sleep(for: .milliseconds(100))
let total = snapshot.reduce(0) { sum, sample in
sum + sample * sample
}
return CalculationResult(total: total, runID: id)
}
active = Flight(id: id, task: task)
flight = active
}
defer {
// An older waiter must not clear a newer flight.
if flight?.id == active.id {
flight = nil
}
}
let result = try await active.task.value
try Task.checkCancellation()
return result
}
}
@main
struct Demo {
static func main() async throws {
let calculation = SharedCalculation(samples: [1, 2, 3, 4])
async let preview = calculation.value()
async let details = calculation.value()
async let inspector = calculation.value()
let (a, b, c) = try await (preview, details, inspector)
precondition(a.total == 30 && b.total == 30 && c.total == 30)
let observedRuns = Set([a.runID, b.runID, c.runID]).count
print("Total: \(a.total)")
print("Runs observed across three requests: \(observedRuns)")
let later = try await calculation.value()
precondition(later.total == 30)
print("Later request started another run: \(later.runID != c.runID)")
}
}
Compílalo y ejecútalo desde el directorio que contiene el archivo:
swiftc -swift-version 6 -parse-as-library SharedCalculation.swift -o SharedCalculation
./SharedCalculation
Las aserciones aritméticas deberían pasar. Cuando las tres llamadas iniciales entran antes de que se libere el espacio de ejecución, el recuento de ejecuciones observado es 1. La solicitud posterior llega después de que esas llamadas retornan, así que inicia otro cálculo. La pausa favorece la simultaneidad, pero no garantiza la planificación: una solicitud inicial que se retrase lo suficiente podría iniciar legítimamente otra ejecución. Swift no estaba disponible en el entorno de investigación, así que el programa se revisó, pero no se compiló ni se ejecutó aquí.
El proceso usa una tarea desacoplada porque este cálculo pertenece al calculador compartido y no a una de las llamadas solicitantes, y porque su aritmética debería ejecutarse independientemente del actor coordinador. Solo captura la instantánea de entrada y el identificador. Apple recomienda tareas hijas estructuradas cuando permiten expresar la duración requerida; las tareas desacopladas exigen decisiones explícitas sobre su duración y cancelación. El ejemplo también especifica explícitamente su prioridad. detached(name:priority:operation:).
¿Cómo evita la limpieza borrar el cálculo siguiente?
La secuencia de comprobación y almacenamiento no contiene ningún await, así que una segunda llamada al actor no puede intercalarse entre la comprobación del espacio vacío y la publicación de la nueva ejecución. Cuando la primera llamada se suspende en active.task.value, otra llamada puede entrar, encontrar esa ejecución y esperar el mismo identificador.
La limpieza necesita su propia comprobación de identidad porque varias llamadas pueden reanudarse desde la misma tarea completada en momentos distintos. Considera esta secuencia:
| Evento | Espacio de ejecución |
|---|---|
| Las llamadas A y B se unen a la ejecución X | X |
| X termina; A se reanuda y libera X | Vacío |
| La llamada C entra e inicia la ejecución Y | Y |
| B se reanuda desde X y realiza la limpieza | Y debe permanecer |
Una asignación incondicional de nil por parte de B borraría el identificador de Y. Entonces, una cuarta llamada podría iniciar trabajo duplicado mientras Y todavía está en curso. Comparar flight?.id con active.id hace que la limpieza se limite a la ejecución a la que realmente se unió cada llamada. Esta secuencia se deriva de la implementación y del comportamiento documentado de reentrancia. Protect mutable state with Swift actors.
El límite exacto de lo que se comparte es el espacio de ejecución almacenado. Es posible que un proceso ya haya terminado mientras sus llamadas en espera aún no se han reanudado para liberar el espacio. Una solicitud que entre durante ese intervalo todavía puede recibir el resultado de esa tarea completada. Una vez liberado el espacio, una nueva solicitud inicia otra ejecución. Este breve intervalo posterior a la finalización forma parte del contrato.
¿Qué ocurre si una llamada se cancela o falla el cálculo?
La cancelación en Swift es cooperativa: el código debe decidir dónde comprobarla y qué hacer. Este ejemplo comprueba si la tarea solicitante está cancelada antes de unirse y de nuevo tras recibir correctamente el resultado compartido. Task — Task Cancellation.
Una llamada que ya se haya cancelado en la primera comprobación genera un error sin iniciar ni unirse a ningún trabajo. Una llamada que se cancele mientras espera sigue esperando al proceso compartido; después de que este devuelva un resultado correcto, la segunda comprobación genera CancellationError. Las demás llamadas aún pueden recibir el resultado porque esta implementación nunca transmite al proceso la cancelación de una llamada individual.
Esta política no proporciona una cancelación inmediata de la espera. Si todas las llamadas se cancelan, el proceso igualmente termina. Es una elección intencional para este cálculo local acotado. Un diseño que requiera que cada llamada pueda retirarse de inmediato necesitaría llevar un registro adicional de quienes esperan y definir claramente cuándo debería detenerse el proceso compartido.
Una entrada no válida hace que el proceso genere CalculationError.invalidInput. Las llamadas que ya comparten esa tarea reciben su error mediante value, y la limpieza diferida de cada una libera el espacio correspondiente. Una solicitud posterior vuelve a intentar el mismo valor de entrada inmutable y vuelve a fallar. No se sustituye ningún total de forma silenciosa ni se descartan elementos no válidos. value.
También hay un detalle explícito sobre el orden de los errores: si una llamada se cancela mientras el proceso falla, el error del proceso se propaga antes de la segunda comprobación de cancelación. Si el proceso tiene éxito, la comprobación de cancelación de la llamada determina si esta devuelve el resultado.
¿Cómo verificar y usar este patrón en una utilidad de Mac?
Los totales iguales por sí solos no demuestran que se haya compartido la ejecución: tres cálculos separados de esta entrada también producen 30. Al comprobar la simultaneidad, compara los identificadores de ejecución y verifica el resultado numérico por separado.
Para una prueba de concurrencia determinista, sustituye la pausa demostrativa por una barrera asíncrona controlable y registra cuándo se admite cada llamada a la ejecución. Mantén el proceso detenido hasta que todas las llamadas previstas se hayan unido y luego libéralo. Comprueba que solo se haya iniciado un proceso, que los identificadores coincidan, que los totales sean correctos, que los errores se propaguen y que se inicie una nueva ejecución tras la limpieza. Prueba también la secuencia de limpieza de la tabla. Aumentar la duración de la pausa no permite establecer esas condiciones de forma fiable.
En una aplicación de escritorio, conserva una instancia del calculador para los consumidores que usan la misma entrada. Si creas una instancia nueva para cada panel, cada uno tendrá su propio espacio de ejecución. Cuando cambie la entrada sintética, crea un calculador nuevo para esa instantánea; este ejemplo no define entradas mutables ni el uso compartido por claves.
Aplica los valores devueltos al estado de la interfaz de usuario en el actor principal. La guía del lenguaje Swift documenta @MainActor para el trabajo de interfaz; el calculador compartido puede seguir coordinando su propio estado por separado. Concurrency — The Swift Programming Language.
Para una suma pequeña, crear tareas supone una sobrecarga innecesaria; el ejemplo hace visibles las reglas de propiedad para que puedan aplicarse a un cálculo local más sustancial. Al adaptarlo, conserva la secuencia esencial: publica una tarea antes de suspenderte, espera ese identificador y libera únicamente la ejecución que terminó.
Fuentes
- Concurrency — The Swift Programming Language — Swift.org.
- Task — Documentación para desarrolladores de Apple.
- value — Documentación para desarrolladores de Apple.
- detached(name:priority:operation:) — Documentación para desarrolladores de Apple.
- sleep(for:tolerance:clock:) — Documentación para desarrolladores de Apple.
- UUID — Documentación para desarrolladores de Apple.
- Protect mutable state with Swift actors — Apple Developer.