पहले निलंबन से पहले गणना का टास्क किसी ऐक्टर के भीतर रखें, फिर समवर्ती कॉलकर्ताओं को उसी टास्क का इंतज़ार करने दें। इस चरण-दर-चरण उदाहरण में Mac उपयोगिता के लिए वर्गों के योग का एक स्थानीय उदाहरण बनाया गया है, जिसमें रद्दीकरण, त्रुटियों और सफ़ाई के स्पष्ट नियम हैं।
समवर्ती कॉलकर्ताओं को क्या साझा करना चाहिए?
मान लें कि Mac डेस्कटॉप उपयोगिता के दो हिस्सों को एक ही गणना किया हुआ सारांश चाहिए: एक पूर्वावलोकन और एक विवरण पैनल। दोनों [1, 2, 3, 4] सिंथेटिक ऐरे में मौजूद संख्याओं के वर्गों का योग माँगते हैं। दोनों को 30 मिलना चाहिए, और एक ही सक्रिय रन से जुड़ने वाले अनुरोधों को एक निष्पादन साझा करना चाहिए।
नीचे दिए गए कार्यान्वयन में प्रत्येक कैलकुलेटर इंस्टेंस को एक अपरिवर्तनीय इनपुट ऐरे दिया गया है। यह मौजूदा टास्क को तब तक साझा करता है, जब तक वह ऐक्टर के फ़्लाइट स्लॉट में रहता है; फिर कोई कॉलर उसके पूरा होने को देखे तो स्लॉट साफ़ कर देता है। इसके बाद का अनुरोध एक और गणना शुरू कर सकता है। यह प्रगति में चल रहे काम को साझा करना है; पूरे हुए उत्तरों को अनिश्चित काल तक बनाए रखने के लिए अलग कैशिंग नीति की ज़रूरत होगी।
मानों का अनुबंध जानबूझकर छोटा रखा गया है:
- अधिकतम 100,000 पूर्णांक स्वीकारें, जिनमें हर संख्या
0से1,000के बीच हो। - उनके वर्गों का योग लौटाएँ; खाली ऐरे
0लौटाता है। - एक रन पहचानकर्ता जोड़ें, ताकि कॉलकर्ता साझा निष्पादन और केवल समान योगों में अंतर कर सकें।
- उस रन का इंतज़ार कर रहे कॉलकर्ताओं तक गणना की त्रुटियाँ पहुँचाएँ।
- किसी कॉलकर्ता को साझा वर्कर रद्द किए बिना अपनी प्रतीक्षा रद्द करने दें।
अनुमत अधिकतम योग 100,000 × 1,000² = 100,000,000,000 है, जो Mac के 64-बिट Int में समा जाता है। ये सीमाएँ इस उदाहरण के अनुबंध का हिस्सा हैं। इन्हें बदलने पर अंकगणितीय ओवरफ़्लो और संसाधनों के उपयोग पर फिर से विचार करना होगा।
ऐक्टर को संग्रहीत टास्क की फिर भी ज़रूरत क्यों है?
ऐक्टर अपनी परिवर्तनशील स्थिति की रक्षा करता है, लेकिन ऐसिंक्रोनस ऐक्टर मेथड निलंबित हो सकता है। उस निलंबन के दौरान कोई दूसरी कॉल ऐक्टर में प्रवेश कर सकती है। Swift की भाषा मार्गदर्शिका ऐक्टर आइसोलेशन और निलंबन बिंदुओं को समझाती है, जबकि Apple का ऐक्टर सत्र दिखाता है कि await से पहले की धारणाएँ कैसे पुरानी पड़ सकती हैं। Concurrency — The Swift Programming Language, Protect mutable state with Swift actors।
इसलिए, मौजूदा उत्तर की जाँच करके फिर नई गणना का इंतज़ार करना दोहराए गए काम को नहीं रोकता। उत्तर उपलब्ध होने से पहले कोई दूसरा कॉलकर्ता आ सकता है। इसके बजाय, टास्क हैंडल को बिना निलंबित हुए प्रकाशित करें: फ़्लाइट स्लॉट जाँचें, स्लॉट खाली होने पर ही वर्कर बनाएँ, उसे संग्रहीत करें, और फिर उसके परिणाम का इंतज़ार करें।
Apple दस्तावेज़ करता है कि टास्क बनने के तुरंत बाद शुरू हो सकता है। उसके ऐसिंक्रोनस value तक पहुँचने पर पूरा होने का इंतज़ार होता है और थ्रो की गई त्रुटि आगे पहुँचती है। इसलिए ऐक्टर एक हैंडल का स्वामित्व समन्वित करता है; कॉलकर्ता उसी हैंडल के ज़रिए अपने परिणाम पाते हैं। Task, value।
पूरा स्थानीय उदाहरण बनाएँ
macOS 13 या उसके बाद के संस्करण पर Swift 6 कंपाइलर का उपयोग करें। यहाँ macOS की आवश्यकता उदाहरण में Task.sleep(for:) कॉल की वजह से है। यह कॉल अपने अंतर्निहित थ्रेड को ब्लॉक किए बिना निलंबित होती है; छोटी-सी रुकावट से ओवरलैप होते अनुरोधों को देखना आसान होता है। यह अंकगणित का कोई हिस्सा नहीं करती और बाद में हटाई जा सकती है। sleep(for:tolerance:clock:)।
निम्नलिखित पूरा प्रोग्राम SharedCalculation.swift नाम से सहेजें। यह नीचे दिखाए गए कमांड-लाइन कंपाइलेशन के लिए सामान्य, गैर-आइसोलेटेड डिफ़ॉल्ट सेटिंग का उपयोग करता है। Foundation UUID उपलब्ध कराता है, जिसके दस्तावेज़ित अनुरूपताओं में Sendable और Equatable शामिल हैं; यह पहचानकर्ता हर फ़्लाइट को लेबल करता है और शर्त-आधारित सफ़ाई में मदद करता है। UUID।
import Foundation
struct CalculationResult: Sendable {
let total: Int
let runID: UUID
}
enum CalculationError: Error {
case invalidInput
}
actor SharedCalculation {
private struct Flight {
let id: UUID
let task: Task<CalculationResult, Error>
}
private let samples: [Int]
private var flight: Flight?
init(samples: [Int]) {
self.samples = samples
}
func value() async throws -> CalculationResult {
try Task.checkCancellation()
let active: Flight
if let existing = flight {
active = existing
} else {
let id = UUID()
let snapshot = samples
let task = Task.detached(priority: .userInitiated) {
[snapshot, id] () async throws -> CalculationResult in
guard snapshot.count <= 100_000,
snapshot.allSatisfy({ (0...1_000).contains($0) })
else {
throw CalculationError.invalidInput
}
// Demonstration only: allow overlap to be observed.
try await Task.sleep(for: .milliseconds(100))
let total = snapshot.reduce(0) { sum, sample in
sum + sample * sample
}
return CalculationResult(total: total, runID: id)
}
active = Flight(id: id, task: task)
flight = active
}
defer {
// An older waiter must not clear a newer flight.
if flight?.id == active.id {
flight = nil
}
}
let result = try await active.task.value
try Task.checkCancellation()
return result
}
}
@main
struct Demo {
static func main() async throws {
let calculation = SharedCalculation(samples: [1, 2, 3, 4])
async let preview = calculation.value()
async let details = calculation.value()
async let inspector = calculation.value()
let (a, b, c) = try await (preview, details, inspector)
precondition(a.total == 30 && b.total == 30 && c.total == 30)
let observedRuns = Set([a.runID, b.runID, c.runID]).count
print("Total: \(a.total)")
print("Runs observed across three requests: \(observedRuns)")
let later = try await calculation.value()
precondition(later.total == 30)
print("Later request started another run: \(later.runID != c.runID)")
}
}
फ़ाइल वाली डायरेक्टरी से इसे कंपाइल करके चलाएँ:
swiftc -swift-version 6 -parse-as-library SharedCalculation.swift -o SharedCalculation
./SharedCalculation
अंकगणित के दावे पास होने चाहिए। जब तीनों शुरुआती कॉल फ़्लाइट साफ़ होने से पहले प्रवेश करती हैं, तो देखी गई रन संख्या 1 होती है। बाद का अनुरोध उन कॉलों के लौटने के बाद आता है, इसलिए वह एक और गणना शुरू करता है। रुकावट से ओवरलैप की संभावना बढ़ती है, लेकिन शेड्यूलिंग की गारंटी नहीं मिलती: शुरुआती अनुरोध बहुत देर से शेड्यूल हो तो वैध रूप से बाद की फ़्लाइट शुरू कर सकता है। शोध परिवेश में Swift उपलब्ध नहीं था, इसलिए यहाँ इस प्रोग्राम का निरीक्षण किया गया, लेकिन इसे कंपाइल या निष्पादित नहीं किया गया।
वर्कर डिटैच्ड टास्क का उपयोग करता है, क्योंकि यह गणना साझा कैलकुलेटर की है, किसी एक अनुरोधकर्ता कॉलकर्ता की नहीं, और इसका अंकगणित समन्वय करने वाले ऐक्टर से स्वतंत्र रूप से चलना चाहिए। यह केवल इनपुट स्नैपशॉट और पहचानकर्ता को कैप्चर करता है। Apple संरचित चाइल्ड टास्क की सलाह देता है, जब वे आवश्यक जीवनकाल व्यक्त कर सकें; डिटैच्ड टास्क के लिए जीवनकाल और रद्दीकरण के बारे में स्पष्ट निर्णय ज़रूरी हैं। इस उदाहरण में प्राथमिकता भी स्पष्ट रूप से दी गई है। detached(name:priority:operation:)।
सफ़ाई अगली गणना को मिटाने से कैसे बचती है?
जाँच और संग्रह करने के क्रम में कोई await नहीं है, इसलिए स्लॉट खाली जाँचने और नई फ़्लाइट प्रकाशित करने के बीच दूसरी ऐक्टर कॉल नहीं आ सकती। जब पहला कॉलकर्ता active.task.value पर निलंबित होता है, तो दूसरा कॉलकर्ता प्रवेश कर सकता है, उस फ़्लाइट को पा सकता है और उसी हैंडल का इंतज़ार कर सकता है।
सफ़ाई के लिए अपनी पहचान जाँच भी ज़रूरी है, क्योंकि कई कॉलकर्ता एक ही पूरे हुए टास्क से अलग-अलग समय पर फिर शुरू हो सकते हैं। इस क्रम पर विचार करें:
| घटना | फ़्लाइट स्लॉट |
|---|---|
| कॉलकर्ता A और B रन X से जुड़ते हैं | X |
| X पूरा होता है; A फिर शुरू होकर X साफ़ करता है | खाली |
| कॉलकर्ता C प्रवेश करके रन Y शुरू करता है | Y |
| B, X से फिर शुरू होकर सफ़ाई करता है | Y बना रहना चाहिए |
B द्वारा nil को बिना शर्त असाइन करने से Y का हैंडल मिट जाएगा। तब चौथा कॉलकर्ता Y के चलते रहने के दौरान दोहराया गया काम शुरू कर सकता है। flight?.id की तुलना active.id से करने पर सफ़ाई केवल उसी रन तक सीमित रहती है, जिससे प्रत्येक कॉलकर्ता वास्तव में जुड़ा था। यह क्रम कार्यान्वयन और दस्तावेज़ित रीएंट्रेंसी व्यवहार का परिणाम है। Protect mutable state with Swift actors।
साझाकरण की सटीक सीमा संग्रहीत फ़्लाइट स्लॉट है। वर्कर पूरा हो चुका हो सकता है, जबकि उसके प्रतीक्षा कर रहे कॉलकर्ता स्लॉट साफ़ करने के लिए अभी फिर से शुरू न हुए हों। इस अंतराल में प्रवेश करने वाला अनुरोध अब भी उस पूरे हुए टास्क का परिणाम पा सकता है। स्लॉट साफ़ होने के बाद नया अनुरोध नया रन शुरू करता है। पूरा होने का यह छोटा अंतराल अनुबंध का हिस्सा है।
कॉलकर्ता रद्द हो या गणना विफल हो तो क्या होता है?
Swift में रद्दीकरण सहकारी होता है: कोड को तय करना पड़ता है कि कहाँ इसकी जाँच करनी है और इसके बाद क्या करना है। यह उदाहरण साझा परिणाम सफलतापूर्वक मिलने के बाद भी अनुरोधकर्ता टास्क की जाँच करता है। Task — Task Cancellation।
पहली जाँच पर पहले से रद्द कॉलकर्ता काम शुरू या जॉइन किए बिना त्रुटि फेंकता है। प्रतीक्षा के दौरान रद्द हुआ कॉलकर्ता साझा वर्कर का इंतज़ार जारी रखता है; वर्कर का परिणाम सफलतापूर्वक मिलने के बाद उसकी दूसरी जाँच CancellationError फेंकती है। दूसरे कॉलकर्ता परिणाम पा सकते हैं, क्योंकि यह कार्यान्वयन किसी एक कॉलकर्ता का रद्दीकरण वर्कर तक कभी नहीं पहुँचाता।
यह नीति प्रतीक्षा को तुरंत रद्द नहीं करती। अगर सभी कॉलकर्ता रद्द हो जाएँ, तब भी वर्कर पूरा होता है। इस सीमित स्थानीय गणना के लिए यह जानबूझकर चुना गया व्यवहार है। जिस डिज़ाइन को किसी एक कॉलकर्ता को तुरंत अलग होने देना हो, उसे प्रतीक्षा कर रहे कॉलकर्ताओं का अतिरिक्त हिसाब रखना होगा और यह स्पष्ट नियम तय करना होगा कि साझा वर्कर कब रुके।
अमान्य इनपुट से वर्कर CalculationError.invalidInput फेंकता है। पहले से उस टास्क को साझा कर रहे कॉलकर्ताओं तक value के ज़रिए उसकी त्रुटि पहुँचती है, और उनकी डिफ़र्ड सफ़ाई मेल खाने वाले स्लॉट को खाली कर देती है। बाद का अनुरोध उसी अपरिवर्तनीय इनपुट पर फिर कोशिश करता है और दोबारा विफल होता है। कोई योग चुपचाप विकल्प के रूप में नहीं दिया जाता और अमान्य तत्व हटाए नहीं जाते। value।
त्रुटियों के क्रम का एक स्पष्ट विवरण भी है: यदि वर्कर विफल होते समय कोई कॉलकर्ता रद्द हो जाए, तो दूसरी रद्दीकरण जाँच से पहले वर्कर की त्रुटि आगे पहुँचती है। यदि वर्कर सफल होता है, तो कॉलकर्ता की रद्दीकरण जाँच तय करती है कि वह कॉलकर्ता परिणाम लौटाएगा या नहीं।
Mac उपयोगिता में इस पैटर्न का उपयोग और सत्यापन कैसे करें?
समान योग अपने आप साझा निष्पादन साबित नहीं करते: इस इनपुट की तीन अलग-अलग गणनाएँ भी 30 देती हैं। ओवरलैप जाँचते समय रन पहचानकर्ताओं की तुलना करें और संख्यात्मक परिणाम अलग से जाँचें।
निर्धारक समवर्ती परीक्षण के लिए उदाहरण की रुकावट को नियंत्रित ऐसिंक्रोनस गेट से बदलें और फ़्लाइट में प्रवेश को इंस्ट्रूमेंट करें। वर्कर को तब तक रोकें, जब तक सभी अपेक्षित कॉलकर्ता जुड़ न जाएँ, फिर उसे आगे बढ़ने दें। एक वर्कर की शुरुआत, मेल खाते पहचानकर्ता, सही योग, त्रुटि का आगे पहुँचना, और सफ़ाई के बाद नया रन शुरू होना जाँचें। तालिका में दिया गया सफ़ाई क्रम भी आज़माएँ। स्लीप की अवधि बढ़ाने से इन स्थितियों की विश्वसनीय गारंटी नहीं मिलती।
डेस्कटॉप ऐप में, समान इनपुट इस्तेमाल करने वाले उपभोक्ताओं के लिए एक ही कैलकुलेटर इंस्टेंस रखें। हर पैनल के लिए नया इंस्टेंस बनाने से प्रत्येक पैनल को अलग फ़्लाइट स्लॉट मिलता है। सिंथेटिक इनपुट बदलने पर उस स्नैपशॉट के लिए नया कैलकुलेटर बनाएँ; यह उदाहरण परिवर्तनशील इनपुट या कुंजी-आधारित साझाकरण परिभाषित नहीं करता।
लौटाए गए मान UI स्थिति पर मेन ऐक्टर में लागू करें। Swift की भाषा मार्गदर्शिका UI के काम के लिए @MainActor दस्तावेज़ित करती है; साझा कैलकुलेटर अपनी स्थिति का समन्वय अलग से जारी रख सकता है। Concurrency — The Swift Programming Language।
छोटे योग के लिए टास्क बनाना अनावश्यक ओवरहेड है; यह नमूना स्वामित्व के नियम स्पष्ट करता है, ताकि उन्हें अधिक महत्वपूर्ण स्थानीय गणना पर लागू किया जा सके। इसे अपनाते समय आवश्यक क्रम बनाए रखें: निलंबन से पहले एक टास्क प्रकाशित करें, उसी हैंडल का इंतज़ार करें, और केवल पूरी हुई फ़्लाइट को साफ़ करें।
स्रोत
- Concurrency — The Swift Programming Language — Swift.org।
- Task — Apple Developer Documentation।
- value — Apple Developer Documentation।
- detached(name:priority:operation:) — Apple Developer Documentation।
- sleep(for:tolerance:clock:) — Apple Developer Documentation।
- UUID — Apple Developer Documentation।
- Protect mutable state with Swift actors — Apple Developer।