Use uma única OperationQueue de longa duração do Foundation e defina seu maxConcurrentOperationCount antes de enviar cálculos. Para um utilitário para Mac que recebe uma solicitação a cada pressionamento de botão, isso limita o trabalho simultâneo e permite que solicitações posteriores aguardem.

Este tutorial cria o mecanismo de cálculo para essa tarefa de desktop e um programa independente para verificar seu comportamento. Cada solicitação contém números inteiros sintéticos, e cada resultado permanece na memória. O cálculo é executado sincronamente dentro da operação, portanto o ciclo de vida da operação abrange o trabalho real.

Qual ferramenta de concorrência é adequada para solicitações individuais recebidas?

Uma OperationQueue aceita operações individualmente, e seu método addOperation(_:), baseado em blocos, encapsula cada closure enviada em uma operação. Mantenha a fila em um objeto que persista entre as solicitações e, em seguida, envie uma closure para cada item recebido. A documentação da addOperation(_:) da Apple documenta esse método de envio.

Defina maxConcurrentOperationCount como um limite positivo, como 2. A Apple define essa propriedade como o número máximo de operações daquela fila que podem ser executadas simultaneamente. O limite pertence àquela fila específica; criar uma fila separada para cada solicitação invalidaria o limite compartilhado. Reduzi-lo posteriormente também não interrompe as operações que já estão em execução. A documentação da Apple sobre limites de concorrência explica esses limites.

Um grupo de tarefas do Swift tem um ciclo de vida diferente. Ele pertence ao escopo de um withTaskGroup, não pode ser usado fora da tarefa que o criou e aguarda a conclusão das tarefas filhas antes de retornar. Portanto, não é permitido armazenar um grupo de tarefas como propriedade para futuros pressionamentos de botão sem relação entre si. Para este mecanismo de cálculo síncrono, uma fila de operações mantida em memória oferece o ciclo de vida necessário e uma configuração explícita de concorrência. A documentação da biblioteca padrão do Swift sobre grupos de tarefas descreve essas restrições.

Organização do trabalho Abordagem adequada
Cálculos síncronos independentes chegam individualmente Mantenha uma única OperationQueue e envie uma operação por cálculo.
Uma operação assíncrona pai possui uma coleção finita de tarefas filhas Use um grupo de tarefas dentro do escopo do pai; controle explicitamente as admissões se for necessário um limite numérico.
O trabalho precisa atualizar a interface do desktop Execute o cálculo separadamente e, em seguida, entregue o resultado ao código isolado em MainActor.

A última linha diz respeito à entrega do resultado. O guia de linguagem do Swift identifica o ator principal como o local para o estado da interface e demonstra closures explicitamente isoladas ao ator principal. Mantenha a aritmética custosa fora dessa etapa de entrega. The Swift Programming Language: Concurrency explica o isolamento do ator principal.

Defina o cálculo e o contrato de conclusão

O exemplo aceita um identificador inteiro e uma quantidade de iterações de 1 a 1_000_000. Para uma quantidade de iterações de n, ele soma os quadrados de todos os números inteiros de 1 a n. Ele retorna o identificador junto com um resultado UInt64. O identificador rotula a saída; ele não afeta a aritmética.

Estes são limites ilustrativos escolhidos para este programa. Com a entrada máxima aceita, o resultado é 333333833333500000, que cabe em UInt64. A validação ocorre antes do envio, portanto uma solicitação inválida gera um erro imediatamente e não cria nenhuma operação na fila. A restrição da entrada também mantém válido o intervalo do loop.

Cada solicitação aceita produz um resultado armazenado quando sua operação termina. Os resultados se acumulam em um array, portanto identificadores repetidos não sobrescrevem resultados anteriores. A demonstração usa identificadores ascendentes distintos e ordena pelo identificador para facilitar a leitura da saída; essa ordem de exibição não implica nenhuma ordem de execução.

Esse contrato pressupõe que a fila permaneça disponível e que suas operações sejam executadas até o fim. O exemplo não expõe cancelamento. Adicionar cancelamento posteriormente exige um resultado separado para solicitações não concluídas, pois a Apple documenta o cancelamento como uma solicitação para parar, cabendo ao trabalho em execução responder a ela. A documentação da OperationQueue da Apple descreve o ciclo de vida e o cancelamento das operações.

