لجعل عملية حسابية في Swift قابلة للإلغاء، احتفظ بمقبض مهمتها، واستدعِ cancel() عند طلب الإلغاء، وتحقق من الإلغاء داخل الحلقة. يحافظ هذا المثال لنظام macOS أيضًا على استجابة الواجهة ويمنع تشغيلًا ملغى من عرض نتيجة متأخرة.

ما الذي يفعله إلغاء مهمة Swift فعليًا؟

تستخدم Swift الإلغاء التعاوني: يؤدي طلب الإلغاء إلى وضع علامة على المهمة، بينما يقرر الرمز قيد التشغيل متى يتوقف. وقد تستمر حلقة وحدة المعالجة المركزية التي لا تتحقق مطلقًا من الإلغاء حتى اكتمالها. يؤدي استدعاء Task.checkCancellation() إلى طرح CancellationError عند ملاحظة الإلغاء. وثائق Apple حول الإلغاء، وcheckCancellation().

مهمة القارئ هنا محددة: أضف زري Start وCancel عاملين إلى تطبيق Mac صغير يحسب مجموعًا اصطناعيًا. يُنشأ كل مُدخل محليًا. تجمع العملية الجذور التربيعية لأعداد صحيحة موجبة متتالية، ولا تنتج قيمة Double إلا بعد معالجة جميع الحدود المطلوبة.

اختر عقد النتيجة قبل تنفيذ الإلغاء. يتخلص هذا المثال من المجموع غير المكتمل ويُبلغ عن الإلغاء بشكل منفصل. أما إرجاع مجموع جزئي فيتطلب عقدًا مختلفًا يحدد عدد الحدود التي أُدرجت؛ وإلا فقد يظن المستدعي أن العمل غير المكتمل هو الإجابة المطلوبة. يوثق دليل لغة Swift طرح الأخطاء وإرجاع نتيجة فارغة وإرجاع عمل جزئي بوصفها استجابات محتملة للإلغاء. التزامن.

هناك مسؤوليتان ينبغي تنفيذهما: إيقاف العمليات الحسابية غير الضرورية، وتحديد ما إذا كانت القيمة المكتملة لا تزال مناسبة للواجهة. يتولى المثال الأمرين، بما في ذلك حالة طلب الإلغاء بعد التحقق الأخير للعامل وقبل عرض نتيجته.

أضف عمليات التحقق من الإلغاء داخل العملية الحسابية

أنشئ مشروع تطبيق macOS في Xcode باستخدام SwiftUI وSwift. استخدم وضع لغة Swift 6 وهدف نشر macOS 13 أو إصدارًا أحدث لهذا المثال. أضف الملف الكامل التالي، واسمه SyntheticCalculation.swift. لا يعتمد هذا الملف على الواجهة.

// 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
    }
}

تتحقق الدالة من الإلغاء قبل البدء، وعند حدود كل جزء، وقبل الإرجاع. هذه نقاط توقف مقصودة: يُلاحظ طلب الإلغاء أثناء معالجة جزء عند التحقق التالي. إن Task.checkCancellation() متزامنة، لذا يمكن أن تظل العملية الحسابية دالة عادية ترمي الأخطاء؛ ولا تحتاج إلى await لمجرد التحقق من الإلغاء. checkCancellation().

تحافظ عملية حساب الأجزاء أيضًا على وضوح حدود المُدخلات. ينتج عن صفر من الحدود صفر إذا لم تكن المهمة ملغاة. أما الحدود السالبة وحجم الجزء غير الموجب فهما خطآن برمجيان تفرضهما الشروط المسبقة. ويؤدي حساب كل جزء استنادًا إلى العدد المتبقي إلى تجنب تجاوز نهايته terms.

يبقى المُجمّع محليًا داخل العامل. يؤدي طرح خطأ الإلغاء إلى خروج الدالة دون إرجاع ذلك المُجمّع، وبذلك يُنفَّذ عقد النتيجة الكاملة المختار. ولأن هذه عملية جمع بأعداد فاصلة عائمة، فإن القيمة المكتملة تقارب المجموع الرياضي.

أبقِ زر Cancel مستجيبًا

