Untuk melanjutkan Swift continuation tepat sekali, buat penyelesaian dan pembatalan bersaing melalui satu mesin status yang tersinkronisasi. Simpan pembatalan bahkan sebelum pemasangan, pilih satu hasil terminal, dan lepaskan kunci sebelum melanjutkan tugas yang menunggu.
Tutorial ini membangun adaptor callback-ke-async lokal untuk program baris perintah Mac. Perhitungan sintetisnya menggandakan bilangan bulat; contoh ini tidak menggunakan panggilan jaringan, rekaman, atau persistensi. Kontrak utamanya dinyatakan secara eksplisit: peristiwa terminal mana pun yang lebih dahulu mengubah status yang dilindungi akan menang.
Apa yang harus dijamin oleh adaptor?
Checked continuation menjembatani callback sinkron ke kode Swift asinkron. Apple mewajibkan setiap jalur eksekusi melanjutkannya tepat sekali. Jika tidak dilanjutkan, tugas akan tetap tertangguh; pemeriksaan runtime membantu mengidentifikasi penyalahgunaan, tetapi tidak menerapkan koordinasi untuk Anda. CheckedContinuation
Pembatalan memperkenalkan pihak lain yang berpotensi melanjutkan continuation tersebut. Pembatalan Swift bersifat kooperatif: membatalkan tugas tidak otomatis menghentikan operasi callback arbitrer atau menyebabkan continuation melempar galat. Adaptor harus secara eksplisit mengubah pembatalan menjadi hasil yang dipilihnya. Task.cancel()
Untuk contoh ini, kontrak nilainya adalah:
- Masukan dari
-1000hingga1000menghasilkan dua kali nilainya. - Masukan lainnya menghasilkan
CalculationError.outOfRange. - Pembatalan menghasilkan
CancellationErrorjika pembatalan memenangkan transisi status. - Setelah hasil terminal dipilih, peristiwa penyelesaian atau pembatalan berikutnya tidak dapat menggantikannya.
- Setiap pemanggilan memiliki gate baru dan memasang satu continuation.
Artinya, penyelesaian dapat menang meskipun pembatalan terjadi sebelum pemanggil menerima nilainya. Momen penentunya adalah transisi status yang dilindungi, bukan waktu ketika pemanggil kembali berjalan. Ini adalah kebijakan aplikasi yang dipilih untuk contoh ini; Swift tidak memberikan jaminan urutan universal seperti itu.
Mengapa pembatalan dapat terjadi sebelum pemasangan?
Apple mendokumentasikan bahwa handler pembatalan dijalankan sebelum closure operasi ketika tugas sudah dibatalkan. Operasi tetap dijalankan. Pembatalan juga dapat berjalan bersamaan dengan operasi yang sudah berlangsung. Karena itu, handler yang hanya melanjutkan continuation opsional akan kehilangan pembatalan jika nilai opsional tersebut belum diisi. withTaskCancellationHandler(operation:onCancel:isolation:)
Karena itu, gate memerlukan lebih banyak informasi daripada sekadar apakah continuation saat ini ada. Continuation yang tidak ada bisa berarti pemasangan belum terjadi, atau hasil terminal sudah dipilih. Kedua kondisi ini memerlukan perilaku yang berbeda.
| Status yang dilindungi | Pasang continuation | Terima penyelesaian atau pembatalan |
|---|---|---|
| Kosong | Simpan continuation | Simpan hasil terminal |
| Menunggu | Tolak pemasangan kedua | Hapus continuation dan simpan hasil terminal |
| Selesai | Lanjutkan continuation yang baru dipasang dengan hasil tersimpan | Abaikan peristiwa terminal berikutnya |
Tabel ini juga membantu diagnosis. Jika implementasi tidak dapat merepresentasikan hasil terminal sebelum pemasangan, periksa jalur yang sudah dibatalkan. Jika dua peristiwa terminal dapat sama-sama mengambil continuation, periksa apakah pemeriksaan dan penghapusan dilakukan dalam satu bagian kritis.
Buat contoh lokal di Mac Anda
Gunakan toolchain Swift 6 dengan macOS SDK. Periksa toolchain yang dipilih di Terminal dengan swift --version. Program di bawah hanya menggunakan Foundation dan Dispatch; tidak memerlukan dependensi paket. Panduan baris perintah Swift menjelaskan cara memeriksa instalasi dan menggunakan titik masuk @main asinkron. Build a Command-line Tool
Simpan tiga blok Swift berikut secara berurutan ke dalam satu berkas bernama ContinuationRace.swift. Ketiganya membentuk satu program lengkap. Blok pertama mengimplementasikan gate, blok kedua menyediakan perhitungan dan adaptor, dan blok ketiga menyediakan pemeriksaan yang dapat dijalankan. Swift tidak tersedia di lingkungan riset, jadi program ini telah diperiksa, tetapi tidak dikompilasi atau dijalankan di sini.
Implementasikan gate yang tersinkronisasi
Foundation mendokumentasikan NSLock sebagai cara untuk melindungi bagian kritis. Penguncian dan pembukaan kunci harus dilakukan pada utas yang sama, dan kunci ini tidak rekursif. Di sini, setiap bagian kritis berada dalam metode sinkron, tidak mengandung titik suspensi, dan berakhir sebelum continuation dilanjutkan. 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 karena penyimpanan yang dapat diubah dilindungi secara manual. Anotasi itu mengalihkan tanggung jawab kepada implementasi; anotasi tersebut tidak menambahkan sinkronisasi. Kedua properti yang dapat diubah tetap privat, dan setiap akses dilakukan di bawah kunci yang sama. Apple secara eksplisit menjelaskan bahwa status yang dilindungi kunci dapat digunakan untuk memenuhi persyaratan semantik sendability unchecked. Sendable
Perhatikan bahwa finish mengubah status sebelum membuka kunci. Peristiwa terminal yang bersaing kemudian hanya dapat melihat finished, meskipun utas yang menang belum memanggil resume. Pihak yang menang menyimpan continuation yang diambil secara lokal; tidak ada pemanggilan lain yang dapat mengambilnya lagi.
Hubungkan penyelesaian dan pembatalan ke gate yang sama
Perhitungan callback menggunakan antrean dispatch global. Apple mendokumentasikan bahwa pekerjaan yang dikirim secara asinkron berjalan terpisah dari kode yang mengirimkannya, dan bahwa pekerjaan dispatch latar belakang tidak menjamin identitas utas tertentu. Karena itu, koordinasi yang tidak bergantung pada utas sesuai untuk kasus ini. 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()
}
}
Buat gate sebelum memasuki cakupan handler pembatalan. Dengan begitu, tugas yang sudah dibatalkan dapat langsung mencatat pembatalan. Ketika continuation dipasang setelahnya, pemasangan melanjutkannya dengan galat yang tersimpan dan mengembalikan false, sehingga adaptor melewati tahap memulai perhitungan.
Pembatalan tetap dapat terjadi setelah pemasangan mengembalikan true dan sebelum startDoubling dipanggil. Dalam urutan itu, perhitungan dimulai dan selesai, tetapi hasilnya diabaikan karena pembatalan sudah menang. Pembungkus membatalkan penantian; pembungkus tidak menjanjikan penghentian perhitungan kecil yang terbatas ini.
Jembatan continuation yang didukung adalah withCheckedThrowingContinuation; closure-nya menerima continuation yang digunakan untuk mengembalikan nilai atau melempar galat. Contoh ini menyatakan secara eksplisit tipe hasil integer dan tipe galat continuation agar inferensi tidak ambigu. withCheckedThrowingContinuation(isolation:function:_:)
Periksa urutan peristiwa tanpa mengandalkan jeda waktu
Penundaan waktu tidak menetapkan peristiwa mana yang menang dalam perlombaan. Pemeriksaan berikut justru menjalankan langsung keenam urutan pemasangan, penyelesaian, dan pembatalan melalui gate. Setiap urutan juga menerima peristiwa terminal tambahan untuk memastikan hasil yang dipilih tetap 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.")
}
}
Kompilasi dan jalankan dari direktori yang berisi berkas tersebut:
swiftc -swift-version 6 -parse-as-library ContinuationRace.swift -o ContinuationRace
./ContinuationRace
Keluaran sukses yang diharapkan adalah Event-order checks and integration checks passed. Pemeriksaan urutan eksplisit memvalidasi transisi status. Perulangan integrasi menjalankan adaptor yang sebenarnya, tetapi tidak menjamin interleaving penjadwalan tertentu terjadi atau membuktikan bahwa setiap eksekusi konkuren telah diuji.
Mengapa implementasi ini melanjutkan tepat sekali?
Untuk memastikan continuation paling banyak dilanjutkan sekali, pertimbangkan pemanggilan pertama yang berhasil ke finish. Pemanggilan itu secara atomik mengganti status saat ini dengan finished. Jika continuation sedang menunggu, hanya pemanggilan tersebut yang mengambilnya. Setiap pemanggilan finish berikutnya kembali tanpa melanjutkan apa pun. Jika continuation belum ada, hasil akan menunggu pemasangan satu-satunya.
Untuk memastikan continuation akhirnya dilanjutkan, ada dua jalur. Pemasangan terjadi lebih dahulu, lalu peristiwa terminal pertama melanjutkan continuation yang menunggu; atau peristiwa terminal terjadi lebih dahulu, lalu pemasangan melanjutkannya dengan hasil yang tersimpan. Argumen ini mengasumsikan pemasangan terjadi sekali dan setidaknya satu peristiwa terminal terjadi. Sumber callback umum yang tidak pernah selesai, bersama tugas yang tidak pernah dibatalkan, tetap akan membuat pemanggil menunggu.
Callback contoh ini melakukan perhitungan terbatas dan memanggil completion pada jalur sukses maupun jalur galat masukan. Callback terminal duplikat sengaja diabaikan setelah ada pemenang. Ini cocok untuk kontrak hasil tunggal yang ditentukan; aliran nilai yang bermakna memerlukan antarmuka berbeda.
Jalan pintas mana yang merusak kontrak?
Memeriksa continuation opsional lalu mengaturnya ke nil tidak cukup jika tindakan tersebut tidak disinkronkan. Dua pemanggil konkuren dapat membaca continuation yang sama dan bukan-nil sebelum salah satu menghapusnya. Pemeriksaan, pengambilan, dan perubahan status terminal harus menjadi satu operasi yang dilindungi.
Pemeriksaan pembatalan sebelum membuat continuation juga menyisakan celah: pembatalan dapat terjadi tepat setelah pemeriksaan itu. Handler dan status terminal yang diingat menutup celah tersebut. Panduan bahasa Swift menjelaskan bahwa pembatalan bersifat kooperatif dan menyebut handler pembatalan sebagai mekanisme untuk pemberitahuan langsung. Concurrency — The Swift Programming Language
Melanjutkan continuation saat memegang kunci bersama menimbulkan masalah lain. Apple secara khusus memperingatkan bahwa handler pembatalan dapat berjalan saat kunci internal tugas sedang dipegang, dan bahwa melanjutkan continuation dapat memperoleh kunci tersebut. Karena itu, contoh ini membuka kunci sebelum setiap pemanggilan resume. Cancellation handlers and locks
Terakhir, menambahkan Task.checkCancellation() setelah continuation kembali akan mengubah kontrak yang terlihat dari contoh ini: pembatalan dapat menggantikan nilai bahkan setelah penyelesaian memenangkan gate. Tentukan kebijakan itu dengan sengaja. Untuk kontrak yang diimplementasikan di sini, pemasangan, penyelesaian, dan pembatalan semuanya menggunakan satu mesin status, dan transisi terminal pertamanya menentukan hasil bagi pemanggil.
Sumber
- CheckedContinuation — Apple.
- withTaskCancellationHandler(operation:onCancel:isolation:) — Apple.
- cancel() — Apple.
- NSLock — Apple.
- Sendable — Apple.
- DispatchQueue — Apple.
- withCheckedThrowingContinuation(isolation:function:_:) — Apple.
- Build a Command-line Tool — Swift.org.
- Concurrency — The Swift Programming Language — The Swift Project.