सहवर्ती टास्क विफल होने पर भी Swift की सफल गणनाएँ बनाए रखने के लिए, हर चाइल्ड टास्क के भीतर उसकी गणना की त्रुटि पकड़ें और परिणाम का मान लौटाएँ। Mac के लिए यह कमांड-लाइन उदाहरण हर परिणाम एकत्र करता है, इनपुट का क्रम बनाए रखता है और सफल मान उपलब्ध रखता है।

यह एक छोटा, कृत्रिम बैच-गणना कार्य है: गैर-ऋणात्मक पूर्णांकों की सूची का वर्ग निकालना और बताना कि कौन-से इनपुट सफल हुए। ऋणात्मक इनपुट और पूर्णांक ओवरफ़्लो से अलग-अलग विफलताएँ होती हैं। गणना का पूरा डेटा मेमोरी में रहता है; प्रोग्राम अपने परिणाम Terminal में प्रिंट करता है।

एक त्रुटि परिणाम एकत्र करने की प्रक्रिया क्यों रोक सकती है?

किसी throwing चाइल्ड टास्क से उसकी सहवर्ती टास्क तुरंत रद्द नहीं होतीं। निर्णायक सीमा टास्क-समूह का बॉडी है: जब कोई त्रुटि उस बॉडी से बाहर निकलती है, तो Swift बाकी चाइल्ड टास्क रद्द कर देता है। Apple अपने withThrowingTaskGroup(of:returning:isolation:body:) संदर्भ में इस अंतर को बताता है।

इसलिए, बाहरी catch वाले throwing iteration loop में पहली देखी गई त्रुटि पर परिणाम एकत्र करना रुक सकता है। पहले के मान शायद पहले ही संग्रहीत हो चुके हों, लेकिन उस catch के बाद लूप फिर शुरू नहीं होता। यदि त्रुटि समूह की बॉडी से बाहर निकलती है, तो बाकी चाइल्ड टास्क को रद्दीकरण संकेत मिलता है।

स्वतंत्र गणनाओं के लिए, विफलता की नीति स्पष्ट रखें। नीचे दिए गए उदाहरण में ऐसा टास्क समूह है जो स्वयं त्रुटियाँ नहीं फेंकता; इसके चाइल्ड टास्क सफलता, विफलता या रद्दीकरण लौटाते हैं। गणना फिर भी त्रुटि फेंकती है; उसका wrapper उस त्रुटि के समूह तक पहुँचने से पहले उसे एक मान में बदल देता है।

यह Swift की दस्तावेज़ित सीमा के अनुरूप है: ऐसा टास्क समूह जिसके चाइल्ड टास्क त्रुटियाँ आगे नहीं भेज सकते, उनमें त्रुटियों को आंतरिक रूप से संभालना या उनका परिणाम के मान के रूप में निरूपण करना ज़रूरी है। Apple का TaskGroup documentation देखें।

तय करें कि हर इनपुट को क्या लौटाना है

बैच का अनुबंध है कि हर इनपुट स्थान के लिए एक परिणाम लौटे। सफल गणना सटीक पूर्णांक वर्ग लौटाती है। अस्वीकृत गणना विफलता का विवरण लौटाती है। रद्दीकरण देखे जाने के कारण छोड़ी गई गणना रद्दीकरण का परिणाम लौटाती है।

स्थिति परिणाम बैच का व्यवहार
गैर-ऋणात्मक इनपुट जिसका वर्ग Int64 में समा जाए वर्ग वाला सफलता परिणाम मान बनाए रखें
ऋणात्मक इनपुट विफलता: negativeInput दूसरे परिणाम एकत्र करना जारी रखें
वर्ग Int64 की सीमा से बड़ा हो विफलता: overflow दूसरे परिणाम एकत्र करना जारी रखें
गणना से पहले रद्दीकरण दिखे रद्द पूरे हो चुके परिणाम बनाए रखें और लंबित काम छोड़ें

