Agar perhitungan Swift dapat dibatalkan, simpan handle task-nya, panggil cancel() saat diminta, dan periksa pembatalan di dalam loop. Contoh macOS ini juga menjaga antarmuka tetap responsif dan mencegah proses yang dibatalkan menampilkan hasil yang terlambat.

Apa yang sebenarnya terjadi saat task Swift dibatalkan?

Swift menggunakan pembatalan kooperatif: permintaan pembatalan menandai sebuah task, sementara kode yang berjalan menentukan kapan harus berhenti. Loop CPU yang tidak pernah memeriksa pembatalan dapat terus berjalan hingga selesai. Memanggil Task.checkCancellation() melempar CancellationError saat pembatalan terdeteksi. Dokumentasi pembatalan Apple, checkCancellation().

Kebutuhan pembaca di sini spesifik: tambahkan tombol Start dan Cancel yang berfungsi ke aplikasi Mac kecil yang menghitung jumlah sintetis. Setiap input dibuat secara lokal. Perhitungan menjumlahkan akar kuadrat dari bilangan bulat positif berurutan, menghasilkan satu Double hanya setelah semua suku yang diminta diproses.

Tentukan kontrak hasil sebelum menerapkan pembatalan. Contoh ini membuang jumlah yang belum selesai dan melaporkan pembatalan secara terpisah. Mengembalikan jumlah parsial memerlukan kontrak berbeda yang menyebutkan berapa suku yang disertakan; jika tidak, pemanggil dapat mengira pekerjaan yang belum selesai sebagai jawaban yang diminta. Panduan bahasa Swift mendokumentasikan melempar error, mengembalikan hasil kosong, dan mengembalikan pekerjaan parsial sebagai respons pembatalan yang mungkin. Concurrency.

Ada dua tanggung jawab yang perlu diterapkan: menghentikan komputasi yang tidak diperlukan dan menentukan apakah nilai yang telah selesai masih layak ditampilkan di antarmuka. Contoh ini menangani keduanya, termasuk pembatalan yang diminta setelah pemeriksaan terakhir worker tetapi sebelum hasilnya ditampilkan.

Letakkan pemeriksaan pembatalan di dalam perhitungan

Buat proyek aplikasi macOS di Xcode menggunakan SwiftUI dan Swift. Untuk contoh ini, gunakan mode bahasa Swift 6 dan target deployment macOS 13 atau lebih baru. Tambahkan file lengkap berikut dengan nama SyntheticCalculation.swift. File ini tidak bergantung pada view.

// SyntheticCalculation.swift
import Swift

enum SyntheticCalculation {
    nonisolated static func sumOfSquareRoots(
        terms: Int,
        checkEvery: Int = 4_096
    ) throws -> Double {
        precondition(terms >= 0, "terms must be nonnegative")
        precondition(checkEvery > 0, "checkEvery must be positive")

        try Task.checkCancellation()

        var completed = 0
        var total = 0.0

        while completed < terms {
            try Task.checkCancellation()

            let count = min(checkEvery, terms - completed)
            let end = completed + count

            while completed < end {
                completed += 1
                total += Double(completed).squareRoot()
            }
        }

        try Task.checkCancellation()
        return total
    }
}

Fungsi ini memeriksa pembatalan sebelum mulai, di setiap batas chunk, dan sebelum mengembalikan hasil. Titik-titik berhenti ini disengaja: permintaan pembatalan selama sebuah chunk akan terdeteksi pada pemeriksaan berikutnya. Task.checkCancellation() bersifat sinkron, sehingga perhitungan dapat tetap menjadi fungsi throwing biasa; fungsi ini tidak memerlukan await hanya untuk memeriksa pembatalan. checkCancellation().

Perhitungan chunk juga menjaga batas input tetap jelas. Nol suku menghasilkan nol jika task tidak dibatalkan. Jumlah suku negatif dan ukuran chunk yang tidak positif adalah kesalahan pemrogram yang diberlakukan melalui precondition. Menghitung setiap chunk berdasarkan jumlah yang tersisa mencegah titik akhirnya melampaui terms.

Akumulator tetap lokal di worker. Error pembatalan yang dilempar membuat fungsi keluar tanpa mengembalikan akumulator tersebut, sehingga kontrak hasil lengkap yang dipilih diterapkan. Karena ini penjumlahan floating-point, nilai yang selesai merupakan pendekatan dari jumlah matematisnya.

Jaga tombol Cancel tetap responsif

