Untuk menyambung semula continuation Swift tepat sekali, biarkan penyelesaian dan pembatalan bersaing melalui satu mesin keadaan yang disegerakkan. Simpan pembatalan walaupun sebelum pemasangan, pilih satu hasil terminal, dan lepaskan kunci sebelum menyambung semula tugas yang menunggu.

Tutorial ini membina penyesuai setempat daripada panggilan balik kepada async untuk program baris perintah Mac. Pengiraan sintetiknya menggandakan integer; contoh ini tidak menggunakan panggilan rangkaian, rekod atau penyimpanan berterusan. Kontrak utamanya dinyatakan dengan jelas: peristiwa terminal yang mula-mula mengubah keadaan terlindung akan menang.

Apakah jaminan yang perlu diberikan oleh penyesuai?

Continuation bertanda semak menjambatani panggilan balik segerak kepada kod Swift tak segerak. Apple menghendaki setiap laluan pelaksanaan menyambung semulanya tepat sekali. Jika tidak disambung semula, tugas akan kekal digantung; semakan masa jalan membantu mengenal pasti penyalahgunaan tetapi tidak melaksanakan penyelarasan untuk anda. CheckedContinuation

Pembatalan memperkenalkan satu lagi pihak yang mungkin mengawal penyambungan semula itu. Pembatalan Swift bersifat kooperatif: membatalkan tugas tidak menghentikan operasi panggilan balik sewenang-wenangnya secara automatik atau menyebabkan continuation melontarkan ralat. Penyesuai mesti menukarkan pembatalan secara jelas kepada hasil yang dipilihnya. Task.cancel()

Bagi contoh ini, kontrak nilainya ialah:

  • Input daripada -1000 hingga 1000 menghasilkan dua kali nilai tersebut.
  • Input lain menghasilkan CalculationError.outOfRange.
  • Pembatalan menghasilkan CancellationError jika ia memenangi peralihan keadaan.
  • Setelah hasil terminal dipilih, peristiwa penyelesaian atau pembatalan yang datang kemudian tidak boleh menggantikannya.
  • Setiap seruan memiliki gate baharu dan memasang satu continuation.

Ini bermakna penyelesaian boleh menang walaupun pembatalan berlaku sebelum pemanggil menerima nilai. Saat penentu ialah peralihan keadaan terlindung, bukannya masa pemanggil berjalan semula. Ini ialah dasar aplikasi yang dipilih untuk contoh ini; Swift tidak menjamin susunan ini secara universal.

Mengapa pembatalan boleh berlaku sebelum pemasangan?

Apple mendokumenkan bahawa pengendali pembatalan berjalan sebelum penutupan operasi apabila tugas sudah dibatalkan. Operasi tetap dilaksanakan. Pembatalan juga boleh berjalan serentak dengan operasi yang sedang berlangsung. Oleh itu, pengendali yang sekadar menyambung semula continuation pilihan akan kehilangan pembatalan jika pilihan itu belum diisi. withTaskCancellationHandler(operation:onCancel:isolation:)

Oleh itu, gate memerlukan lebih banyak maklumat daripada sekadar sama ada continuation wujud pada masa ini. Ketiadaan continuation mungkin bermaksud pemasangan belum berlaku, atau hasil terminal sudah dipilih. Kedua-dua keadaan ini memerlukan tingkah laku yang berbeza.

Keadaan terlindung Pasang continuation Terima penyelesaian atau pembatalan
Kosong Simpan continuation Simpan hasil terminal
Menunggu Tolak pemasangan kedua Keluarkan continuation dan simpan hasil terminal
Selesai Sambung semula continuation yang baru dipasang dengan hasil tersimpan Abaikan peristiwa terminal yang datang kemudian

Jadual ini juga membantu diagnosis. Jika pelaksanaan tidak dapat mewakili hasil terminal sebelum pemasangan, periksa laluan tugas yang sudah dibatalkan. Jika dua peristiwa terminal boleh mengeluarkan continuation, periksa sama ada semakan dan pengeluaran berlaku dalam satu bahagian kritikal.