ऋणात्मक इनपुट अस्वीकार करना इस उदाहरण का अनुप्रयोग-नियम है; गणितीय रूप से ऋणात्मक संख्याओं का भी वर्ग निकाला जा सकता है। इस नियम को ओवरफ़्लो से अलग रखने पर नमूना विफलता की दो अलग स्थितियाँ दिखाता है।

गुणा करने के लिए multipliedReportingOverflow(by:) का इस्तेमाल करें और उसका ओवरफ़्लो फ़्लैग जाँचें। Apple बताता है कि लौटाया गया आंशिक मान पूरा गुणनफल तभी होता है जब वह फ़्लैग false हो; वरना वह truncated होता है। उदाहरण उस truncated मान को छोड़कर त्रुटि फेंकता है। Int64.multipliedReportingOverflow(by:) देखें।

हर परिणाम में इनपुट का इंडेक्स भी होता है। Swift चाइल्ड टास्क के पूरा होने का क्रम सुनिश्चित नहीं करता, इसलिए सिर्फ़ पूरे हुए मान जोड़ने से मूल क्रम बनाए नहीं रखा जा सकता। भाषा की गाइड Concurrency में शेड्यूलिंग के इस व्यवहार का वर्णन करती है।

अपने Mac पर पूरा उदाहरण चलाएँ

Swift 6 या उसके बाद का टूलचेन वाला Mac इस्तेमाल करें। Terminal में swift --version चलाकर इंस्टॉल किया गया कंपाइलर जाँचें, जैसा Swift.org की Getting Started guide सुझाती है। यदि Swift उपलब्ध न हो, तो आगे बढ़ने से पहले उस गाइड में दिए इंस्टॉलेशन लिंक का पालन करें।

निम्न पूरा प्रोग्राम किसी कामकाजी फ़ोल्डर में Calculations.swift नाम से सेव करें। यह सक्रिय प्रोसेसर की संख्या के आधार पर शुरुआती concurrency सीमा चुनने के लिए Foundation इंपोर्ट करता है।

import Foundation

enum CalculationError: Error {
    case negativeInput
    case overflow
}

enum Outcome: Sendable {
    case success(Int64)
    case failure(String)
    case cancelled
}

struct Row: Sendable {
    let index: Int
    let outcome: Outcome
}

func square(_ input: Int64) throws -> Int64 {
    guard input >= 0 else {
        throw CalculationError.negativeInput
    }
    let product = input.multipliedReportingOverflow(by: input)
    guard !product.overflow else {
        throw CalculationError.overflow
    }
    return product.partialValue
}

func evaluate(index: Int, input: Int64) -> Row {
    do {
        try Task.checkCancellation()
        let value = try square(input)
        return Row(index: index, outcome: .success(value))
    } catch is CancellationError {
        return Row(index: index, outcome: .cancelled)
    } catch {
        return Row(
            index: index,
            outcome: .failure(String(describing: error))
        )
    }
}

func collect(
    _ inputs: [Int64],
    maxConcurrent: Int
) async -> [Row] {
    guard !inputs.isEmpty else { return [] }
    let width = min(inputs.count, max(1, maxConcurrent))

    return await withTaskGroup(of: Row.self) { group in
        var rows: [Row] = []
        rows.reserveCapacity(inputs.count)

        for index in 0..<width {
            let input = inputs[index]
            group.addTask {
                evaluate(index: index, input: input)
            }
        }
        var nextIndex = width

        while let row = await group.next() {
            rows.append(row)

            if Task.isCancelled {
                while nextIndex < inputs.count {
                    rows.append(Row(
                        index: nextIndex,
                        outcome: .cancelled
                    ))
                    nextIndex += 1
                }
            } else if nextIndex < inputs.count {
                let index = nextIndex
                let input = inputs[index]
                nextIndex += 1
                group.addTask {
                    evaluate(index: index, input: input)
                }
            }
        }

        return rows.sorted { $0.index < $1.index }
    }
}