Crie um programa completo com um limite compartilhado

Use um Mac com um compilador Swift 6.2 ou mais recente e o SDK do macOS disponível pelo Xcode ou pelas ferramentas de linha de comando. Salve o programa completo abaixo como main.swift. Os comandos abaixo selecionam explicitamente o isolamento padrão não isolado, mantendo os auxiliares de cálculo independentes das configurações de isolamento do destino do app. O Swift documenta essa configuração do compilador em SE-0466: Control default actor isolation inference.

O registro de resultados usa NSLock para proteger tanto seu array quanto seus contadores de atividade. A conformidade @unchecked Sendable atribui à implementação a responsabilidade por essa sincronização: cada acesso ao estado mutável usa o mesmo bloqueio. A Apple documenta essa responsabilidade em Sendable e documenta a proteção de seções críticas em NSLock.

import Foundation

enum InputError: Error {
    case invalidIterations
}

struct Calculation: Sendable {
    let id: Int
    let iterations: Int
}

func sumOfSquares(_ job: Calculation) -> UInt64 {
    var sum: UInt64 = 0
    for integer in 1...job.iterations {
        let value = UInt64(integer)
        sum += value * value
    }
    return sum
}

final class ExecutionLog: @unchecked Sendable {
    private let lock = NSLock()
    private var active = 0
    private var peak = 0
    private var rows: [(id: Int, value: UInt64)] = []

    func begin() {
        lock.lock()
        defer { lock.unlock() }
        active += 1
        peak = max(peak, active)
    }

    func finish(id: Int, value: UInt64) {
        lock.lock()
        defer { lock.unlock() }
        rows.append((id: id, value: value))
        active -= 1
    }

    func snapshot() -> (
        rows: [(id: Int, value: UInt64)], peak: Int
    ) {
        lock.lock()
        defer { lock.unlock() }
        return (rows, peak)
    }
}

final class CalculationPool {
    private let queue = OperationQueue()
    private let log = ExecutionLog()

    init(limit: Int) {
        precondition(limit > 0)
        queue.name = "Synthetic calculations"
        queue.maxConcurrentOperationCount = limit
    }

    func submit(id: Int, iterations: Int) throws {
        guard (1...1_000_000).contains(iterations) else {
            throw InputError.invalidIterations
        }
        let job = Calculation(id: id, iterations: iterations)
        queue.addOperation { [log] in
            log.begin()
            let value = sumOfSquares(job)
            log.finish(id: job.id, value: value)
        }
    }

    // Call only after the producer has stopped submitting work.
    // This blocking method is for the command-line demonstration.
    func finishForDemo() -> (
        rows: [(id: Int, value: UInt64)], peak: Int
    ) {
        queue.waitUntilAllOperationsAreFinished()
        return log.snapshot()
    }
}

@main
struct Demo {
    static func main() throws {
        let limit = 2
        let pool = CalculationPool(limit: limit)

        try pool.submit(id: 1, iterations: 100_000)
        try pool.submit(id: 2, iterations: 200_000)
        try pool.submit(id: 3, iterations: 300_000)

        let report = pool.finishForDemo()
        let rows = report.rows.sorted { $0.id < $1.id }
        precondition(rows.map { $0.id } == [1, 2, 3])
        precondition(rows.map { $0.value } == [
            UInt64(333338333350000),
            UInt64(2666686666700000),
            UInt64(9000045000050000)
        ])
        precondition(report.peak >= 1 && report.peak <= limit)

        for row in rows {
            print("Job \(row.id): \(row.value)")
        }
        print("Observed peak: \(report.peak)")
    }
}

Compile e execute o programa no diretório que contém main.swift:

swiftc -swift-version 6 -default-isolation nonisolated \
  -parse-as-library main.swift -o calculation-demo
./calculation-demo

As três linhas de resultado devem ser:

Job 1: 333338333350000
Job 2: 2666686666700000
Job 3: 9000045000050000

A última linha informa um pico observado de 1 ou 2. Um limite permite sobreposição; não a exige. As somas esperadas foram verificadas independentemente usando aritmética inteira e a fórmula n * (n + 1) * (2 * n + 1) / 6. O programa Swift foi inspecionado, mas não pôde ser compilado no ambiente de pesquisa porque nenhum executável do Swift estava instalado.