Bina contoh setempat pada Mac anda

Gunakan toolchain Swift 6 dengan SDK macOS. Semak toolchain yang dipilih dalam Terminal menggunakan swift --version. Program di bawah hanya menggunakan Foundation dan Dispatch; ia tidak memerlukan kebergantungan pakej. Panduan baris perintah Swift menerangkan cara menyemak pemasangan dan menggunakan titik masuk @main tak segerak. Build a Command-line Tool

Simpan tiga blok Swift berikut mengikut turutan dalam satu fail bernama ContinuationRace.swift. Kesemuanya membentuk satu program lengkap. Blok pertama melaksanakan gate, blok kedua menyediakan pengiraan dan penyesuai, dan blok ketiga menyediakan semakan yang boleh dilaksanakan. Swift tidak tersedia dalam persekitaran penyelidikan, jadi program ini telah diperiksa tetapi tidak dikompil atau dilaksanakan di sini.

Laksanakan gate yang disegerakkan

Apple mendokumenkan NSLock sebagai cara untuk melindungi bahagian kritikal. Penguncian dan pembukaan kunci mesti berlaku pada bebenang yang sama, dan kunci ini tidak rekursif. Di sini, setiap bahagian kritikal kekal dalam kaedah segerak, tidak mengandungi titik penggantungan, dan berakhir sebelum continuation disambung semula. NSLock

import Foundation
import Dispatch

final class IntContinuationGate: @unchecked Sendable {
    private enum State {
        case empty
        case waiting(CheckedContinuation<Int, any Error>)
        case finished(Result<Int, any Error>)
    }

    private let lock = NSLock()
    private var state: State = .empty
    private var didInstall = false

    // Returns false if a terminal event already won.
    func install(
        _ continuation: CheckedContinuation<Int, any Error>
    ) -> Bool {
        lock.lock()
        precondition(!didInstall, "Install exactly once per gate")
        didInstall = true

        switch state {
        case .empty:
            state = .waiting(continuation)
            lock.unlock()
            return true

        case .waiting:
            preconditionFailure("Unexpected second installation")

        case .finished(let result):
            lock.unlock()
            continuation.resume(with: result)
            return false
        }
    }

    func finish(_ result: Result<Int, any Error>) {
        let continuation: CheckedContinuation<Int, any Error>?

        lock.lock()
        switch state {
        case .empty:
            continuation = nil
            state = .finished(result)

        case .waiting(let waiting):
            continuation = waiting
            state = .finished(result)

        case .finished:
            lock.unlock()
            return
        }
        lock.unlock()

        continuation?.resume(with: result)
    }

    func cancel() {
        finish(.failure(CancellationError()))
    }
}

Gate menggunakan @unchecked Sendable kerana storan boleh ubahnya dilindungi secara manual. Anotasi itu memindahkan tanggungjawab kepada pelaksanaan; ia tidak menambah penyegerakan. Kedua-dua sifat boleh ubah kekal peribadi, dan setiap akses berlaku di bawah kunci yang sama. Apple secara jelas menyatakan bahawa keadaan yang dilindungi kunci ialah cara untuk memenuhi keperluan semantik bagi sendability tanpa semakan. Sendable

Perhatikan bahawa finish mengubah keadaan sebelum membuka kunci. Peristiwa terminal yang bersaing hanya akan melihat finished, walaupun bebenang yang menang belum memanggil resume. Pemenang memegang continuation yang dikeluarkan secara setempat; tiada panggilan yang bersaing dapat mengeluarkannya sekali lagi.

Hubungkan penyelesaian dan pembatalan kepada gate yang sama

Pengiraan panggilan balik menggunakan baris gilir dispatch global. Apple mendokumenkan bahawa kerja yang dihantar secara tak segerak berjalan secara berasingan daripada kod yang menghantarnya, dan kerja dispatch latar belakang tidak menjamin identiti bebenang. Oleh itu, penyelarasan yang tidak bergantung pada bebenang sesuai digunakan di sini. DispatchQueue

enum CalculationError: Error {
    case outOfRange
}