Pemeriksaan pembatalan tidak membantu jika aksi tombol tidak mendapat kesempatan untuk dijalankan. Apple menjelaskan bahwa menempatkan pekerjaan berat yang sinkron di dalam Task biasa yang dibuat dari view SwiftUI tetap dapat menyibukkan main actor, karena task tersebut mewarisi konteks actor di sekitarnya. Apple mendokumentasikan detached task sebagai cara yang didukung untuk menjalankan pekerjaan sinkron di luar konteks yang diwarisi tersebut. Meningkatkan responsivitas aplikasi.

Di sini, worker terpisah hanya menangkap integer yang immutable dan memanggil fungsi sinkron nonisolated secara eksplisit. Semua state antarmuka tetap berada di main actor. Task terpisah di main actor menunggu nilai dari worker lalu memperbarui tampilan.

Ganti file entry aplikasi yang dihasilkan dengan file lengkap berikut, dan hapus ContentView.swift yang dihasilkan jika masih ada. Pastikan hanya ada satu deklarasi @main di target, sebagaimana diwajibkan oleh siklus hidup aplikasi SwiftUI. Kedua file ini membentuk aplikasi contoh. App.

// CancellationDemoApp.swift
import Foundation
import SwiftUI

@main
struct CancellationDemoApp: App {
    var body: some Scene {
        WindowGroup {
            CalculationView()
        }
    }
}

@MainActor
struct CalculationView: View {
    @State private var worker: Task<Double, Error>?
    @State private var runID: UUID?
    @State private var status = "Ready"

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Synthetic square-root sum")
                .font(.headline)
            Text(status)
                .textSelection(.enabled)

            HStack {
                Button("Start") {
                    start()
                }
                .disabled(worker != nil)

                Button("Cancel") {
                    guard let worker else { return }
                    worker.cancel()
                    status = "Cancellation requested"
                }
                .disabled(worker == nil)
            }
        }
        .padding(24)
        .frame(minWidth: 420, minHeight: 160)
        .onDisappear {
            worker?.cancel()
            runID = nil
            worker = nil
            status = "Ready"
        }
    }

    private func start() {
        guard worker == nil else { return }

        let id = UUID()
        let terms = 500_000_000
        runID = id
        status = "Calculating"

        let run = Task.detached(priority: .userInitiated) {
            try SyntheticCalculation.sumOfSquareRoots(terms: terms)
        }
        worker = run

        Task { @MainActor in
            do {
                let value = try await run.value
                guard runID == id else { return }

                if run.isCancelled {
                    status = "Cancelled"
                } else {
                    status = "Result: \(value)"
                }
            } catch is CancellationError {
                guard runID == id else { return }
                status = "Cancelled"
            } catch {
                guard runID == id else { return }
                status = "Failed: \(error)"
            }

            guard runID == id else { return }
            worker = nil
            runID = nil
        }
    }
}

Menyimpan worker sangat penting. Menghapus handle task tidak membatalkan task; tindakan itu menghilangkan kemampuan Anda untuk membatalkannya melalui handle tersebut. Menunggu value dari worker yang melempar error juga menangani error-nya. Detached task memerlukan pengelolaan siklus hidup secara eksplisit, sehingga tombol dan handler saat view menghilang membatalkan worker secara langsung. Membatalkan hanya task yang menunggu hasilnya tidak akan meneruskan pembatalan ke worker terpisah ini. Dokumentasi detached task.

UUID dari Foundation menyediakan pengenal untuk setiap proses. Perbandingan pengenal ini merupakan aturan pada tingkat aplikasi: setelah view menghilang dan membatalkan suatu proses, penyelesaiannya nanti tidak dapat memperbarui status terkini view ini. Pengenal tersebut tidak menghentikan komputasi; worker.cancel() dan pemeriksaan di dalam loop yang menanganinya. UUID.

Tombol Start tetap dinonaktifkan sampai worker selesai atau melempar error. Menekan Cancel terlebih dahulu menampilkan Cancellation requested; observer penyelesaian mengubahnya menjadi Cancelled saat menangani hasilnya. Dengan begitu, permintaan pembatalan dibedakan dari konfirmasi bahwa worker telah berhenti.

Bagaimana sebaiknya hasil yang terlambat ditangani?

Pertimbangkan urutan ketika worker melewati pemeriksaan terakhirnya, selesai, lalu pengguna menekan Cancel sebelum main actor menerima nilainya. Apple secara eksplisit menyebutkan bahwa pembatalan setelah pemeriksaan terakhir yang mungkin dilakukan dapat tidak menimbulkan efek yang terlihat pada worker. Pemeriksaan terakhir mempersempit jeda tersebut, tetapi tidak dapat menghilangkannya. cancel().

