Para una app de Mac que debe completar cálculos asíncronos locales en el orden en que se envían, usa un método síncrono de envío aislado al actor principal que encadene cada tarea detrás de la anterior. El aislamiento del actor por sí solo no garantiza ese orden.
Este recorrido crea una pequeña cola para cálculos de enteros sintéticos. Define exactamente cuándo se realiza el envío, preserva el orden a través de los puntos de suspensión, devuelve el resultado de cada cálculo y permite que los trabajos posteriores continúen aunque falle uno anterior.
¿Qué significa estricto orden de envío?
Supongamos que una calculadora de escritorio envía los trabajos A, B y C. El contrato requerido es: el cálculo de A termina antes de que comience el de B, y el de B termina antes de que comience el de C. Esperar a que terminen tres tareas iniciadas de forma independiente y en orden alfabético solo ordenaría la observación de sus resultados.
El límite de envío es importante. En esta implementación, el envío se produce cuando se ejecuta el método submit, aislado al actor principal, y se instala el siguiente enlace de la cadena. Las llamadas síncronas consecutivas desde un mismo contexto del actor principal establecen el orden A, B, C sin suspenderse entre ellas.
Si tareas independientes solicitan el envío de forma concurrente, el orden de llegada no está especificado. La cola conserva el orden en que acepta las solicitudes mediante su método de envío. Si tu aplicación necesita un orden concreto entre esos productores, establécelo antes de enviar; por ejemplo, recopilando un lote finito en un arreglo ordenado explícitamente.
El diseño usa el actor principal para el breve paso de contabilidad. Apple define MainActor como un actor singleton cuyo ejecutor equivale a la cola principal de despacho. El cálculo propiamente dicho que se muestra abajo pertenece a un actor trabajador independiente. Documentación de MainActor
¿Por qué no bastan un actor o una cola de operaciones en serie?
Un actor ejecuta el trabajo aislado a él de una pieza a la vez, pero un método asíncrono puede suspenderse y permitir que avance otra llamada. El diseño de actores de Swift también especifica expresamente que no se garantiza que las tareas que esperan a un actor se ejecuten en el orden en que originalmente lo esperaron. Por tanto, incluir un cálculo asíncrono completo dentro de un actor no establece por sí solo que cada trabajo termine antes de que comience el siguiente. SE-0306: Actores
Crear instancias de Task independientes tampoco establece ninguna dependencia entre ellas. Las tareas pueden empezar a ejecutarse después de crearse; conservar sus identificadores permite esperarlas o cancelarlas, pero no establece una secuencia de ejecución. Documentación de Task
Foundation ofrece otro mecanismo de ordenación. OperationQueue programa las operaciones según su disponibilidad, prioridad y dependencias; Apple recomienda usar dependencias cuando importa un orden de ejecución específico. Establecer maxConcurrentOperationCount en 1 limita la concurrencia, pero el requisito de orden sigue necesitando dependencias explícitas. Documentación de OperationQueue
Elige el mecanismo según lo que ya necesite tu aplicación:
| Situación | Enfoque adecuado | ¿Qué establece el orden? |
|---|---|---|
| Un lote conocido en una función asíncrona | Un bucle que espera cada cálculo antes de continuar | Flujo de control secuencial |
| Llegan trabajos con el tiempo a partir de acciones de escritorio | Un método de envío síncrono con una cadena de tareas | Cada cálculo espera la compuerta de finalización de su predecesor |
| Operaciones Foundation existentes | Dependencias configuradas antes de añadirlas a la cola | Cada operación depende del estado de finalización de la anterior |
La cadena de tareas resulta útil aquí porque el envío devuelve un identificador de inmediato, mientras los cálculos se ejecutan después. Para un lote fijo sin trabajos que vayan llegando, el bucle secuencial suele requerir menos código.
Crear una cola que espere a que terminen los cálculos asíncronos
La cola almacena una tarea de finalización privada y no lanzadora llamada tail. Cada envío captura el elemento final actual, crea una tarea de cálculo que espera a que termine y reemplaza el elemento final por una nueva tarea que observa la finalización del cálculo.
La tarea de finalización espera a result, que representa el éxito o el fallo de una tarea una vez que termina. Así, un cálculo fallido permite continuar al siguiente trabajo sin descartar el error del identificador de la tarea de cálculo. Los llamadores aún pueden obtener ese error a través del identificador. Documentación del resultado de Task
A continuación se muestra una demostración completa de línea de comandos para un Mac con una cadena de herramientas de Swift 6. Guárdala como OrderedDemo.swift. Se revisó el código fuente, pero no se pudo compilar en este entorno de investigación porque no había ningún compilador de Swift disponible.
import Foundation
@MainActor
final class OrderedIntQueue {
private var tail: Task<Void, Never>?
func submit(
_ operation: @escaping @Sendable () async throws -> Int
) -> Task<Int, Error> {
let predecessor = tail
let job = Task<Int, Error> {
if let predecessor {
await predecessor.value
}
try Task.checkCancellation()
return try await operation()
}
tail = Task<Void, Never> {
_ = await job.result
}
return job
}
}
enum CalculationError: Error {
case inputOutOfRange
}
actor CalculationWorker {
private var events: [String] = []
func square(_ input: Int, label: String) async throws -> Int {
events.append("start \(label)")
guard (-1000...1000).contains(input) else {
throw CalculationError.inputOutOfRange
}
await Task.yield()
try Task.checkCancellation()
let answer = input * input
events.append("finish \(label)")
return answer
}
func trace() -> [String] {
events
}
}
@main
struct OrderedDemo {
@MainActor
static func main() async {
let queue = OrderedIntQueue()
let worker = CalculationWorker()
let a = queue.submit {
try await worker.square(3, label: "A")
}
let b = queue.submit {
try await worker.square(4, label: "B")
}
let c = queue.submit {
try await worker.square(5, label: "C")
}
var answers: [Int] = []
for job in [a, b, c] {
switch await job.result {
case .success(let answer):
answers.append(answer)
case .failure(let error):
print("Calculation failed: \(error)")
}
}
let events = await worker.trace()
precondition(answers == [9, 16, 25])
precondition(events == [
"start A", "finish A",
"start B", "finish B",
"start C", "finish C"
])
print(events.joined(separator: "\n"))
print("Answers: \(answers)")
}
}
Compila y ejecuta el programa guardado desde su directorio en Terminal:
swiftc -swift-version 6 -parse-as-library OrderedDemo.swift -o OrderedDemo
./OrderedDemo
El resultado esperado se desprende de las entradas y la cadena de dependencias del ejemplo:
start A
finish A
start B
finish B
start C
finish C
Answers: [9, 16, 25]
Las aserciones comprueban tanto los valores devueltos como los límites de los cálculos. Limitarse a imprimir los resultados mientras se esperan los identificadores en el orden de envío no demostraría que los cálculos se ejecutaron en ese orden.
El trabajador llama deliberadamente a Task.yield() para que el cálculo tenga la oportunidad de suspenderse. Ceder permite que se ejecuten otras tareas, aunque el ejecutor puede reanudar inmediatamente la misma tarea. Por tanto, el orden de la cola proviene de sus dependencias, no de suponer que ceder cambiará de tarea. Documentación de Task yield
¿Por qué la cadena conserva el orden a través de la suspensión?
Tres detalles establecen la garantía. Primero, submit no contiene ningún punto de suspensión, por lo que otro envío al actor principal no puede intercalarse mientras captura y reemplaza tail. Swift documenta que el código aislado a un actor que no contiene puntos de suspensión se ejecuta sin que otra tarea del actor interrumpa esa secuencia. Concurrencia: Actores
Segundo, cada trabajo espera a su predecesor capturado antes de invocar la clausura de cálculo. Acceder a value de una tarea no lanzadora espera a que esa tarea termine. El trabajo posterior puede ejecutar antes la lógica que espera, pero no puede empezar su cálculo antes de tiempo. Documentación de value de Task no lanzadora
Tercero, la compuerta del predecesor solo se completa después de que el trabajo anterior produce su resultado o error. Seguir los enlaces hacia atrás establece la secuencia para cada envío aceptado: C espera a la compuerta de finalización de B, que espera a B, que espera a la compuerta de finalización de A.
Esta garantía de orden se aplica al trabajo que se espera dentro de cada clausura enviada. Si una clausura inicia otra tarea no estructurada y retorna sin esperarla, ese trabajo adicional puede seguir ejecutándose después de que la clausura termine. Incluye todos los pasos de cálculo necesarios en la operación esperada antes de retornar.
Para integrar la cola en una app de escritorio, conserva una instancia en el controlador o modelo aislado al actor principal que gestione las acciones pertinentes. Llama a submit directamente desde ese contexto y luego usa el identificador devuelto para observar la finalización. Crear primero una tarea independiente y enviar el trabajo solo después de que empiece trasladaría el límite de envío a la ejecución posterior de esa tarea.
¿Cómo deben comportarse los errores y la cancelación?
Esta implementación adopta una política de continuar tras un fallo. Un trabajo fallido conserva su error, su compuerta de finalización se completa y el siguiente trabajo aceptado puede comenzar el cálculo. La demostración inspecciona el result de cada identificador en vez de abandonar los resultados restantes tras el primer error.
Para comprobar un fallo sencillo, cambia la entrada de B de 4 a 1001. El trabajador la rechaza antes de multiplicar. Actualiza las respuestas esperadas a [9, 25] y el registro esperado a start A, finish A, start B, start C, finish C. El programa debería mostrar el fallo y aun así calcular C. El límite de entrada es un contrato ilustrativo elegido para mantener pequeña esta operación aritmética.
La cancelación es cooperativa. La cola comprueba la cancelación después de esperar al predecesor, y el trabajador vuelve a comprobarla después de ceder. Task.checkCancellation() lanza CancellationError cuando se ha solicitado la cancelación. Documentación de checkCancellation de Task
Para comprobar la cancelación antes de la ejecución, llama a b.cancel() inmediatamente después de enviar C y antes del bucle de resultados. Con estos envíos ininterrumpidos al actor principal, B aún no habrá comenzado su cálculo. Ajusta las respuestas esperadas a [9, 25] y quita las dos entradas de B del registro esperado. B seguirá esperando a la compuerta de finalización de A antes de informar de la cancelación; luego C continúa.
Mantén privadas las compuertas de finalización. Representan la contabilidad de la cadena, mientras que los identificadores devueltos representan cálculos individuales. Cancelar un trabajo no significa que deba detenerse un cálculo anterior ni que los posteriores deban empezar mientras ese cálculo siga en curso.
Si tu aplicación debe detener todo el lote tras un fallo, necesitarás una política explícita para el lote. Cambiar la compuerta para propagar errores alteraría qué trabajos posteriores se ejecutan y cómo deben comunicarse sus resultados.
¿Cuándo conviene usar dependencias de Foundation?
Si la aplicación de escritorio ya representa el trabajo mediante objetos Operation de Foundation, establece una dependencia de cada operación respecto de su predecesora antes de añadirla a la cola. Foundation considera satisfecha la dependencia cuando termina la operación predecesora, tanto si tuvo éxito como si falló; el comportamiento que depende del éxito requiere gestionar los resultados por separado. Documentación de Operation
Ten cuidado al integrar funciones asíncronas de Swift con ese modelo. Una operación síncrona que inicia una Task y retorna de inmediato no representa toda la vida útil de la tarea. Una operación asíncrona personalizada debe gestionar correctamente su estado de ejecución y finalización, incluidas las notificaciones necesarias, para que las operaciones dependientes esperen al trabajo previsto. Apple documenta estas responsabilidades en las secciones sobre operaciones asíncronas y notificación del estado de finalización. Documentación del ciclo de vida de Operation
Para la calculadora asíncrona local desarrollada aquí, la cadena de tareas deja claro el contrato: aceptar los envíos de forma síncrona, esperar a que termine el predecesor, ejecutar el cálculo completo y exponer su resultado. Conserva esos límites al conectar la cola con las acciones de escritorio, y valida la ejecución con registros de inicio y fin además de los valores devueltos.
Fuentes
- MainActor — Apple
- SE-0306: Actores — Swift
- Task — Apple
- OperationQueue — Apple
- result — Apple
- yield() — Apple
- Concurrency — Swift
- value: tarea no lanzadora — Apple
- checkCancellation() — Apple
- Operation — Apple