@main
struct CalculationDemo {
    static func main() async {
        let inputs: [Int64] = [3, -2, 4, Int64.max, 0]
        let width = min(
            4,
            max(1, ProcessInfo.processInfo.activeProcessorCount)
        )
        let rows = await collect(inputs, maxConcurrent: width)
        var successes: [Int64] = []

        for row in rows {
            let input = inputs[row.index]
            switch row.outcome {
            case .success(let value):
                successes.append(value)
                print("[\(row.index)] \(input): success \(value)")
            case .failure(let message):
                print("[\(row.index)] \(input): failure \(message)")
            case .cancelled:
                print("[\(row.index)] \(input): cancelled")
            }
        }
        print("Successful values: \(successes)")
    }
}

उस फ़ोल्डर से प्रोग्राम कंपाइल और रन करें:

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

-parse-as-library विकल्प इस एक स्रोत फ़ाइल को अपना स्पष्ट @main entry point इस्तेमाल करने देता है। एक्ज़ीक्यूटेबल एप्लिकेशन डेटा पढ़े या लिखे बिना गणनाएँ करता है। शोध परिवेश में Swift उपलब्ध नहीं था, इसलिए वहाँ इस प्रोग्राम का निरीक्षण तो किया गया, लेकिन इसे कंपाइल या चलाया नहीं गया।

दिए गए इनपुट के लिए, रद्दीकरण के बिना, अपेक्षित आउटपुट यह है:

[0] 3: सफलता 9
[1] -2: विफलता negativeInput
[2] 4: सफलता 16
[3] 9223372036854775807: विफलता overflow
[4] 0: सफलता 0
सफल मान: [9, 16, 0]

परिणाम एकत्र होने के बाद इनपुट इंडेक्स के आधार पर उन्हें सॉर्ट करने से प्रिंट हुआ क्रम मिलता है। प्रस्तुति बदले बिना निष्पादन का क्रम अलग हो सकता है। सफल मानों का array बाद में बनाया जाता है, जिससे पूरे परिणामों की रिपोर्ट निरीक्षण के लिए उपलब्ध रहती है।

परिणाम एकत्र करने वाला हर सफल नतीजा कैसे बनाए रखता है?

परिणामों का array पैरेंट टास्क के पास रहता है। हर चाइल्ड एक इंडेक्स और एक immutable इनपुट लेता है, उसका परिणाम निकालता है और एक row लौटाता है। चाइल्ड सीधे पैरेंट के array में नहीं जोड़ते। यह भाषा की गाइड के उस तरीके के अनुरूप है जिसमें टास्क-समूह की बॉडी के भीतर चाइल्ड के परिणाम एकत्र किए जाते हैं। Concurrency देखें।

परिणाम एकत्र करने वाला शुरू में सीमित संख्या में चाइल्ड टास्क शुरू करता है। हर पूरी हुई row से अगला इनपुट देने के लिए एक जगह खाली होती है। इसलिए विफल गणना भी बैच को सफल गणना की तरह आगे बढ़ाती है: दोनों एक सामान्य row लौटाते हैं और परिणामों का संग्रह जारी रहता है।

Foundation का activeProcessorCount कंप्यूटर पर उपलब्ध सक्रिय प्रोसेसिंग कोर की संख्या बताता है। उदाहरण में एक व्याख्यात्मक शेड्यूलिंग विकल्प के तौर पर यह संख्या अधिकतम चार रखी गई है। यह सीमा न तो मापकर निकाला गया सर्वोत्तम मान है, न ही इस बात की गारंटी कि चार गणनाएँ एक साथ चलेंगी। Apple का activeProcessorCount documentation देखें।

Concurrency की सीमा एक समय में लंबित चाइल्ड टास्क की संख्या बाँधती है, जबकि इनपुट की संख्या के साथ परिणामों का array बढ़ता रहता है। यह डेस्कटॉप पर छोटे बैच के लिए ठीक है। इन छोटे वर्गों के लिए, यह उदाहरण परिणाम-संभाल दिखाता है; यह क्रमिक लूप की तुलना में बेहतर प्रदर्शन का दावा नहीं करता।

खाली इनपुट की जाँच खाली array लौटाती है। शून्य या ऋणात्मक concurrency सीमा को एक कर दिया जाता है। समान इनपुट अलग-अलग बने रहते हैं, क्योंकि उनकी पहचान स्थान से होती है; इसलिए दो समान इनपुट फिर भी दो rows देते हैं।