Karena itu, contoh ini memeriksa run.isCancelled setelah menunggu nilai yang berhasil. Ini memeriksa handle worker. Menggunakan Task.isCancelled yang statis di sana akan memeriksa task observer. Panduan bahasa Swift membedakan pemeriksaan task saat ini melalui properti tipe dengan pemeriksaan task lain melalui properti instansnya. Concurrency.

Aturan penerimaannya jelas: pembatalan yang diproses sebelum hasil diterima akan diutamakan. Setelah penerimaan selesai dan worker dihapus, tombol Cancel yang dinonaktifkan tidak memiliki hasil selesai yang bisa ditarik kembali. Aturan ini mengikuti urutan aksi di main actor, tanpa menjanjikan bahwa pembatalan membalikkan pekerjaan yang sudah selesai.

Pilih interval pemeriksaan pembatalan yang sesuai

Ukuran chunk default 4.096 suku merupakan titik awal ilustratif. Chunk yang lebih kecil memberi lebih banyak kesempatan untuk mendeteksi pembatalan dan menjalankan lebih banyak pemeriksaan. Chunk yang lebih besar mengurangi frekuensi pemeriksaan, tetapi memungkinkan lebih banyak komputasi di antara titik penghentian.

Sebagai perkiraan kasar, misalkan sebuah chunk berisi 4.096 suku dan setiap suku memerlukan dua mikrodetik sebagai angka ilustratif. Chunk itu mewakili sekitar 8,2 milidetik komputasi. Angka-angka ini dibuat untuk menunjukkan perhitungannya, bukan hasil pengukuran Swift atau Mac tertentu. Penjadwalan dan pembaruan antarmuka dapat menambah waktu tunggu.

Pengamatan Hal yang perlu diperiksa atau diubah
Antarmuka berhenti merespons selama perhitungan Pastikan loop berjalan di worker terpisah dan tetap berada di luar isolasi main actor.
Antarmuka merespons, tetapi pembatalan memerlukan waktu terlalu lama Kurangi checkEvery dan ukur durasi chunk terlama.
Sebuah chunk kemudian mendapat satu operasi yang mahal Tambahkan titik penghentian di dalam operasi tersebut jika implementasinya mendukungnya.
Proses yang dibatalkan menampilkan hasil Periksa handle worker saat penerimaan dan pertahankan pemeriksaan pengenal proses.

Jangan menambahkan handler pembatalan hanya untuk mengatur flag lain bagi loop ini. Task sudah menyediakan status pembatalan, dan panduan bahasa Swift menjelaskan bahwa memasang handler tetap membuat penghentian bersifat kooperatif. Untuk perhitungan lokal ini, pemeriksaan eksplisit menyediakan mekanisme penghentian yang diperlukan. Concurrency.

Verifikasi penyelesaian, pembatalan, dan proses berulang

Compiler Swift dan runtime macOS tidak tersedia di lingkungan riset, jadi contoh ini telah ditinjau tetapi belum dikompilasi atau dijalankan. Build kedua file di Xcode, lalu verifikasi perilaku berikut secara terpisah:

  1. Hasil kecil yang selesai: ubah sementara terms menjadi 4. Hasil yang ditampilkan seharusnya sekitar 6.146264369941973, dengan kemungkinan pembulatan floating-point.
  2. Input kosong: ubah terms menjadi 0. Proses yang tidak dibatalkan seharusnya menampilkan nol.
  3. Pembatalan saat bekerja: kembalikan jumlah suku ke 500_000_000, tekan Start, lalu tekan Cancel saat proses berjalan. Harapkan Cancellation requested, lalu Cancelled tanpa hasil parsial.
  4. Proses lainnya: setelah pembatalan selesai, tekan Start lagi. Proses baru seharusnya dimulai secara normal.
  5. View menghilang: tutup jendela yang sedang melakukan perhitungan. Handler saat view menghilang seharusnya meminta pembatalan dan membatalkan pembaruan antarmuka untuk proses tersebut.

Jumlah suku yang besar dimaksudkan untuk memberi waktu agar Anda dapat berinteraksi, tetapi durasinya bergantung pada Mac dan konfigurasi build. Jika proses selesai sebelum Anda sempat membatalkannya, tingkatkan jumlahnya dalam rentang integer yang didukung. Validasi aritmetika yang selesai menggunakan input kecil, alih-alih menunggu demonstrasi besar selesai.

Desain akhir ini memiliki jalur penghentian yang jelas dan aturan penerimaan yang jelas: tombol membatalkan worker yang disimpan, batas chunk memungkinkan perhitungan melempar error, dan main actor hanya menerima hasil yang tidak dibatalkan dari proses terkini. Ketiga keputusan itu membuat pembatalan berguna untuk perhitungan Mac biasa.

Sumber