Por que esse limite abrange o cálculo real?

Cada operação inicia seu contador de atividade, realiza todo o cálculo sincronamente, registra o resultado e, em seguida, retorna. Nenhum cálculo assíncrono adicional escapa dessa closure. Assim, limitar as operações em execução também limita esses cálculos.

Mantenha essa relação ao adaptar o programa. Se uma operação apenas criar uma Task separada e retornar imediatamente, a fila verá a operação como concluída enquanto essa tarefa pode continuar calculando. Nesse caso, o limite de operações regeria as breves closures de inicialização. Isso decorre do ciclo de vida documentado das operações e da API de envio de blocos. A visão geral da OperationQueue da Apple e addOperation(_:) definem esses elementos.

O bloqueio do registro abrange apenas pequenas seções de controle. Mantê-lo durante sumOfSquares(_:) serializaria o acesso ao cálculo e impediria a sobreposição pretendida. Cada aquisição e liberação do bloqueio ocorre dentro de um único método síncrono, em conformidade com o requisito da Apple de que um NSLock seja desbloqueado na mesma thread que o bloqueou. A documentação da Apple sobre NSLock explica esse requisito.

Para verificar uma adaptação, execute as mesmas três solicitações com limit definido como 1 e, em seguida, restaure 2. As duas execuções devem manter os três resultados exatos. A execução serial deve informar um pico de 1; a segunda execução deve permanecer dentro de 2. Essas verificações exercitam a preservação dos resultados e o limite observado de cálculos sem pressupor uma ordem específica de agendamento.

Conecte as ações recebidas do desktop sem bloquear a interface

Mantenha uma única CalculationPool no componente que controla o recurso de cálculo do desktop. Cada ação recebida chama submit(id:iterations:) uma vez. Evite recriar o pool dentro do manipulador de ações, pois cada nova instância tem uma fila e um limite independentes.

A demonstração chama finishForDemo() somente após o envio final, para que o processo de linha de comando possa coletar os resultados antes de sair. Esse método bloqueia a thread que o chama. Em um app para desktop, use a entrega individual de cada resultado em vez de chamá-lo em um manipulador de botão ou no ator principal. A Apple documenta explicitamente o comportamento de bloqueio de waitUntilAllOperationsAreFinished().

Para integração com a interface, estenda a API de envio com um callback de resultado isolado em MainActor e marcado como @Sendable. Após calcular um resultado dentro da operação, agende uma Task com uma closure explícita @MainActor para invocar esse callback. Capture o valor imutável do resultado e o callback. Mantenha somente a atualização da exibição dentro dessa closure; deixe a aritmética na operação. O Swift documenta tanto a sintaxe de closure do ator principal quanto as capturas em closures Sendable.

Essa versão para a interface precisa de uma definição adicional de conclusão: terminar o cálculo e exibir seu resultado são eventos separados. O programa de linha de comando aguarda os cálculos e o armazenamento síncrono dos resultados. Um callback da interface agendado separadamente pode ser executado depois.

O que acontece quando os envios superam a capacidade de cálculo?

A configuração de concorrência limita as operações em execução, enquanto o trabalho enviado adicional pode permanecer na fila. Ela também permite que o array de resultados concluídos do exemplo continue crescendo. Consequentemente, este exemplo limita a concorrência dos cálculos, mas não limita o uso total de memória nem o tamanho da fila pendente. A Apple documenta que a fila mantém as operações até que terminem. A visão geral da OperationQueue explica essa retenção.

Se o recurso do desktop puder gerar picos contínuos, escolha uma política de admissão antes de ampliá-lo: aceite apenas um número fixo de solicitações pendentes e informe a rejeição, pause novos envios ou substitua uma prévia sintética pendente por uma solicitação mais recente quando esse comportamento for explicitamente desejado. Coordene as contagens de admissão e as atualizações de conclusão por meio de um único responsável sincronizado. O limite de trabalho em execução, por si só, não implementa essas políticas.

Para solicitações síncronas individuais, o projeto essencial é simples: mantenha uma fila, configure seu limite uma vez, mantenha cada cálculo dentro de sua operação e sincronize os resultados compartilhados. Preserve esses limites ao conectar as ações do desktop e, em seguida, adicione uma política explícita para a fila pendente somente quando o padrão de chegada exigir.

Fontes