func startDoubling(
    _ value: Int,
    completion: @escaping @Sendable (Result<Int, any Error>) -> Void
) {
    DispatchQueue.global().async {
        guard (-1000...1000).contains(value) else {
            completion(.failure(CalculationError.outOfRange))
            return
        }
        completion(.success(value * 2))
    }
}

func doubled(_ value: Int) async throws -> Int {
    let gate = IntContinuationGate()

    return try await withTaskCancellationHandler {
        try await withCheckedThrowingContinuation {
            (continuation: CheckedContinuation<Int, any Error>) in
            guard gate.install(continuation) else { return }

            startDoubling(value) { result in
                gate.finish(result)
            }
        }
    } onCancel: {
        gate.cancel()
    }
}

Cipta gate sebelum memasuki skop pengendali pembatalan. Dengan itu, tugas yang sudah dibatalkan boleh merekodkan pembatalan serta-merta. Apabila continuation dipasang selepas itu, pemasangan menyambung semulanya dengan kegagalan yang tersimpan dan mengembalikan false, maka penyesuai tidak memulakan pengiraan.

Pembatalan masih boleh berlaku selepas pemasangan mengembalikan true tetapi sebelum startDoubling dipanggil. Dalam susunan itu, pengiraan bermula dan selesai, tetapi hasilnya diabaikan kerana pembatalan sudah menang. Pembungkus membatalkan penantian; ia tidak menjanjikan untuk menghentikan pengiraan kecil yang terhad ini.

Jambatan continuation yang disokong ialah withCheckedThrowingContinuation; penutupannya menerima continuation yang digunakan untuk memulangkan nilai atau melontarkan ralat. Contoh ini menyatakan jenis hasil integer dan ralat bagi continuation secara jelas supaya penaakulan jenis tidak kabur. withCheckedThrowingContinuation(isolation:function:_:)

Semak susunan peristiwa tanpa bergantung pada masa tunggu

Jeda masa tidak menentukan peristiwa mana yang menang dalam perlumbaan. Sebaliknya, semakan berikut menguji secara langsung keenam-enam susunan pemasangan, penyelesaian dan pembatalan melalui gate. Setiap urutan turut menerima peristiwa terminal tambahan untuk memastikan hasil yang dipilih kekal tidak berubah.

enum GateEvent {
    case install
    case complete
    case cancel
}

func checkOrder(
    _ events: [GateEvent],
    expected: Int?
) async throws {
    let gate = IntContinuationGate()

    do {
        let value: Int = try await withCheckedThrowingContinuation {
            (continuation: CheckedContinuation<Int, any Error>) in
            for event in events {
                switch event {
                case .install:
                    _ = gate.install(continuation)
                case .complete:
                    gate.finish(.success(42))
                case .cancel:
                    gate.cancel()
                }
            }

            // Neither of these may replace the selected result.
            gate.finish(.success(99))
            gate.cancel()
        }

        guard let expected else {
            preconditionFailure("Expected cancellation")
        }
        precondition(value == expected, "Unexpected value")
    } catch is CancellationError {
        precondition(expected == nil, "Unexpected cancellation")
    }
}

@main
struct ContinuationRaceDemo {
    static func main() async throws {
        try await checkOrder(
            [.install, .complete, .cancel], expected: 42
        )
        try await checkOrder(
            [.install, .cancel, .complete], expected: nil
        )
        try await checkOrder(
            [.complete, .install, .cancel], expected: 42
        )
        try await checkOrder(
            [.complete, .cancel, .install], expected: 42
        )
        try await checkOrder(
            [.cancel, .install, .complete], expected: nil
        )
        try await checkOrder(
            [.cancel, .complete, .install], expected: nil
        )

        let value = try await doubled(21)
        precondition(value == 42)

        do {
            _ = try await doubled(1001)
            preconditionFailure("Expected outOfRange")
        } catch CalculationError.outOfRange {
            // Expected input-domain error.
        }

        // Integration smoke check: either terminal outcome is valid.
        for _ in 0..<100 {
            let task = Task { try await doubled(21) }
            task.cancel()
            do {
                let result = try await task.value
                precondition(result == 42)
            } catch is CancellationError {
                // Cancellation won.
            }
        }

        print("Event-order checks and integration checks passed.")
    }
}

