CPU-गहन Swift गणनाओं को समानांतर चलाने के लिए, worker को स्पष्ट @concurrent सीमा दें और स्वतंत्र काम को child tasks में बाँटें। Mac का यह उदाहरण interface updates को MainActor पर रखता है और साझा बदलाव किए बिना कृत्रिम परिणामों को जोड़ता है।
टास्क जोड़ने पर भी गणना क्रमिक क्यों रह सकती है
मान लें कि किसी Mac ऐप में Calculate बटन है, जो एक बड़ी स्थानीय गणना शुरू करता है। गणना को Task { ... } में लपेटने से एक asynchronous entry point मिलता है, लेकिन main-actor context में बनाया गया task उस isolation को विरासत में ले सकता है। उसके भीतर का लंबा synchronous loop अब भी main actor को व्यस्त रखता है। Apple इस अंतर को Embracing Swift concurrency में दिखाता है।
Task group जोड़ने पर भी एक अहम सवाल अनुत्तरित रहता है: महँगा loop रखने वाला function कहाँ चलता है? यदि हर child उसी @MainActor calculation method को call करता है, तो वह actor गणना को क्रमिक बनाता है। Actors एक समय में केवल एक task को अपने isolated state तक पहुँचने देते हैं, जैसा कि Swift भाषा मार्गदर्शिका के Concurrency अध्याय में बताया गया है।
Swift 6.2 और उसके बाद के संस्करणों में, asynchronous calculation entry point को @concurrent चिह्नित करें। यह स्पष्ट रूप से execution को caller के actor से बाहर ले जाता है। NonisolatedNonsendingByDefault feature सक्षम होने पर, सामान्य nonisolated asynchronous function इसके बजाय caller के actor पर चलता है; पहले का व्यवहार अलग था। लागू किए गए SE-0461 प्रस्ताव में feature की सीमा और स्पष्ट attribute, दोनों दर्ज हैं।
मनचाहा व्यवहार दो अलग चुनावों से मिलता है: worker का isolation MainActor से दूर execution की अनुमति देता है, और कई child tasks स्वतंत्र रूप से schedule किए जा सकने वाले काम देते हैं। Runtime तय करता है कि कौन-से उपलब्ध threads उन tasks को चलाएँगे। न task की संख्या, न ही @concurrent, एक साथ काम कर रहे cores की किसी खास संख्या का वादा करता है। Apple इस scheduling model का वर्णन Embracing Swift concurrency में करता है।
ऐसी गणना तय करें जिसे सही ढंग से बाँटा जा सके
नीचे दिया उदाहरण Mac उपयोगकर्ता को एक कृत्रिम गणना शुरू या रद्द करने और उसका अंतिम checksum दिखाने देता है। सभी inputs constants या बनाए गए integers हैं। गणना का यह सटीक value contract है:
0..<countमें हर integer index पर ठीक एक बार जाएँ।- हर index को
UInt64में बदलें और माँगी गई rounds संख्या तक बार-बारx = (x &* 1_664_525) &+ 1_013_904_223लागू करें। - प्राप्त मानों को wrapping addition से जोड़ें।
- खाली input के लिए zero लौटाएँ, negative counts या rounds अस्वीकार करें, और nonpositive worker limit अस्वीकार करें।
- रद्द किए जाने का पता चलने पर throw करें, ताकि अधूरे checksum को पूर्ण परिणाम के रूप में न दिखाया जाए।
यहाँ wrapping arithmetic जानबूझकर इस्तेमाल किया गया है। Swift का सामान्य integer arithmetic overflow पर trap करता है; इसके &+ और &* operators overflow व्यवहार को स्पष्ट रूप से संभालते हैं। Advanced Operators देखें।
इस contract के लिए, हर index स्वतंत्र है और wrapping addition आंशिक totals को 2 की घात 64 के modulo में जोड़ता है। इसलिए grouping या completion order बदलने पर भी अंतिम मान सटीक रूप से वही रहता है। यह निष्कर्ष चुने गए arithmetic से निकलता है। Floating-point गणना के लिए अपना correctness rule चाहिए, क्योंकि additions को फिर से समूहित करने से rounding बदल सकती है।
Partition में division और remainder का उपयोग होता है: हर worker को indices की बराबर मूल संख्या मिलती है, और शुरुआती कुछ workers को एक अतिरिक्त index मिलता है। उदाहरण के लिए, count 17 और तीन workers होने पर ranges 0..<6, 6..<12, और 12..<17 होंगी। इनमें remainder सहित पूरा input ठीक एक बार शामिल है।
Mac की पूरी गणना का उदाहरण बनाएँ
Swift 6.2 या बाद के toolchain और macOS 13 या बाद के deployment target वाले macOS SwiftUI app target का उपयोग करें। बने हुए app entry point और content view को नीचे के दो blocks से बदलें और उन्हें एक ही Swift file में साथ रखें। ये मिलकर एक पूरा program बनाते हैं; target में केवल एक @main app declaration रखें।
Calculation namespace स्पष्ट रूप से nonisolated है, इसलिए इसके members को inferred global-actor isolation नहीं मिलता। Swift इस type-level declaration को SE-0449 में दस्तावेज़ित करता है। दोनों asynchronous calculation methods अपनी execution boundary को स्पष्ट बताने के लिए @concurrent का उपयोग करते हैं।
import Foundation
import SwiftUI
nonisolated enum CalculationError: Error {
case invalidInput
}
nonisolated enum SyntheticCalculator {
@concurrent
static func run(
count: Int,
rounds: Int,
workerLimit: Int
) async throws -> UInt64 {
guard count >= 0, rounds >= 0, workerLimit > 0 else {
throw CalculationError.invalidInput
}
try Task.checkCancellation()
guard count > 0 else { return 0 }
let workers = min(count, workerLimit)
let baseSize = count / workers
let remainder = count % workers
return try await withThrowingTaskGroup(
of: UInt64.self,
returning: UInt64.self
) { group in
var lower = 0
for worker in 0..<workers {
try Task.checkCancellation()
let size = baseSize + (worker < remainder ? 1 : 0)
let start = lower
let end = start + size
lower = end
group.addTask {
try await calculateChunk(
lower: start,
upper: end,
rounds: rounds
)
}
}
var total: UInt64 = 0
for try await partial in group {
total = total &+ partial
}
try Task.checkCancellation()
return total
}
}
@concurrent
private static func calculateChunk(
lower: Int,
upper: Int,
rounds: Int
) async throws -> UInt64 {
try Task.checkCancellation()
var total: UInt64 = 0
for index in lower..<upper {
if index % 256 == 0 {
try Task.checkCancellation()
}
var value = UInt64(index)
for round in 0..<rounds {
if round % 4096 == 0 {
try Task.checkCancellation()
}
value = (value &* 1_664_525) &+ 1_013_904_223
}
total = total &+ value
}
try Task.checkCancellation()
return total
}
}
हर child immutable bounds capture करता है और एक integer लौटाता है। केवल parent task ही total को update करता है; children किसी shared collection में append नहीं करते और interface state नहीं बदलते। Task groups concurrent child execution का समर्थन करते हैं, sendable results माँगते हैं और child lifetimes को group के दायरे में रखते हैं। Apple का TaskGroup documentation देखें।
यह interface code उसी file में जोड़ें:
@main
@MainActor
struct ParallelCalculationApp: App {
var body: some Scene {
WindowGroup {
CalculationView()
}
}
}
@MainActor
struct CalculationView: View {
@State private var status = "Ready"
@State private var work: Task<Void, Never>?
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Synthetic Calculation")
.font(.title2)
Text(status)
.textSelection(.enabled)
HStack {
Button("Calculate") {
startCalculation()
}
.disabled(work != nil)
Button("Cancel") {
work?.cancel()
}
.disabled(work == nil)
}
}
.padding(24)
.frame(minWidth: 440, minHeight: 180)
.onDisappear {
work?.cancel()
}
}
private func startCalculation() {
guard work == nil else { return }
status = "Calculating…"
let workerLimit = max(
1,
ProcessInfo.processInfo.activeProcessorCount
)
work = Task { @MainActor in
defer { work = nil }
do {
let checksum = try await SyntheticCalculator.run(
count: 1_000_000,
rounds: 128,
workerLimit: workerLimit
)
try Task.checkCancellation()
status = "Checksum: \(checksum)"
} catch is CancellationError {
status = "Cancelled"
} catch {
status = "Calculation failed: \(error)"
}
}
}
}
Interface outer task handle का स्वामी है, ताकि Cancel बटन उसे रद्द कर सके। उसका task main-actor पर isolated रहता है, concurrent calculation को call करता है और परिणाम की प्रतीक्षा के बाद प्रदर्शित status update करता है। Handle रखने से view गायब होने पर भी cancellation संभव है। Apple का Task documentation बताता है कि handle छोड़ देने से task रद्द नहीं होता।
Worker limit की शुरुआती संख्या Foundation के activeProcessorCount से ली गई है, जो कंप्यूटर पर उपलब्ध सक्रिय processing cores की संख्या बताता है। इसे मापकर परखा जाने वाला शुरुआती आकार मानें, optimal task count की गारंटी नहीं। इसका दस्तावेज़ित अर्थ activeProcessorCount में है।
प्रदर्शन की तुलना करने से पहले शुद्धता जाँचें
छोटे उदाहरण वाले input से शुरू करें: call में count: 17, rounds: 4, और workerLimit: 3 रखें। अपेक्षित checksum 18149427060254280572 है। इसे workerLimit: 1 के साथ दोहराएँ; मान समान होना चाहिए। count: 0 भी जाँचें, जिसमें zero लौटना चाहिए, और ऐसा positive count भी जो worker limit से छोटा हो; उसमें हर index शामिल होना चाहिए।
उदाहरण के checksum और range coverage को wrapping arithmetic की छोटी Python पुनरावृत्ति से जाँचा गया। शोध वातावरण में Swift compiler उपलब्ध नहीं था, इसलिए Swift program को यहाँ compile या execute नहीं किया गया है।
इसके बाद बड़ा input बहाल करें और worker limits एक, दो और सक्रिय processor count पर रखकर optimized builds की तुलना करें। Count और rounds समान रखें, प्राप्त checksum दर्ज करें और कई runs का मापन करें। यह मापन की प्रस्तावित प्रक्रिया है; किसी timing या speedup का दावा नहीं किया जा रहा है।
एक worker वाला run इसी implementation की concurrent boundary और task-group संरचना का उपयोग करता है, इसलिए तुलना के लिए उपयोगी है। छोटे workloads में partitioning और scheduling का overhead उचित न हो। यदि किसी वास्तविक गणना में अलग-अलग indices की लागत काफ़ी अलग हो, तो बराबर आकार की ranges के कारण एक worker बाकी से देर तक चल सकता है; कम संख्या में एक साथ चल रहे tasks के साथ छोटे chunks अगला उचित प्रयोग हैं।
Array बनाने वाली गणना में, हर परिणाम के साथ chunk index जोड़ें और chunks को input क्रम में जोड़ें। Task-group children किसी भी क्रम में चल सकते हैं, और group पूरे होते ही उपलब्ध results देता है। Checksum में क्रम का सवाल केवल इसलिए नहीं उठता, क्योंकि चुना गया reduction क्रम से स्वतंत्र है। TaskGroup देखें।
Cancellation और isolation की समस्याएँ स्पष्ट करें
Cancellation cooperative होती है। cancel() call करने से task चिह्नित होता है; calculation code को उस स्थिति का पता लगाना होगा। दोनों loops में checks worker को throw करने के मौके देते हैं, उस स्थिति में भी जब किसी एक index पर कई rounds हों। ये intervals implementation के चुनाव हैं, cancellation latency की गारंटी नहीं। Apple इस ज़िम्मेदारी का वर्णन Task में करता है।
Throw करने वाला result loop भी अहम है। यदि कोई child throw करता है और उसकी error group body से बाहर propagate होती है, तो बाकी children रद्द कर दिए जाते हैं। फिर भी group उनके पूरा होने तक प्रतीक्षा करता है, इसलिए cancellation checks आवश्यक हैं। Swift standard library का implementation इस व्यवहार को TaskGroup.swift में दर्ज करता है।
यदि interface अब भी रुक-रुक कर चलता है, तो महँगे call path का पूरा trace लें। देखें कि कहीं worker main-actor method तो call नहीं कर रहा, actor-isolated properties तक पहुँच तो नहीं रहा, या concurrent boundary तक पहुँचने से पहले काफ़ी तैयारी तो नहीं कर रहा। ज़रूरी integer values worker को दें और लंबे loops उसी boundary के भीतर रखें।
यदि compiler @concurrent स्वीकार नहीं करता, तो toolchain version जाँचें। यह attribute Swift 6.2 में लागू हुआ और asynchronous functions पर लागू होता है। पुराने toolchain के लिए, पहले का nonisolated asynchronous execution model SE-0461 में दर्ज है; उस compiler से मेल खाता documentation उपयोग करें और यह न मानें कि सभी versions में nonisolated async का व्यवहार एक जैसा है।
इस Mac task के लिए उपयोगी architecture सरल है: main actor interface state का स्वामी है, concurrent workers को स्वतंत्र values मिलती हैं, और parent पूरे results को जोड़ता है। Chunk sizes को tune करते समय value contract बनाए रखें, और performance में बदलाव रखने से पहले checksum की समानता तथा interface की responsiveness, दोनों जाँचें।
स्रोत
- Embracing Swift concurrency — Apple.
- Run nonisolated async functions on the caller’s actor by default — Swift.org, Swift Evolution.
- Allow nonisolated to prevent global actor inference — Swift.org, Swift Evolution.
- Concurrency — Swift.org, The Swift Programming Language.
- Advanced Operators — Swift.org, The Swift Programming Language.
- Task — Apple Developer Documentation.
- TaskGroup — Apple Developer Documentation.
- activeProcessorCount — Apple Developer Documentation.
- TaskGroup.swift — Swift.org, Swift standard library.