Swift गणना को रद्द करने योग्य बनाने के लिए, उसके task handle को अपने पास रखें, अनुरोध किए जाने पर cancel() कॉल करें, और लूप के भीतर रद्दीकरण की जाँच करें। macOS का यह उदाहरण इंटरफ़ेस को प्रतिक्रियाशील भी रखता है और रद्द किए गए रन को देर से परिणाम दिखाने से रोकता है।
Swift task को रद्द करने से वास्तव में क्या होता है?
Swift में सहकारी रद्दीकरण होता है: रद्दीकरण का अनुरोध task को चिह्नित करता है, जबकि चलता हुआ कोड तय करता है कि कब रुकना है। ऐसा CPU लूप जो कभी रद्दीकरण की जाँच नहीं करता, पूरा होने तक चलता रह सकता है। रद्दीकरण का पता चलने पर Task.checkCancellation() कॉल करने से CancellationError थ्रो होता है। Apple का रद्दीकरण दस्तावेज़, checkCancellation()।
यहाँ पाठक के लिए काम स्पष्ट है: एक छोटे Mac ऐप में काम करने वाले Start और Cancel बटन जोड़ें, जो एक कृत्रिम योग की गणना करता है। हर इनपुट स्थानीय रूप से बनाया जाता है। यह गणना लगातार आने वाली धनात्मक पूर्ण संख्याओं के वर्गमूल जोड़ती है और सभी अनुरोधित पदों की प्रक्रिया पूरी होने पर ही एक Double देती है।
रद्दीकरण लागू करने से पहले तय करें कि परिणाम का अनुबंध क्या होगा। यह उदाहरण अधूरे योग को छोड़ देता है और रद्दीकरण की अलग सूचना देता है। आंशिक योग लौटाने के लिए अलग अनुबंध चाहिए, जो बताए कि कितने पद शामिल किए गए; अन्यथा कॉलर अधूरे काम को माँगा गया उत्तर समझ सकता है। Swift की भाषा मार्गदर्शिका रद्दीकरण के संभावित प्रतिसादों में त्रुटि थ्रो करना, खाली परिणाम लौटाना और आंशिक काम लौटाना दर्ज करती है। Concurrency।
लागू करने की दो ज़िम्मेदारियाँ हैं: अनावश्यक गणना रोकना और तय करना कि पूरा हुआ मान अब भी इंटरफ़ेस में दिखना चाहिए या नहीं। यह उदाहरण दोनों संभालता है, जिसमें वह स्थिति भी शामिल है जब worker की अंतिम जाँच के बाद, लेकिन उसका परिणाम दिखाए जाने से पहले, रद्दीकरण का अनुरोध किया जाता है।
गणना के भीतर रद्दीकरण की जाँच रखें
Xcode में SwiftUI और Swift का उपयोग करके macOS ऐप प्रोजेक्ट बनाएँ। इस उदाहरण के लिए Swift 6 भाषा मोड और macOS 13 या उसके बाद का deployment target इस्तेमाल करें। SyntheticCalculation.swift नाम से नीचे दी गई पूरी फ़ाइल जोड़ें। इसकी 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
}
}
यह फ़ंक्शन शुरू होने से पहले, हर chunk की सीमा पर और लौटने से पहले रद्दीकरण की जाँच करता है। ये सोच-समझकर चुने गए रुकने के बिंदु हैं: chunk के दौरान आया रद्दीकरण अनुरोध अगली जाँच पर पकड़ा जाएगा। Task.checkCancellation() सिंक्रोनस है, इसलिए गणना सामान्य throwing फ़ंक्शन बनी रह सकती है; केवल रद्दीकरण जाँचने के लिए इसे await होने की ज़रूरत नहीं है। checkCancellation()।
Chunk की गणना इनपुट की स्पष्ट सीमाएँ भी बनाए रखती है। रद्द न किए गए task पर शून्य पद देने से शून्य मिलता है। ऋणात्मक पदों और शून्य या उससे कम chunk आकार को precondition के ज़रिए प्रोग्रामर की त्रुटियाँ माना जाता है। शेष संख्या से हर chunk की गणना करने पर उसका अंतिम बिंदु terms से आगे नहीं बढ़ता।
Accumulator worker के भीतर स्थानीय रहता है। रद्दीकरण की त्रुटि थ्रो होने पर फ़ंक्शन उस accumulator को लौटाए बिना समाप्त हो जाता है, जिससे चुना हुआ पूर्ण-परिणाम अनुबंध लागू होता है। चूँकि यह floating-point जोड़ है, पूरा हुआ मान गणितीय योग का सन्निकटन है।
Cancel बटन को प्रतिक्रियाशील रखें
रद्दीकरण की जाँचें उस बटन की मदद नहीं कर सकतीं जिसके action को चलने का मौका ही न मिले। Apple बताता है कि SwiftUI view से बनाया गया सामान्य Task अगर भारी सिंक्रोनस काम करता है, तो वह अब भी main actor को व्यस्त रख सकता है, क्योंकि वह task आसपास के actor context को विरासत में लेता है। Apple ने उस विरासत में मिले context के बाहर सिंक्रोनस काम चलाने के समर्थित तरीके के रूप में detached task दर्ज किया है। ऐप की प्रतिक्रियाशीलता बेहतर बनाना।
यहाँ detached worker केवल एक immutable integer कैप्चर करता है और स्पष्ट रूप से nonisolated घोषित सिंक्रोनस फ़ंक्शन कॉल करता है। इंटरफ़ेस की सारी state main actor पर रहती है। एक अलग main-actor task worker के मान का इंतज़ार करता है और डिस्प्ले अपडेट करता है।
बनाई गई app entry फ़ाइल को नीचे दी गई पूरी फ़ाइल से बदलें, और अगर बनाई गई ContentView.swift फ़ाइल मौजूद हो तो उसे हटाएँ। SwiftUI के app lifecycle की आवश्यकता के अनुसार target में ठीक एक @main घोषणा रखें। ये दोनों फ़ाइलें मिलकर उदाहरण ऐप बनाती हैं। 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 को अपने पास रखना आवश्यक है। किसी task handle को छोड़ देने से task रद्द नहीं होता; इससे उस handle के ज़रिए उसे रद्द करने की आपकी क्षमता चली जाती है। throwing worker के value का इंतज़ार करने से उसकी त्रुटि भी पकड़ी जाती है। Detached task के लिए lifecycle का स्पष्ट प्रबंधन ज़रूरी है, इसलिए बटन और view के गायब होने पर चलने वाला handler सीधे worker को रद्द करते हैं। केवल उसके परिणाम का इंतज़ार कर रहे task को रद्द करने से इस detached worker तक रद्दीकरण पहुँचने की पुष्टि नहीं होती। Detached task का दस्तावेज़।
Foundation का UUID हर रन के लिए एक identifier देता है। identifier की तुलना ऐप-स्तर का नियम लागू करती है: view के गायब होने पर किसी रन को अमान्य कर देने के बाद, उसका पूरा हुआ परिणाम इस view की मौजूदा स्थिति अपडेट नहीं कर सकता। identifier गणना नहीं रोकता; यह काम worker.cancel() और लूप की जाँचें करती हैं। UUID।
Worker के पूरा होने या त्रुटि फेंकने तक Start बटन अक्षम रहता है। Cancel दबाने पर पहले Cancellation requested दिखता है; परिणाम संभालने वाला completion observer इसे Cancelled में बदलता है। इससे रद्दीकरण के अनुरोध और worker के समाप्त होने की पुष्टि के बीच अंतर बना रहता है।
देर से आए परिणाम को कैसे संभालना चाहिए?
उस क्रम पर विचार करें जिसमें worker अपनी अंतिम जाँच पार कर लेता है, पूरा हो जाता है और फिर main actor द्वारा उसका मान स्वीकार किए जाने से पहले उपयोगकर्ता Cancel दबाता है। Apple स्पष्ट रूप से बताता है कि आख़िरी संभव जाँच के बाद रद्दीकरण का worker पर कोई दिखाई देने वाला असर नहीं हो सकता। अंतिम जाँच उस अंतराल को छोटा करती है, लेकिन समाप्त नहीं कर सकती। cancel()।
इसलिए उदाहरण सफल मान का इंतज़ार करने के बाद run.isCancelled की जाँच करता है। यह worker के handle की जाँच है। वहाँ static Task.isCancelled इस्तेमाल करने पर observer task की जाँच होगी। Swift की भाषा मार्गदर्शिका बताती है कि type property से मौजूदा task की और instance property से किसी दूसरे task की जाँच की जाती है। Concurrency।
इससे परिणाम स्वीकार करने का ठोस नियम तय होता है: परिणाम स्वीकार होने से पहले संसाधित हुआ रद्दीकरण प्रभावी होगा। स्वीकार करने की प्रक्रिया पूरी होकर worker को साफ़ कर देने के बाद, अक्षम Cancel बटन के पास वापस लेने के लिए कोई पूरा परिणाम नहीं रहता। यह नियम main-actor actions के क्रम पर आधारित है और यह वादा नहीं करता कि रद्दीकरण पूरा हो चुका काम पलट देगा।
रद्दीकरण जाँचों के बीच उपयोगी अंतराल चुनें
डिफ़ॉल्ट chunk आकार, 4,096 पद, शुरुआत के लिए एक उदाहरण है। छोटे chunks रद्दीकरण का पता लगाने के अधिक अवसर देते हैं और जाँचें ज़्यादा बार चलाते हैं। बड़े chunks जाँच की आवृत्ति घटाते हैं, लेकिन रुकने के बिंदुओं के बीच अधिक गणना होने देते हैं।
एक मोटे अनुमान के लिए मान लें कि एक chunk में 4,096 पद हैं और हर पद में उदाहरण के तौर पर दो माइक्रोसेकंड लगते हैं। वह chunk करीब 8.2 मिलीसेकंड की गणना दर्शाता है। ये गणित दिखाने के लिए गढ़े गए इनपुट हैं, Swift या किसी खास Mac के माप नहीं। Scheduling और इंटरफ़ेस अपडेट में अतिरिक्त समय लग सकता है।
| अवलोकन | क्या जाँचें या बदलें |
|---|---|
| गणना के दौरान इंटरफ़ेस प्रतिक्रिया देना बंद कर देता है | जाँचें कि लूप detached worker में चलता है और main-actor isolation से बाहर रहता है। |
| इंटरफ़ेस प्रतिक्रिया देता है, लेकिन रद्दीकरण में बहुत देर लगती है | checkEvery घटाएँ और सबसे लंबे chunk का समय मापें। |
| किसी chunk में बाद में एक महँगा ऑपरेशन जुड़ता है | अगर उसका implementation समर्थन करता है, तो उस ऑपरेशन के भीतर रुकने के बिंदु जोड़ें। |
| रद्द किए गए रन में परिणाम दिखता है | परिणाम स्वीकार करते समय worker handle जाँचें और run-identifier guard बनाए रखें। |
इस लूप के लिए एक और flag सेट करने भर के लिए cancellation handler न जोड़ें। Task पहले से ही रद्दीकरण की स्थिति उपलब्ध कराता है, और Swift की भाषा मार्गदर्शिका बताती है कि handler लगाने पर भी रुकना सहकारी ही रहता है। इस स्थानीय गणना के लिए स्पष्ट जाँचें ही आवश्यक रुकने का तरीका देती हैं। Concurrency।
पूरा होना, रद्दीकरण और बार-बार चलाना जाँचें
शोध के वातावरण में Swift compiler और macOS runtime उपलब्ध नहीं थे, इसलिए इस उदाहरण का निरीक्षण किया गया, लेकिन इसे compile या execute नहीं किया गया। Xcode में दोनों फ़ाइलें build करें, फिर इन अलग-अलग व्यवहारों की पुष्टि करें:
- छोटा पूरा परिणाम:
termsको अस्थायी रूप से4में बदलें। Floating-point rounding को ध्यान में रखते हुए प्रदर्शित परिणाम लगभग6.146264369941973होना चाहिए। - खाली इनपुट:
termsको0में बदलें। रद्द न किया गया रन शून्य दिखाना चाहिए। - काम के दौरान रद्दीकरण:
500_000_000वापस रखें,Startदबाएँ, फिर चलते समयCancelदबाएँ। पहलेCancellation requestedऔर उसके बाद बिना आंशिक परिणाम केCancelledदिखना चाहिए। - एक और रन: रद्दीकरण पूरा होने के बाद फिर
Startदबाएँ। नया रन सामान्य रूप से शुरू होना चाहिए। - View का गायब होना: गणना करती हुई window बंद करें। गायब होने वाला handler रद्दीकरण का अनुरोध करे और उस रन के इंटरफ़ेस अपडेट अमान्य करे।
बड़ी पद-संख्या का उद्देश्य इतना समय देना है कि आप बीच में क्रिया कर सकें, लेकिन इसकी अवधि Mac और build configuration पर निर्भर करती है। अगर रद्द करने से पहले ही यह पूरी हो जाए, तो समर्थित integer सीमा के भीतर संख्या बढ़ाएँ। बड़ी गणना पूरी होने का इंतज़ार करने के बजाय छोटे इनपुट से पूरी हुई गणना की पुष्टि करें।
तैयार डिज़ाइन में रुकने का स्पष्ट रास्ता और परिणाम स्वीकार करने का स्पष्ट नियम है: बटन अपने पास रखे worker को रद्द करता है, chunk की सीमाएँ गणना को त्रुटि फेंकने देती हैं, और main actor केवल मौजूदा रन का ऐसा परिणाम स्वीकार करता है जिसे रद्द नहीं किया गया हो। ये तीन निर्णय किसी सामान्य Mac गणना के लिए रद्दीकरण को उपयोगी बनाते हैं।
स्रोत
- Concurrency — Swift The Programming Language, Swift project।
- checkCancellation() — Apple Developer Documentation।
- cancel() — Apple Developer Documentation।
- ऐप की प्रतिक्रियाशीलता बेहतर बनाना — Apple Developer Documentation।
- detached(name:priority:operation:) — Apple Developer Documentation।
- App — Apple Developer Documentation।
- UUID — Apple Developer Documentation।