पैरेंट टास्क रद्द होने पर क्या होता है?

Swift में रद्दीकरण cooperative है। पैरेंट टास्क रद्द करने से रद्दीकरण उसके structured चाइल्ड टास्क तक पहुँचता है, लेकिन चाइल्ड को उस संकेत को देखकर उस पर प्रतिक्रिया देनी होती है। Apple का TaskGroup documentation संकेत के पहुँचने और उसके लिए cooperative तरीके से प्रतिक्रिया देने की ज़रूरत—दोनों समझाता है।

यहाँ, evaluate गणना से पहले Task.checkCancellation() को कॉल करता है। Apple दस्तावेज़ बताता है कि रद्दीकरण दिखने पर यह मेथड CancellationError फेंकता है; wrapper इसे अलग से पकड़कर रद्द की गई row लौटाता है। checkCancellation() देखें।

जब परिणाम एकत्र करने वाला रद्दीकरण देखता है, तो वह उन इनपुट को रद्द चिह्नित करता है जिनके लिए अभी तक चाइल्ड टास्क शुरू नहीं हुई हैं। शुरू की जा चुकी चाइल्ड टास्क के परिणाम फिर भी एकत्र किए जाते हैं। उनके सफल नतीजे रिपोर्ट में बने रहते हैं, और रद्दीकरण देखने वाली चाइल्ड रद्द की गई rows लौटाती हैं।

रद्दीकरण किसी चाइल्ड की जाँच के बाद भी आ सकता है। वह चाइल्ड फिर भी सफलतापूर्वक पूरी हो सकती है, और यह अनुबंध इसकी अनुमति देता है। यदि गणना लंबा लूप बन जाए, तो उसके भीतर उपयोगी अंतराल पर रद्दीकरण की जाँचें जोड़ें। लौटने से पहले समूह शुरू की गई सभी चाइल्ड टास्क के पूरा होने की प्रतीक्षा करता है, जैसा TaskGroup के लिए दस्तावेज़ित है।

इसके बजाय throwing task group कब इस्तेमाल करें?

यदि मौजूदा चाइल्ड टास्क पहले से त्रुटियाँ फेंकती हैं और आप वही interface बनाए रखना चाहते हैं, तो throwing task group से भी अलग-अलग परिणाम एकत्र किए जा सकते हैं। बार-बार nextResult() कॉल करें और लौटाई गई हर सफलता या विफलता पर तब तक स्विच करें, जब तक वैकल्पिक परिणाम खाली न हो जाए। next() के विपरीत, यह मेथड चाइल्ड की त्रुटि कॉलर को फेंकने के बजाय Result मान लौटाता है। Apple completion-order व्यवहार को nextResult() में दर्ज करता है।

इस डिज़ाइन में, सफल मानों और श्रेय बताने की ज़रूरत वाली त्रुटियों—दोनों में इनपुट की पहचान शामिल करें। साधारण arithmetic त्रुटि यह नहीं बताती कि कई इनपुट में से किसने उसे पैदा किया। साथ ही, रद्दीकरण को सामान्य गणना-विफलता मानने के बजाय उसकी स्पष्ट नीति चुनें।

जब हर इनपुट की पूरी रिपोर्ट मुख्य ज़रूरत हो, तो परिणाम लौटाने वाला तरीका इस्तेमाल करें। जब समूह से त्रुटि बाहर भेजना बैच का वांछित व्यवहार हो, या nextResult() के ज़रिए मौजूदा throwing ऑपरेशन इस्तेमाल करना अधिक सुविधाजनक हो, तब throwing group चुनें।

इस स्थानीय गणना कार्य के लिए ज़रूरी अनुबंध सरल है: हर सफल मान बनाए रखें, हर विफल इनपुट दर्ज करें और संग्रह जारी रखें। हर परिणाम के साथ इनपुट इंडेक्स रखने से अंतिम रिपोर्ट उपयोगी रहती है, भले ही गणनाएँ अप्रत्याशित क्रम में पूरी हों।

स्रोत