Para um app para Mac que precisa concluir cálculos assíncronos locais na ordem de submissão, use um método síncrono de submissão isolado no ator principal que encadeie cada tarefa após sua predecessora. O isolamento do ator, por si só, não garante essa ordem.
Este passo a passo cria uma pequena fila para cálculos sintéticos com inteiros. Ele define exatamente quando ocorre a submissão, preserva a ordem através de pontos de suspensão, retorna o resultado de cada cálculo e permite que tarefas posteriores continuem mesmo que uma tarefa anterior falhe.
O que significa estrita ordem de submissão?
Suponha que uma calculadora para desktop envie as tarefas A, B e C. O contrato exigido é: o cálculo de A termina antes de o cálculo de B começar, e B termina antes de C começar. Aguardar três tarefas iniciadas independentemente em ordem alfabética apenas ordenaria a observação de seus resultados.
O limite de submissão importa. Nesta implementação, a submissão ocorre quando o método submit, isolado no ator principal, é executado e instala o próximo elo da cadeia. Chamadas síncronas consecutivas no mesmo contexto do ator principal estabelecem a ordem A, B, C sem suspender entre elas.
Se tarefas independentes solicitarem submissão ao mesmo tempo, a ordem de chegada não é especificada. A fila preserva a ordem em que seu método de submissão as aceita. Se o aplicativo exigir uma ordem específica entre esses produtores, estabeleça-a antes de submeter, por exemplo, reunindo um lote finito em um array ordenado explicitamente.
O projeto usa o ator principal para a breve etapa de controle. A Apple define MainActor como um ator singleton cujo executor é equivalente à fila principal de despacho. O cálculo propriamente dito, abaixo, pertence a um ator de trabalho separado. Documentação de MainActor
Por que um ator ou uma fila serial de operações não bastam?
Um ator executa trabalho isolado a ele uma parte de cada vez, mas um método assíncrono pode suspender e permitir que outra chamada prossiga. O projeto de atores do Swift também afirma explicitamente que não há garantia de que as tarefas à espera de um ator sejam executadas na ordem em que originalmente aguardaram por ele. Portanto, colocar um cálculo assíncrono inteiro dentro de um ator não estabelece, por si só, que um cálculo termine antes de o próximo começar. SE-0306: Atores
Criar instâncias separadas de Task também não estabelece nenhuma dependência entre elas. As tarefas podem começar a ser executadas após a criação; manter suas referências permite aguardá-las ou cancelá-las, mas não estabelece uma sequência de execução. Documentação de Task
O Foundation oferece outro mecanismo de ordenação. OperationQueue agenda operações conforme sua prontidão, prioridade e dependências; a Apple recomenda usar dependências quando uma ordem de execução específica for importante. Definir maxConcurrentOperationCount como 1 limita a concorrência, mas o requisito de ordenação ainda exige dependências explícitas. Documentação de OperationQueue
Escolha o mecanismo conforme as necessidades atuais do seu aplicativo:
| Situação | Abordagem adequada | O que estabelece a ordem? |
|---|---|---|
| Um lote conhecido em uma função assíncrona | Um loop que aguarda cada cálculo antes de continuar | Fluxo de controle sequencial |
| Tarefas chegam ao longo do tempo a partir de ações no desktop | Um método síncrono de submissão com uma cadeia de tarefas | Cada cálculo aguarda o sinal de conclusão da tarefa anterior |
| Operações Foundation existentes | Dependências configuradas antes de enfileirar | Cada operação depende do estado de conclusão da operação anterior |
A cadeia de tarefas é útil aqui porque a submissão retorna imediatamente uma referência enquanto os cálculos são executados depois. Para um lote fixo sem novas tarefas chegando, o loop sequencial geralmente exige menos código.
Crie uma fila que aguarde cálculos assíncronos completos
A fila armazena uma tarefa de conclusão privada e não lança erros, chamada tail. Cada submissão captura a cauda atual, cria uma tarefa de cálculo que a aguarda e substitui a cauda por uma nova tarefa que observa a conclusão do cálculo.
A tarefa de conclusão aguarda result, que representa sucesso ou falha após a conclusão de uma tarefa. Isso permite que um cálculo malsucedido libere a próxima tarefa sem descartar o erro da própria referência do cálculo. Os chamadores ainda podem obter esse erro por meio da referência. Documentação sobre resultados de Task
A seguir, uma demonstração completa de linha de comando para um Mac com uma toolchain do Swift 6. Salve-a como OrderedDemo.swift. O código-fonte foi inspecionado, mas não foi possível compilá-lo neste ambiente de pesquisa porque não havia um compilador Swift disponível.
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)")
}
}
Compile e execute o programa salvo no Terminal, a partir do diretório onde ele está:
swiftc -swift-version 6 -parse-as-library OrderedDemo.swift -o OrderedDemo
./OrderedDemo
A saída esperada decorre das entradas e da cadeia de dependências do exemplo:
start A
finish A
start B
finish B
start C
finish C
Answers: [9, 16, 25]
As asserções verificam tanto os valores retornados quanto os limites dos cálculos. Apenas imprimir os resultados enquanto se aguardam as referências na ordem de submissão não garantiria que os cálculos fossem executados nessa ordem.
O ator de trabalho chama intencionalmente Task.yield() para que o cálculo tenha uma oportunidade de suspender. Ceder a execução permite que outras tarefas sejam executadas, embora o executor possa retomar imediatamente a mesma tarefa. Portanto, a ordenação da fila vem de suas dependências, e não de uma suposição de que ceder a execução trocará de tarefa. Documentação sobre yield de Task
Por que a cadeia preserva a ordem através de suspensões?
Três detalhes estabelecem essa garantia. Primeiro, submit não contém nenhum ponto de suspensão, então outra submissão no ator principal não pode se intercalar enquanto o método captura e substitui tail. O Swift documenta que o código isolado a um ator, sem pontos de suspensão, é executado sem que outra tarefa do ator interrompa essa sequência. Concorrência: atores
Segundo, cada tarefa aguarda a predecessora capturada antes de invocar a closure de cálculo. Acessar value de uma tarefa que não lança erros aguarda a conclusão dessa tarefa. A tarefa posterior pode executar a lógica de espera antes, mas não pode iniciar o cálculo antecipadamente. Documentação sobre value de Task que não lança erros
Terceiro, o sinal de conclusão da predecessora só é concluído depois que a tarefa anterior produz seu resultado ou erro. Seguir os elos para trás estabelece a sequência de cada submissão aceita: C aguarda o sinal de conclusão de B, que aguarda B, que aguarda o sinal de conclusão de A.
Essa garantia de ordenação vale para o trabalho aguardado dentro de cada closure submetida. Se uma closure iniciar outra tarefa não estruturada e retornar sem aguardá-la, esse trabalho adicional poderá continuar após o retorno da closure. Inclua todas as etapas de cálculo necessárias na operação aguardada antes de retornar.
Para integração com o desktop, mantenha uma instância da fila no controlador ou modelo isolado no ator principal que lida com as ações relevantes. Chame submit diretamente a partir desse contexto e use a referência retornada para observar a conclusão. Criar primeiro uma tarefa independente e submetê-la apenas depois que começar deslocaria o limite de submissão para a execução posterior dessa tarefa.
Como devem funcionar os erros e o cancelamento?
Esta implementação adota uma política de continuar após uma falha. Uma tarefa com falha mantém seu erro, seu sinal de conclusão é finalizado e a próxima tarefa aceita torna-se elegível para cálculo. A demonstração inspeciona result de cada referência, em vez de abandonar os resultados restantes após o primeiro erro.
Para verificar uma falha simples, altere a entrada de B de 4 para 1001. O ator de trabalho a rejeita antes da multiplicação. Atualize os resultados esperados para [9, 25] e o rastreamento esperado para start A, finish A, start B, start C, finish C. O programa deverá imprimir a falha e ainda calcular C. O limite de entrada é um contrato ilustrativo escolhido para manter essa aritmética simples.
O cancelamento é cooperativo. A fila verifica se houve cancelamento após aguardar a predecessora, e o ator de trabalho verifica novamente após ceder a execução. Task.checkCancellation() lança CancellationError quando há uma solicitação de cancelamento. Documentação sobre checkCancellation de Task
Para verificar o cancelamento antes da execução, chame b.cancel() imediatamente após submeter C e antes do loop de resultados. Com essas submissões ininterruptas no ator principal, B ainda não pode ter iniciado seu cálculo. Ajuste os resultados esperados para [9, 25] e remova as duas entradas de B do rastreamento esperado. B ainda aguarda o sinal de conclusão de A antes de informar o cancelamento; em seguida, C prossegue.
Mantenha os sinais de conclusão privados. Eles representam o controle da cadeia, enquanto as referências retornadas representam cálculos individuais. Cancelar uma tarefa não significa que um cálculo anterior deva parar nem que cálculos posteriores devam começar enquanto ele ainda está em execução.
Se, em vez disso, o aplicativo precisar interromper todo o lote após uma falha, será necessária uma política explícita para o lote. Alterar o sinal para propagar erros mudaria quais tarefas posteriores seriam executadas e como seus resultados teriam de ser informados.
Quando usar dependências do Foundation?
Se o aplicativo para desktop já representa o trabalho como objetos Operation do Foundation, estabeleça uma dependência de cada operação para sua predecessora antes de enfileirá-la. O Foundation considera a dependência satisfeita quando a predecessora termina, tenha ela sido bem-sucedida ou não; comportamentos que dependam de sucesso exigem tratamento separado do resultado. Documentação de Operation
Tenha cuidado ao adaptar funções assíncronas do Swift a esse modelo. Uma operação síncrona que inicia uma Task e retorna imediatamente não representa todo o ciclo de vida da tarefa. Uma operação assíncrona personalizada precisa gerenciar corretamente seu estado de execução e de conclusão, incluindo as notificações necessárias, para que as operações dependentes aguardem o trabalho desejado. A Apple documenta essas responsabilidades nas seções sobre operações assíncronas e comunicação do estado de conclusão. Documentação sobre o ciclo de vida de Operation
Para a calculadora assíncrona local desenvolvida aqui, a cadeia de tarefas deixa o contrato claro: aceitar submissões de forma síncrona, aguardar a conclusão da predecessora, executar o cálculo completo e expor o resultado. Preserve esses limites ao conectar a fila às ações do desktop e valide a execução com rastreamentos de início e conclusão, além dos valores retornados.
Fontes
- MainActor — Apple
- SE-0306: Atores — Swift
- Task — Apple
- OperationQueue — Apple
- result — Apple
- yield() — Apple
- Concorrência — Swift
- value: tarefa que não lança erros — Apple
- checkCancellation() — Apple
- Operation — Apple