Concurrent Swift गणनाओं में टाइप की गई त्रुटियाँ सुरक्षित रखने के लिए, हर चाइल्ड टास्क से टाइप किया हुआ Result लौटाएँ और संग्रह के बाद उसे अनरैप करें। यह Mac उदाहरण सिंथेटिक आयतों के पिक्सेल की संख्या की गणना करता है और इनपुट क्रम, स्पष्ट विफलताओं तथा पूरे बैच के सफल होने या त्रुटि देने की शर्त को बनाए रखता है।
टास्क-ग्रुप की सीमा पर टाइप किए हुए नतीजे क्यों इस्तेमाल करें?
मान लें कि आप डेस्कटॉप ग्राफ़िक्स प्रीव्यू बना रहे हैं, जो कई आयतों के पिक्सेल की संख्या की गणना करता है। हर गणना अमान्य आयामों को अस्वीकार कर सकती है या पूर्णांक ओवरफ़्लो बता सकती है। कॉलर को या तो पूरा, क्रमबद्ध ऐरे चाहिए या कोई विशिष्ट गणना त्रुटि, ताकि वह हर समर्थित विफलता को स्पष्ट रूप से संभाल सके।
Swift ने Swift 6.0 में टाइप्ड throws पेश किया। throws(CalculationError) जैसी घोषणा फ़ंक्शन से फेंके जा सकने वाले मानों को उस त्रुटि प्रकार तक सीमित करती है। यह ऐसे छोटे और सीमित गणना अनुबंध के लिए उपयोगी है, जिसके कार्यान्वयन और कॉलर साथ-साथ विकसित होते हैं। देखें SE-0413: Typed throws।
Concurrency API की सीमा मायने रखती है। Apple के दस्तावेज़ में दिए गए withThrowingTaskGroup सिग्नेचर के अनुसार, उसके बॉडी को ThrowingTaskGroup<ChildTaskResult, any Error> मिलता है। समूह का सामान्यीकृत दिखने वाला प्रकार उस फ़ैक्टरी के ज़रिए कोई ठोस डोमेन-त्रुटि पैरामीटर नहीं देता। इसलिए केवल अपनी गणना में typed throws जोड़ने से इस पूरे समूह API में ठोस फेंका जाने वाला प्रकार स्थापित नहीं होता। देखें withThrowingTaskGroup।
एक समर्थित तरीका है withTaskGroup का इस्तेमाल करना और हर चाइल्ड से Result<Int, CalculationError> लौटवाना। टास्क सामान्य रूप से एक ऐसा मान पूरा करता है जो सफलता या विफलता दर्शाता है। उन मानों को इकट्ठा करने के बाद, बैच फ़ंक्शन get() कॉल करता है, जिसका दस्तावेज़ित सिग्नेचर throws(Failure) है। इससे रनटाइम कास्ट के बिना typed throwing वापस मिलता है। Apple के Result दस्तावेज़ और get() दस्तावेज़ देखें।
Concurrency कोड लिखने से पहले बैच अनुबंध तय करें
इस उदाहरण में, हर इनपुट में पिक्सेल में मापी गई पूर्णांक चौड़ाई और ऊँचाई होती है। गणना इनका गुणनफल है। सब कुछ सिंथेटिक है और मेमोरी में रहता है; प्रोग्राम न तो कोई छवि रेंडर करता है, न एप्लिकेशन का डेटा लोड या सेव करता है।
इस उदाहरण के लिए निम्न नीतियाँ डिज़ाइन संबंधी विकल्प हैं। इनसे लौटाए गए मान और चुनी गई त्रुटि, दोनों अनुमानित रहते हैं:
| स्थिति | बैच का व्यवहार |
|---|---|
| सभी आयतें मान्य हैं | पिक्सेल की संख्याएँ इनपुट क्रम में लौटाएँ |
| चौड़ाई या ऊँचाई शून्य या ऋणात्मक है | उस आयत को अमान्य बताएँ |
गुणनफल Int में समा नहीं सकता |
उस आयत के लिए ओवरफ़्लो बताएँ |
| कई आयतों में विफलता होती है | सबसे कम इनपुट इंडेक्स वाली विफलता बताएँ |
| बैच चेकपॉइंट पर कैंसलेशन देखा जाता है | .cancelled बताएँ |
| इनपुट ऐरे खाली है | खाली ऐरे लौटाएँ, जब तक कैंसलेशन न दिखे |
सबसे कम इनपुट इंडेक्स चुनने के लिए गणना की विफलता चुनने से पहले हर चाइल्ड का नतीजा इकट्ठा करना ज़रूरी है। Swift टास्क-ग्रुप शेड्यूलिंग क्रम की गारंटी नहीं देता, और पूरा होने का क्रम सबमिशन क्रम से अलग हो सकता है। हर नतीजे के साथ इंडेक्स जोड़ने से कलेक्टर इच्छित क्रम बहाल कर सकता है। देखें TaskGroup।
यह नीति छोटे प्रीव्यू बैच के लिए उपयुक्त है, जहाँ हर बार एक जैसा फ़ीडबैक मायने रखता है। यह जानबूझकर सभी गणनाओं का इंतज़ार करती है। निष्पादन के दौरान सबसे पहले देखी गई विफलता बताने वाला डिज़ाइन अलग समझौता करेगा और अलग-अलग रन में अलग आयत चुन सकता है।
अपने Mac पर पूरा स्थानीय उदाहरण बनाएँ
Swift 6 या नए टूलचेन का इस्तेमाल करें। Terminal में swift --version से जाँचें। Swift.org Getting Started में यह जाँच बताता है और Install Swift — macOS में Mac पर इंस्टॉल करने के निर्देश देता है।
इंस्टॉलेशन पेज पर दिए गए वर्कफ़्लो से एक छोटा executable पैकेज बनाएँ:
mkdir TypedCalculations
cd TypedCalculations
swift package init --name TypedCalculations --type executable
Sources के अंतर्गत बनाई गई Swift एंट्री फ़ाइल को नीचे दिए गए तीन कोड ब्लॉकों से इसी क्रम में बदलें और उन्हें एक ही फ़ाइल में रखें। केवल एक executable एंट्री पॉइंट रखें। यह उदाहरण पैकेज के सामान्य डिफ़ॉल्ट actor isolation का इस्तेमाल करता है; जिस प्रोजेक्ट में डिफ़ॉल्ट main-actor isolation कॉन्फ़िगर है, उसमें गणना के प्रकारों और फ़ंक्शनों को उस isolation के बाहर रखना होगा।
इनपुट और ठोस विफलताएँ तय करें
त्रुटि enum संरचित जानकारी सुरक्षित रखता है: केस समस्या बताता है और उससे जुड़ा इंडेक्स इनपुट की पहचान करता है। Foundation का LocalizedError प्रोटोकॉल प्रस्तुति के लिए एक वैकल्पिक errorDescription देता है। नीचे दिए गए अंग्रेज़ी संदेश उदाहरण हैं; इस प्रोटोकॉल के अनुरूप होने से उनका स्वतः अनुवाद नहीं होता। देखें LocalizedError।
import Foundation
struct Rectangle: Sendable {
let width: Int
let height: Int
}
enum CalculationError: Error, Sendable, LocalizedError {
case invalidDimensions(index: Int)
case overflow(index: Int)
case cancelled
var errorDescription: String? {
switch self {
case .invalidDimensions(let index):
return "Rectangle \(index + 1) needs positive dimensions."
case .overflow(let index):
return "Rectangle \(index + 1) has too many pixels for Int."
case .cancelled:
return "Calculation cancelled."
}
}
}
struct IndexedOutcome: Sendable {
let index: Int
let result: Result<Int, CalculationError>
}
गणना करें, नतीजे इकट्ठा करें और typed throwing बहाल करें
अंकगणित में multipliedReportingOverflow(by:) इस्तेमाल होता है। Apple के दस्तावेज़ के अनुसार, इसका partialValue पूरा गुणनफल तभी होता है जब overflow false हो; अन्यथा इसमें कटा हुआ गुणनफल होता है। नीचे दिया गया guard उस कटे हुए मान को सफल पिक्सेल संख्या बनने से रोकता है। देखें multipliedReportingOverflow(by:)।
func pixelCount(
for rectangle: Rectangle,
index: Int
) throws(CalculationError) -> Int {
if Task.isCancelled {
throw .cancelled
}
guard rectangle.width > 0, rectangle.height > 0 else {
throw .invalidDimensions(index: index)
}
let product = rectangle.width.multipliedReportingOverflow(
by: rectangle.height
)
guard !product.overflow else {
throw .overflow(index: index)
}
return product.partialValue
}
func calculateBatch(
_ rectangles: [Rectangle]
) async throws(CalculationError) -> [Int] {
if Task.isCancelled {
throw .cancelled
}
let outcomes: [IndexedOutcome] = await withTaskGroup(
of: IndexedOutcome.self,
returning: [IndexedOutcome].self
) { group in
for (index, rectangle) in rectangles.enumerated() {
group.addTask {
let result = Result<Int, CalculationError> {
() throws(CalculationError) -> Int in
try pixelCount(for: rectangle, index: index)
}
return IndexedOutcome(index: index, result: result)
}
}
var collected: [IndexedOutcome] = []
for await outcome in group {
collected.append(outcome)
}
return collected.sorted { $0.index < $1.index }
}
if Task.isCancelled {
throw .cancelled
}
var counts: [Int] = []
counts.reserveCapacity(outcomes.count)
for outcome in outcomes {
counts.append(try outcome.result.get())
}
return counts
}
Result closure पर स्पष्ट throws(CalculationError) annotation उसके त्रुटि अनुबंध को दिखाता है, बजाय closure inference पर निर्भर रहने के। SE-0413 बताता है कि typed throws inference में पहले नियोजित पूर्ण बदलाव Swift 6.0 में लागू नहीं किए गए थे। Typed throws में कार्यान्वयन संबंधी टिप्पणी देखें।
केवल parent collector ही collected में मान जोड़ता है। हर चाइल्ड को एक आयत और इंडेक्स मिलता है और वह अपना Sendable नतीजा लौटाता है। यह task-group की उस शर्त के अनुरूप है कि child result मान Sendable होने चाहिए; nonthrowing group body उन्हें for await से इकट्ठा करती है। देखें withTaskGroup।
Executable एंट्री पॉइंट जोड़ें
यह अंतिम ब्लॉक सभी उदाहरण इनपुट देता है और बैच फ़ंक्शन को कॉल करता है। catch के अंदर स्पष्ट असाइनमेंट संकलन के समय इच्छित API अनुबंध भी जाँचता है: पकड़ा गया मान CalculationError होना चाहिए।
@main
struct TypedCalculations {
static func main() async {
let examples: [(name: String, rectangles: [Rectangle])] = [
(
name: "Valid",
rectangles: [
Rectangle(width: 640, height: 480),
Rectangle(width: 320, height: 200)
]
),
(
name: "Multiple failures",
rectangles: [
Rectangle(width: 0, height: 20),
Rectangle(width: Int.max, height: 2)
]
),
(
name: "Overflow",
rectangles: [
Rectangle(width: Int.max, height: 2)
]
),
(name: "Empty", rectangles: [])
]
for example in examples {
do {
let counts = try await calculateBatch(example.rectangles)
print("\(example.name): \(counts)")
} catch {
let failure: CalculationError = error
print("\(example.name): \(failure.errorDescription ?? "Calculation failed.")")
}
}
}
}
पैकेज डायरेक्टरी से executable चलाएँ:
swift run TypedCalculations
बिल्ड संदेशों को छोड़कर, अपेक्षित एप्लिकेशन आउटपुट नीचे दिया गया है। शोध के परिवेश में Swift कंपाइलर उपलब्ध नहीं था, इसलिए इस प्रोग्राम को प्राप्त API दस्तावेज़ों के आधार पर जाँचा गया, लेकिन वहाँ इसे संकलित या चलाया नहीं गया।
Valid: [307200, 64000]
Multiple failures: Rectangle 1 needs positive dimensions.
Overflow: Rectangle 1 has too many pixels for Int.
Empty: []
मान्य संख्याएँ उदाहरण के इनपुट से सीधे निकलती हैं: 640 गुणा 480, 307,200 होता है, और 320 गुणा 200, 64,000 होता है। कई विफलताओं वाले उदाहरण में पहला आयत चुना जाता है, क्योंकि नतीजों की जाँच इनपुट क्रम में होती है, भले ही दूसरा चाइल्ड पहले पूरा हो जाए।
कैंसलेशन और आंशिक नतीजे कैसे काम करते हैं?
Swift में कैंसलेशन सहकारी होता है। किसी टास्क को कैंसलेशन देखना और उस पर प्रतिक्रिया देनी होती है; कैंसलेशन का अनुरोध करने से उसकी गणना अपने आप बीच में नहीं रुकती। Structured task groups लौटने से पहले अपने चाइल्ड के पूरा होने का इंतज़ार भी करते हैं। Swift भाषा गाइड का Concurrency अध्याय और Apple के withTaskGroup दस्तावेज़ देखें।
यहाँ Task.isCancelled को सबमिशन से पहले, हर गणना के भीतर और संग्रह के बाद जाँचा जाता है। जब किसी चेकपॉइंट पर कैंसलेशन दिखता है, तो कोड इसे मौजूदा .cancelled केस से दर्शाता है। Boolean पढ़ने से इस सीमित अनुबंध में एक और फेंके जाने वाला त्रुटि प्रकार नहीं जुड़ता। Apple इस प्रॉपर्टी को Task.isCancelled में दस्तावेज़ित करता है।
संग्रह के बाद की जाँच देखे गए बैच कैंसलेशन को आयतों की विफलताओं से प्राथमिकता देती है। ये जाँच चेकपॉइंट हैं, कोई परमाण्विक गारंटी नहीं: अंतिम जाँच के बाद अनुरोध किया गया कैंसलेशन लौटाए गए नतीजे को प्रभावित नहीं कर सकता। बार-बार काम करने वाली लंबी गणना में उसके भीतर जाँचें जोड़ें, ताकि कैंसलेशन जल्द देखा जा सके।
इस कार्यान्वयन में कोई विफल चाइल्ड कैंसलेशन शुरू नहीं करता। वह सामान्य Result.failure मान लौटाता है और कलेक्टर आगे चलता रहता है। अगर हर चाइल्ड सफलतापूर्वक पूरा हो, तो कॉलर को हर संख्या मिलती है। अगर अनरैप करते समय कोई विफलता मिलती है, तो फ़ंक्शन त्रुटि फेंकता है और उसका स्थानीय, आंशिक रूप से बनाया गया counts ऐरे कभी लौटाया नहीं जाता।
अगर डेस्कटॉप इंटरफ़ेस को हर आयत के लिए सफल या विफल स्थिति चाहिए, तो सार्वजनिक बैच अनुबंध बदलकर क्रमबद्ध नतीजे लौटाएँ। हर विफलता को उसके इंडेक्स से जुड़ा रखें। यह यहाँ लागू किए गए पूरे या त्रुटि वाले ऐरे से पाठक को मिलने वाला अलग व्यवहार है।
विफलताओं को छिपाए बिना खोए हुए त्रुटि प्रकार का पता लगाएँ
अगर अनुकूलित कोड संकलित नहीं होता, तो हर सीमा पर प्रकार को इस क्रम में जाँचें:
- जाँचें कि गणना
throws(CalculationError)घोषित करती है। - जाँचें कि child मान
Result<Int, CalculationError>है, न कि ऐसा Result जिसकी विफलताany Errorहो। - अगर inference उसका त्रुटि प्रकार व्यापक कर दे, तो closure का स्पष्ट typed-throws annotation रखें।
- इन Result मानों को आगे ले जाने के लिए nonthrowing
withTaskGroupइस्तेमाल करें। - टाइप किए हुए बैच फ़ंक्शन के भीतर
get()से अनरैप करें। - जाँचें कि जोड़ा गया कोई throwing operation अलग त्रुटि प्रकार तो नहीं लाता।
जबरन किया गया cast इस अनुबंध को साबित नहीं कर सकता। इसी तरह, विफल नतीजों को छोड़ देना या उनकी जगह शून्य रख देना गणना का अर्थ बदल देगा। विफलताओं को मान के रूप में तब तक सुरक्षित रखें, जब तक चुनी गई बैच नीति यह तय न करे कि उन्हें कैसे बताना है।
इन छोटे गुणनों के लिए concurrency एक आर्किटेक्चरल प्रदर्शन है; इससे प्रदर्शन में सुधार का दावा नहीं किया जाता। उपयोगी नतीजा एक सटीक अनुबंध है: सफल संख्याएँ इनपुट क्रम में रहती हैं, कई विफलताओं के लिए चयन का नियत नियम है और कॉलर को सीधे संभालने योग्य ठोस त्रुटि enum मिलता है।
स्रोत
Typed throws — SE-0413 — Swift Project.
withThrowingTaskGroup(of:returning:isolation:body:) — Apple.
Result — Apple.
get() — Apple.
TaskGroup — Apple.
Getting Started — Swift.org.
Install Swift — macOS — Swift.org.
LocalizedError — Apple.
multipliedReportingOverflow(by:) — Apple.
Concurrency — The Swift Programming Language — Swift Project.
isCancelled — Apple.