لن تفيد عمليات التحقق من الإلغاء إذا لم تتح لعملية الزر فرصة للتنفيذ. توضح Apple أن وضع عمل متزامن مكثف داخل Task عادية تُنشأ من عرض SwiftUI قد يظل يشغل الممثل الرئيسي، لأن تلك المهمة ترث سياق الممثل المحيط بها. وتوثق Apple المهمة المنفصلة بوصفها طريقة مدعومة لتنفيذ عمل متزامن خارج ذلك السياق الموروث. تحسين استجابة التطبيق.

هنا، لا يلتقط العامل المنفصل سوى عدد صحيح غير قابل للتغيير، ويستدعي دالة متزامنة محددة صراحةً بـ nonisolated. وتبقى كل حالة الواجهة على الممثل الرئيسي. تنتظر مهمة منفصلة على الممثل الرئيسي قيمة العامل وتحدّث العرض.

استبدل ملف إدخال التطبيق المُنشأ بالملف الكامل التالي، واحذف ContentView.swift المُنشأ إن كان لا يزال موجودًا. أبقِ تصريحًا واحدًا بالضبط لـ @main في الهدف، وفقًا لما تقتضيه دورة حياة تطبيق SwiftUI. يشكّل هذان الملفان معًا تطبيق المثال. 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
        }
    }
}

يُعد الاحتفاظ بـ worker أمرًا أساسيًا. إسقاط مقبض المهمة لا يلغيها؛ بل يزيل إمكانية إلغائها عبر ذلك المقبض. كما أن انتظار value للعامل الذي يرمي الأخطاء يتيح أيضًا رصد خطئه. تتطلب المهام المنفصلة إدارة صريحة لدورة حياتها، لذا يلغي الزر ومعالج الاختفاء العامل مباشرةً. إلغاء المهمة التي تنتظر نتيجته فقط لا يضمن انتقال الإلغاء إلى هذا العامل المنفصل. وثائق المهام المنفصلة.

يوفر UUID في Foundation معرّفًا لكل تشغيل. ومقارنة المعرّفات قاعدة على مستوى التطبيق: بمجرد أن يبطل الاختفاء تشغيلًا ما، لا يمكن لاكتماله اللاحق أن يحدّث حالة هذا العرض الحالية. لا يوقف المعرّف العملية الحسابية؛ ويتولى ذلك worker.cancel() وعمليات التحقق في الحلقة. UUID.

يظل زر Start معطلًا حتى ينتهي العامل أو يطرح خطأً. يؤدي الضغط على Cancel أولًا إلى عرض Cancellation requested؛ ثم يغيّر مراقب الاكتمال ذلك إلى Cancelled عند معالجة النتيجة. وبذلك يظل طلب الإلغاء متميزًا عن التأكد من انتهاء العامل.

كيف ينبغي التعامل مع النتيجة المتأخرة؟

تأمل التسلسل الذي يجتاز فيه العامل التحقق الأخير وينتهي، ثم يضغط المستخدم Cancel قبل أن يقبل الممثل الرئيسي قيمته. تشير Apple صراحةً إلى أن الإلغاء بعد آخر نقطة تحقق ممكنة قد لا يكون له أثر ملحوظ على العامل. يقلص التحقق الأخير هذه الفترة، لكنه لا يستطيع إلغاءها. cancel().

لذلك يتحقق المثال من run.isCancelled بعد انتظار قيمة ناجحة. وهذا تحقق من مقبض العامل. أما استخدام Task.isCancelled الثابتة في ذلك الموضع فيتحقق من مهمة المراقب بدلًا من ذلك. يميز دليل لغة Swift بين التحقق من المهمة الحالية باستخدام خاصية النوع، والتحقق من مهمة أخرى باستخدام خاصية المثيل. التزامن.

يضع هذا قاعدة قبول محددة: يفوز الإلغاء إذا عولج قبل قبول النتيجة. بعد اكتمال القبول ومسح العامل، لا توجد نتيجة مكتملة يمكن لزر Cancel المعطل التراجع عنها. وتتبع القاعدة ترتيب إجراءات الممثل الرئيسي بدلًا من الوعد بأن الإلغاء يعكس العمل المكتمل.

اختر فترة زمنية مفيدة للتحقق من الإلغاء

