للاحتفاظ بحسابات Swift الناجحة عند فشل المهام الشقيقة، التقط خطأ كل حساب داخل مهمته الفرعية وأعِد قيمة تمثل النتيجة. يجمع مثال سطر الأوامر هذا على Mac كل النتائج، ويحافظ على ترتيب الإدخال، ويُبقي القيم الناجحة متاحة.
المهمة عبارة عن حساب دفعي صغير اصطناعي: تربيع قائمة من الأعداد الصحيحة غير السالبة والإبلاغ عن المدخلات التي نجحت. تؤدي المدخلات السالبة وتجاوز نطاق الأعداد الصحيحة إلى إخفاقات فردية. تبقى جميع بيانات الحساب في الذاكرة؛ ويطبع البرنامج نتائجه في Terminal.
لماذا قد يوقف خطأ واحد جمع النتائج؟
لا تؤدي مهمة فرعية ترمي خطأً إلى إلغاء أشقائها فورًا. الحد الفاصل المؤثر هو متن مجموعة المهام: فعندما يخرج خطأ من ذلك المتن، تلغي Swift المهام الفرعية المتبقية. توثّق Apple هذا التمييز في مرجعها للدالة withThrowingTaskGroup(of:returning:isolation:body:).
لذلك، قد تتوقف حلقة تكرار ترمي الأخطاء مع التقاط خارجي عند أول خطأ تتم ملاحظته. قد تكون القيم السابقة قد خُزّنت بالفعل، لكن الحلقة لا تستأنف عملها بعد ذلك الالتقاط. وتتلقى المهام الفرعية المتبقية إشارة الإلغاء إذا خرج الخطأ من متن مجموعة المهام.
في الحسابات المستقلة، اجعل سياسة التعامل مع الإخفاق واضحة. يستخدم المثال أدناه مجموعة مهام لا ترمي الأخطاء، وتعيد مهامها الفرعية نجاحًا أو إخفاقًا أو إلغاءً. يظل الحساب نفسه يرمي الأخطاء؛ لكن غلافه يحوّل ذلك الخطأ إلى قيمة قبل وصوله إلى المجموعة.
يتوافق هذا مع الحد الذي توثّقه Swift: لا يمكن للمهام الفرعية في مجموعة مهام لا ترمي الأخطاء تمرير أخطاء مرمية، لذا عليها التعامل معها داخليًا أو تمثيل نتيجتها بقيمة مُعادة. راجع توثيق TaskGroup من Apple.
حدّد ما يجب أن يعيده كل إدخال
عقد الدفعة هو نتيجة واحدة لكل موضع إدخال. يعيد الحساب الناجح مربعًا صحيحًا دقيقًا. ويعيد الحساب المرفوض وصفًا للإخفاق. أما الحساب الذي يُتخطى بعد ملاحظة الإلغاء فيعيد نتيجة إلغاء.
| الحالة | النتيجة | سلوك الدفعة |
|---|---|---|
إدخال غير سالب يتسع مربعه ضمن نطاق Int64 |
نجاح يتضمن المربع | احتفِظ بالقيمة |
| إدخال سالب | إخفاق: negativeInput |
واصل جمع النتائج الأخرى |
تجاوز المربع نطاق Int64 |
إخفاق: overflow |
واصل جمع النتائج الأخرى |
| ملاحظة الإلغاء قبل الحساب | مُلغى | احتفِظ بالنتائج المكتملة وتخطَّ العمل المعلّق |
رفض المدخلات السالبة قاعدة تطبيق توضيحية؛ إذ يمكن تربيع الأعداد السالبة رياضيًا. ويساعد فصل هذه القاعدة عن تجاوز النطاق على توضيح حالتي إخفاق مختلفتين في المثال.
للضرب، استخدم multipliedReportingOverflow(by:) وافحص علامة تجاوز النطاق. تحدد Apple أن القيمة الجزئية المُعادة تمثل حاصل الضرب الكامل فقط عندما تكون تلك العلامة غير مفعّلة؛ وإلا تكون القيمة مبتورة. يتخلص المثال من تلك القيمة المبتورة ويرمي خطأً. راجع Int64.multipliedReportingOverflow(by:).
تحمل كل نتيجة أيضًا فهرس إدخالها. لا تضمن Swift ترتيب اكتمال المهام الفرعية، لذا فإن إلحاق القيم المكتملة وحدها لا يحافظ على الترتيب الأصلي. يشرح دليل اللغة سلوك الجدولة هذا ضمن التزامن.
شغّل المثال الكامل على Mac
استخدم Mac مثبتًا عليه إصدار Swift 6 أو أحدث. في Terminal، شغّل swift --version للتحقق من المترجم المثبّت، وفق توصية دليل البدء على Swift.org. إذا لم تكن Swift متاحة، فاتبع رابط التثبيت في ذلك الدليل قبل المتابعة.
احفظ البرنامج الكامل التالي في مجلد عمل باسم Calculations.swift. يستورد البرنامج 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. ينفّذ البرنامج الحسابات من دون قراءة بيانات التطبيق أو كتابتها. لم تكن Swift متاحة في بيئة البحث، لذا جرى فحص البرنامج دون ترجمته أو تنفيذه هناك.
للمدخلات المقدمة ومن دون إلغاء، يكون الناتج المتوقع:
[0] 3: success 9
[1] -2: failure negativeInput
[2] 4: success 16
[3] 9223372036854775807: failure overflow
[4] 0: success 0
Successful values: [9, 16, 0]
يأتي الترتيب المطبوع من الفرز حسب فهرس الإدخال بعد الجمع. وقد يختلف ترتيب التنفيذ من دون أن يتغير هذا العرض. تُبنى مصفوفة القيم الناجحة بعد ذلك، مع إبقاء تقرير النتائج الكامل متاحًا للفحص.
كيف يحتفظ المُجمّع بكل نتيجة ناجحة؟
تملك المهمة الأم مصفوفة النتائج. تلتقط كل مهمة فرعية فهرسًا واحدًا ومدخلًا ثابتًا واحدًا، وتحسب نتيجته، ثم تعيد صفًا. لا تلحق المهام الفرعية مباشرةً بمصفوفة المهمة الأم. يتبع هذا نهج دليل اللغة في تجميع نتائج المهام الفرعية داخل متن مجموعة المهام. راجع التزامن.
يرسل المُجمّع في البداية عددًا محدودًا من المهام الفرعية. ويتيح كل صف مكتمل إرسال مهمة أخرى لمعالجة مدخل جديد. لذا يتقدم الحساب الفاشل في الدفعة بالطريقة نفسها التي يتقدم بها الحساب الناجح: فكلاهما يعيد صفًا عاديًا ويستمر الجمع.
تعرض activeProcessorCount في Foundation عدد أنوية المعالجة النشطة المتاحة على الكمبيوتر. يضع المثال حدًا أقصى لهذا العدد عند أربعة بوصفه خيارًا توضيحيًا للجدولة. هذا الحد ليس قيمة مثلى مقاسة، ولا يضمن تشغيل أربعة حسابات في الوقت نفسه. راجع توثيق Apple لـactiveProcessorCount.
يحد حد التزامن من عدد المهام الفرعية القائمة، بينما تظل مصفوفة النتائج تكبر مع عدد المدخلات. وهذا مناسب لدفعة صغيرة على كمبيوتر مكتبي. بالنسبة إلى عمليات التربيع الصغيرة هذه، يوضح المثال التعامل مع النتائج؛ ولا يدّعي أي تفوق في الأداء على حلقة متسلسلة.
يعيد التحقق من المدخلات الفارغة مصفوفة فارغة. ويُضبط حد التزامن الصفري أو السالب إلى واحد. وتظل قيم الإدخال المكررة منفصلة لأن الهوية تُحدد حسب الموضع، لذا ينتج عن مدخلين متساويين صفّان.
ماذا يحدث عند إلغاء المهمة الأم؟
الإلغاء في Swift تعاوني. يؤدي إلغاء مهمة أم إلى تمرير الإلغاء إلى مهامها الفرعية المنظّمة، لكن يجب على المهمة الفرعية ملاحظة تلك الإشارة والاستجابة لها. يشرح توثيق TaskGroup من Apple آلية التمرير وضرورة التعامل التعاوني مع الإلغاء.
هنا، تستدعي evaluate الدالة Task.checkCancellation() قبل الحساب. توثّق Apple أن هذه الدالة ترمي CancellationError عند ملاحظة الإلغاء؛ ويلتقطها الغلاف على نحو منفصل ويعيد صفًا ملغى. راجع checkCancellation().
عندما يلاحظ المُجمّع الإلغاء، يضع علامة الإلغاء على المدخلات التي لم تُرسل بعد. وتظل المهام الفرعية التي أُرسلت بالفعل قيد الجمع حتى اكتمالها. تبقى نتائجها الناجحة في التقرير، وتعيد المهام الفرعية التي تلاحظ الإلغاء صفوفًا ملغاة.
قد يصل الإلغاء بعد فحص المهمة الفرعية. وقد تكتمل تلك المهمة بنجاح، وهو ما يقبله هذا العقد. إذا أصبح الحساب حلقة طويلة، فأضف فحوص الإلغاء على فترات مناسبة داخل الحساب. وتظل المجموعة تنتظر انتهاء جميع المهام الفرعية المُرسلة قبل الإعادة، كما هو موثق لـTaskGroup.
متى ينبغي استخدام مجموعة مهام ترمي الأخطاء بدلًا من ذلك؟
إذا كانت المهام الفرعية الموجودة ترمي أخطاءً بالفعل وأردت الحفاظ على واجهتها، تتيح مجموعة المهام التي ترمي الأخطاء أيضًا جمع النتائج الفردية. استدعِ nextResult() مرارًا واستخدم التبديل على كل نجاح أو إخفاق مُعاد حتى تصبح النتيجة الاختيارية فارغة. وعلى خلاف next()، تعيد هذه الدالة قيمة Result بدلًا من رمي خطأ المهمة الفرعية إلى المستدعي. توثّق Apple سلوكها وفق ترتيب الاكتمال ضمن nextResult().
في هذا التصميم، أدرج هوية الإدخال في القيم الناجحة وفي أي أخطاء تحتاج إلى إسنادها. فالخطأ الحسابي المجرد لا يحدد أي إدخال من عدة مدخلات تسبب فيه. واختر أيضًا سياسة إلغاء صريحة بدل التعامل مع الإلغاء على أنه إخفاق حساب عادي.
استخدم نهج إرجاع النتائج عندما يكون التقرير الكامل لكل إدخال هو المتطلب الأساسي. واستخدم مجموعة ترمي الأخطاء عندما يكون تمرير خطأ خارج المجموعة هو سلوك الدفعة المطلوب، أو عندما يكون استهلاك العمليات الحالية التي ترمي الأخطاء عبر nextResult() أسهل.
في مهمة الحساب المحلية هذه، العقد الأساسي بسيط: احتفِظ بكل قيمة ناجحة، وسجّل كل إدخال فاشل، وواصل الجمع. يتيح حمل فهرس الإدخال في كل نتيجة الاستفادة من التقرير النهائي حتى عندما تنتهي الحسابات بترتيب غير متوقع.
المصادر
- withThrowingTaskGroup(of:returning:isolation:body:) — توثيق Apple للمطورين.
- TaskGroup — توثيق Apple للمطورين.
- التزامن — لغة البرمجة Swift، مشروع Swift.
- multipliedReportingOverflow(by:) — توثيق Apple للمطورين.
- البدء — Swift.org.
- activeProcessorCount — توثيق Apple للمطورين.
- checkCancellation() — توثيق Apple للمطورين.
- nextResult() — توثيق Apple للمطورين.