Kompil dan jalankan dari direktori yang mengandungi fail tersebut:

swiftc -swift-version 6 -parse-as-library ContinuationRace.swift -o ContinuationRace
./ContinuationRace

Output berjaya yang dijangka ialah Event-order checks and integration checks passed. Semakan susunan yang eksplisit mengesahkan peralihan keadaan. Gelung integrasi menguji penyesuai sebenar, tetapi tidak menjamin bahawa selangan penjadualan tertentu akan berlaku atau membuktikan bahawa setiap pelaksanaan serentak telah diuji.

Mengapa pelaksanaan ini menyambung semula tepat sekali?

Untuk memastikan penyambungan semula berlaku paling banyak sekali, pertimbangkan panggilan pertama yang berjaya kepada finish. Panggilan itu menggantikan keadaan semasa secara atomik dengan finished. Jika continuation sedang menunggu, hanya panggilan itu yang mengeluarkannya. Setiap panggilan finish selepas itu kembali tanpa menyambung semula apa-apa. Jika continuation belum wujud, hasilnya menunggu sehingga pemasangan tunggal berlaku.

Untuk memastikan penyambungan semula akhirnya berlaku, terdapat dua laluan. Pemasangan berlaku dahulu, kemudian peristiwa terminal pertama menyambung semula continuation yang menunggu; atau peristiwa terminal berlaku dahulu, kemudian pemasangan menyambung semula dengan hasil tersimpan. Hujah ini mengandaikan pemasangan berlaku sekali dan sekurang-kurangnya satu peristiwa terminal berlaku. Sumber panggilan balik umum yang tidak pernah selesai, digandingkan dengan tugas yang tidak pernah dibatalkan, masih akan menyebabkan pemanggil terus menunggu.

Panggilan balik contoh ini melakukan pengiraan terhad dan memanggil penyelesaian pada laluan kejayaan serta ralat input. Panggilan balik terminal pendua sengaja diabaikan selepas pemenang dipilih. Ini sesuai untuk kontrak hasil tunggal yang dinyatakan; strim nilai yang bermakna memerlukan antara muka yang berbeza.

Jalan pintas yang manakah melanggar kontrak?

Menyemak continuation pilihan lalu menetapkannya kepada nil tidak mencukupi apabila tindakan itu tidak disegerakkan. Dua pemanggil serentak boleh membaca continuation bukan-nil yang sama sebelum salah seorang mengeluarkannya. Semakan, pengeluaran dan perubahan keadaan terminal mesti menjadi satu operasi terlindung.

Semakan pembatalan sebelum mencipta continuation juga meninggalkan ruang: pembatalan boleh berlaku sejurus selepas semakan itu. Pengendali dan keadaan terminal yang diingati menutup ruang tersebut. Panduan bahasa Swift menerangkan bahawa pembatalan bersifat kooperatif dan mengenal pasti pengendali pembatalan sebagai mekanisme pemberitahuan segera. Concurrency — The Swift Programming Language

Menyambung semula sambil memegang kunci kongsi menimbulkan masalah lain. Apple secara khusus memberi amaran bahawa pengendali pembatalan boleh berjalan ketika kunci tugas dalaman dipegang, dan penyambungan semula continuation mungkin memperoleh kunci tersebut. Oleh itu, contoh ini membuka kunci sebelum setiap panggilan resume. Pengendali pembatalan dan kunci

Akhir sekali, menambah Task.checkCancellation() selepas continuation kembali akan mengubah kontrak luaran contoh ini: pembatalan boleh menggantikan nilai walaupun penyelesaian sudah memenangi gate. Tentukan dasar itu dengan sengaja. Bagi kontrak yang dilaksanakan di sini, pemasangan, penyelesaian dan pembatalan semuanya menggunakan satu mesin keadaan, dan peralihan terminal pertama menentukan hasil pemanggil.

Sumber