حجم الجزء الافتراضي، وهو 4,096 حدًا، نقطة بداية توضيحية. تتيح الأجزاء الأصغر فرصًا أكثر لملاحظة الإلغاء، لكنها تنفذ عمليات تحقق أكثر. أما الأجزاء الأكبر فتقلل تواتر التحقق، مع السماح بمزيد من العمليات الحسابية بين نقاط التوقف.

لإجراء حساب تقريبي لأغراض التخطيط، افترض أن الجزء يحتوي على 4,096 حدًا، وأن معالجة كل حد تستغرق ميكروثانيتين توضيحيتين. يمثّل هذا الجزء نحو 8.2 ميلي ثانية من الحساب. هذه مُدخلات افتراضية لتوضيح الحساب، وليست قياسات لـ Swift أو لجهاز Mac بعينه. وقد يضيف الجدولة وتحديثات الواجهة مزيدًا من التأخير.

الملاحظة ما ينبغي فحصه أو تغييره
تتوقف الواجهة عن الاستجابة أثناء العملية الحسابية تحقق من أن الحلقة تعمل في العامل المنفصل وتبقى خارج عزل الممثل الرئيسي.
تستجيب الواجهة، لكن الإلغاء يستغرق وقتًا طويلًا قلّل checkEvery وقِس مدة أطول جزء.
أُضيفت لاحقًا عملية مكلفة إلى أحد الأجزاء أضف نقاط توقف داخل تلك العملية إذا كان تنفيذها يدعم ذلك.
يعرض تشغيل ملغى نتيجةً تحقق من مقبض العامل وقت القبول، واحتفظ بحارس معرّف التشغيل.

لا تضف معالج إلغاء لمجرد تعيين علامة أخرى لهذه الحلقة. فالمهمة تعرض حالة الإلغاء بالفعل، ويوضح دليل لغة Swift أن تثبيت معالج لا يغير حقيقة أن الإيقاف يظل تعاونيًا. توفر عمليات التحقق الصريحة آلية الإيقاف اللازمة لهذا الحساب المحلي. التزامن.

تحقّق من الاكتمال والإلغاء والتشغيلات المتكررة

لم يكن مترجم Swift ولا بيئة تشغيل macOS متاحين في بيئة البحث، لذا جرت مراجعة هذا المثال دون ترجمته أو تنفيذه. أنشئ كلا الملفين في Xcode، ثم تحقّق من السلوكيات المختلفة التالية:

  1. نتيجة مكتملة صغيرة: غيّر terms مؤقتًا إلى 4. ينبغي أن تكون النتيجة المعروضة نحو 6.146264369941973، مع السماح بتقريب الفاصلة العائمة.
  2. مُدخل فارغ: غيّر terms إلى 0. ينبغي أن يعرض التشغيل غير الملغى صفرًا.
  3. الإلغاء أثناء العمل: أعد القيمة إلى 500_000_000، واضغط Start ثم اضغط Cancel أثناء التشغيل. توقّع ظهور Cancellation requested ثم Cancelled دون نتيجة جزئية.
  4. تشغيل آخر: بعد اكتمال الإلغاء، اضغط Start مجددًا. ينبغي أن يبدأ التشغيل الجديد بشكل طبيعي.
  5. اختفاء العرض: أغلق نافذة أثناء إجراء الحساب. ينبغي لمعالج الاختفاء أن يطلب الإلغاء ويبطل تحديثات الواجهة لذلك التشغيل.

يُراد من العدد الكبير للحدود إتاحة وقت للتفاعل، لكن المدة تعتمد على جهاز Mac وإعدادات الإنشاء. إذا اكتمل التشغيل قبل أن تتمكن من إلغائه، فزِد العدد ضمن نطاق الأعداد الصحيحة المدعوم. تحقّق من الحساب المكتمل باستخدام المُدخل الصغير بدلًا من انتظار انتهاء العرض التوضيحي الكبير.

يتميز التصميم المكتمل بمسار إيقاف واضح وقاعدة قبول واضحة: يلغي الزر العامل المحتفَظ به، وتتيح حدود الأجزاء للعملية الحسابية أن تطرح خطأً، ولا يقبل الممثل الرئيسي إلا نتيجة غير ملغاة من التشغيل الحالي. تجعل هذه القرارات الثلاثة الإلغاء مفيدًا في عملية حسابية عادية على Mac.

المصادر