لتطبيق Mac يجب أن يُنهي الحسابات المحلية غير المتزامنة بترتيب إرسالها، استخدم أسلوب إرسال متزامنًا معزولًا على الممثل الرئيسي، بحيث يربط كل مهمة بالمهمة السابقة لها. عزل الممثل وحده لا يضمن هذا الترتيب.
يبني هذا الشرح قائمة انتظار صغيرة لحسابات أعداد صحيحة اصطناعية. ويحدد بدقة متى يحدث الإرسال، ويحافظ على الترتيب عبر نقاط التعليق، ويعيد نتيجة كل حساب، ويتيح للمهام اللاحقة المتابعة بعد فشل مهمة سابقة.
ماذا يعني الترتيب الصارم للإرسال؟
لنفترض أن تطبيقًا للحساب على سطح المكتب يرسل المهام A وB وC. والعقد المطلوب هو: ينتهي حساب A قبل بدء حساب B، وينتهي B قبل بدء C. أما انتظار ثلاث مهام بدأت كل منها على نحو مستقل بترتيب أبجدي، فلن يرتب سوى ملاحظة نتائجها.
حد الإرسال مهم. في هذا التنفيذ، يحدث الإرسال عند تنفيذ الأسلوب submit المعزول على الممثل الرئيسي وإضافة الرابط التالي إلى السلسلة. وتُنشئ الاستدعاءات المتزامنة المتتالية من سياق واحد للممثل الرئيسي الترتيب A ثم B ثم C، من دون تعليق بينها.
إذا طلبت مهام مستقلة الإرسال بالتزامن، فلن يكون ترتيب وصولها محددًا. تحافظ قائمة الانتظار على الترتيب الذي يقبل به أسلوب الإرسال الطلبات. وإذا كان تطبيقك يتطلب ترتيبًا معينًا بين تلك الجهات المنتجة، فحدّد ذلك الترتيب قبل الإرسال، مثلًا بجمع دفعة محدودة في مصفوفة مرتبة صراحةً.
يستخدم التصميم الممثل الرئيسي لخطوة التسجيل القصيرة. وتعرّف Apple MainActor بأنه ممثل مفرد يكون منفّذه مكافئًا لقائمة انتظار الإرسال الرئيسية. أما الحساب الفعلي أدناه فيُنفّذ ضمن ممثل عامل منفصل. توثيق MainActor
لماذا لا يكفي ممثل أو قائمة عمليات متسلسلة؟
ينفّذ الممثل العمل المعزول عليه قطعةً واحدة في كل مرة، لكن الأسلوب غير المتزامن قد يعلّق التنفيذ ويتيح لاستدعاء آخر المتابعة. كما يذكر تصميم الممثلين في Swift صراحةً أن المهام التي تنتظر ممثلًا لا يُضمن تشغيلها بترتيب انتظارها الأصلي. لذلك، فإن وضع حساب غير متزامن كامل داخل ممثل لا يضمن وحده أن ينتهي العمل قبل بدء العمل التالي. SE-0306: الممثلون
إنشاء مثيلات Task منفصلة لا ينشئ أيضًا أي تبعية بينها. قد تبدأ المهام بالتنفيذ بعد إنشائها؛ والاحتفاظ بمقابضها يتيح لك انتظارها أو إلغاءها، لكنه لا ينشئ تسلسلًا للتنفيذ. توثيق Task
توفّر Foundation آلية مختلفة للترتيب. يجدول OperationQueue العمليات وفقًا للجاهزية والأولوية والتبعيات؛ وتوصي Apple باستخدام التبعيات عندما يكون ترتيب تنفيذ محدد مهمًا. إن ضبط maxConcurrentOperationCount على 1 يحدّ من التزامن، لكن شرط الترتيب يظل بحاجة إلى تبعيات صريحة. توثيق OperationQueue
اختر الآلية وفقًا لما يحتاجه تطبيقك بالفعل:
| الحالة | النهج المناسب | ما الذي يحدد الترتيب؟ |
|---|---|---|
| دفعة واحدة معروفة ضمن دالة غير متزامنة واحدة | حلقة تنتظر كل حساب قبل المتابعة | تدفق التحكم التسلسلي |
| وصول المهام بمرور الوقت من إجراءات سطح المكتب | أسلوب إرسال متزامن مع سلسلة مهام | ينتظر كل حساب بوابة اكتمال المهمة السابقة |
| عمليات Foundation موجودة بالفعل | ضبط التبعيات قبل الإضافة إلى قائمة الانتظار | تعتمد كل عملية على حالة انتهاء العملية السابقة |
سلسلة المهام مفيدة هنا لأن الإرسال يعيد مقبضًا فورًا بينما تُنفّذ الحسابات لاحقًا. أما للدفعة الثابتة التي لا ترد إليها مهام جديدة، فعادةً ما تتطلب الحلقة التسلسلية شيفرة أقل.
أنشئ قائمة انتظار تنتظر اكتمال الحسابات غير المتزامنة بالكامل
تخزّن قائمة الانتظار مهمة اكتمال خاصة لا ترمي أخطاء، اسمها tail. ويلتقط كل إرسال الذيل الحالي، وينشئ مهمة حساب تنتظره، ثم يستبدل الذيل بمهمة جديدة تراقب اكتمال الحساب.
تنتظر مهمة الاكتمال result، التي تمثل النجاح أو الفشل بعد اكتمال المهمة. وهذا يتيح للحساب غير الناجح تحرير المهمة التالية من دون إسقاط الخطأ من مقبض الحساب نفسه. ولا يزال بإمكان المستدعين استرجاع ذلك الخطأ عبر المقبض. توثيق نتيجة Task
فيما يلي عرض كامل لسطر الأوامر على جهاز Mac مزود بسلسلة أدوات Swift 6. احفظه باسم OrderedDemo.swift. فُحص المصدر، لكن تعذّر تجميعه في بيئة البحث هذه لعدم توفر مُصرّف Swift.
import Foundation
@MainActor
final class OrderedIntQueue {
private var tail: Task<Void, Never>?
func submit(
_ operation: @escaping @Sendable () async throws -> Int
) -> Task<Int, Error> {
let predecessor = tail
let job = Task<Int, Error> {
if let predecessor {
await predecessor.value
}
try Task.checkCancellation()
return try await operation()
}
tail = Task<Void, Never> {
_ = await job.result
}
return job
}
}
enum CalculationError: Error {
case inputOutOfRange
}
actor CalculationWorker {
private var events: [String] = []
func square(_ input: Int, label: String) async throws -> Int {
events.append("start \(label)")
guard (-1000...1000).contains(input) else {
throw CalculationError.inputOutOfRange
}
await Task.yield()
try Task.checkCancellation()
let answer = input * input
events.append("finish \(label)")
return answer
}
func trace() -> [String] {
events
}
}
@main
struct OrderedDemo {
@MainActor
static func main() async {
let queue = OrderedIntQueue()
let worker = CalculationWorker()
let a = queue.submit {
try await worker.square(3, label: "A")
}
let b = queue.submit {
try await worker.square(4, label: "B")
}
let c = queue.submit {
try await worker.square(5, label: "C")
}
var answers: [Int] = []
for job in [a, b, c] {
switch await job.result {
case .success(let answer):
answers.append(answer)
case .failure(let error):
print("Calculation failed: \(error)")
}
}
let events = await worker.trace()
precondition(answers == [9, 16, 25])
precondition(events == [
"start A", "finish A",
"start B", "finish B",
"start C", "finish C"
])
print(events.joined(separator: "\n"))
print("Answers: \(answers)")
}
}
جمّع البرنامج المحفوظ وشغّله من مجلده في Terminal:
swiftc -swift-version 6 -parse-as-library OrderedDemo.swift -o OrderedDemo
./OrderedDemo
الناتج المتوقع مستمد من مدخلات المثال وسلسلة التبعيات:
start A
finish A
start B
finish B
start C
finish C
Answers: [9, 16, 25]
تتحقق التأكيدات من القيم المعادة وحدود الحساب معًا. فمجرد طباعة النتائج أثناء انتظار المقابض بترتيب الإرسال لا يثبت أن الحسابات نُفذت بذلك الترتيب.
يستدعي العامل عمدًا Task.yield() كي يتضمن الحساب فرصة للتعليق. يتيح التنازل لمهام أخرى التنفيذ، مع أن المنفّذ قد يستأنف المهمة نفسها فورًا. لذلك، يأتي ترتيب قائمة الانتظار من التبعيات، لا من افتراض أن التنازل سيبدّل المهام. توثيق تنازل Task
لماذا تحافظ السلسلة على الترتيب عبر التعليق؟
ثلاثة أمور تحقق هذا الضمان. أولًا، لا يحتوي submit على نقطة تعليق، لذا لا يمكن لإرسال آخر على الممثل الرئيسي أن يتداخل أثناء التقاطه tail واستبداله. وتوثّق Swift أن الشيفرة المعزولة على ممثل، إذا خلت من نقاط التعليق، تعمل من دون أن تقاطع مهمة أخرى على الممثل ذلك التسلسل. التزامن: الممثلون
ثانيًا، تنتظر كل مهمة سابقتها التي التقطتها قبل استدعاء إغلاق الحساب. ويؤدي الوصول إلى value لمهمة لا ترمي أخطاء إلى انتظار اكتمال تلك المهمة. وقد تنفّذ المهمة اللاحقة خطوات الانتظار التحضيرية مبكرًا، لكنها لا تستطيع بدء حسابها مبكرًا. توثيق value لمهمة لا ترمي أخطاء
ثالثًا، لا تكتمل بوابة السابق إلا بعد أن تنتج المهمة السابقة نتيجتها أو خطأها. ويؤدي تتبع الروابط إلى الخلف إلى إثبات التسلسل لكل إرسال مقبول: تنتظر C بوابة اكتمال B، التي تنتظر B، التي تنتظر بوابة اكتمال A.
هذا ضمان ترتيب للعمل الذي ينتظره داخل كل إغلاق مُرسل. إذا أطلق الإغلاق مهمة غير مرتبطة وأعاد التنفيذ من دون انتظارها، فقد يستمر ذلك العمل الإضافي بعد انتهاء الإغلاق. أدرج كل خطوة حساب مطلوبة ضمن العملية المنتظرة قبل إعادتها.
لدمج ذلك مع تطبيق سطح المكتب، احتفظ بمثيل واحد من قائمة الانتظار ضمن المتحكم أو النموذج المعزول على الممثل الرئيسي الذي يتعامل مع الإجراءات ذات الصلة. استدعِ submit مباشرةً من ذلك السياق، ثم استخدم المقبض المعاد لمراقبة الاكتمال. إن إنشاء مهمة مستقلة أولًا وإرسالها بعد أن تبدأ، ينقل حد الإرسال إلى وقت تنفيذها لاحقًا.
كيف ينبغي أن تتصرف الأخطاء والإلغاء؟
يتبع هذا التنفيذ سياسة المتابعة بعد الفشل. تحتفظ المهمة الفاشلة بخطئها، وتكتمل بوابة اكتمالها، وتصبح المهمة التالية المقبولة مؤهلة للحساب. ويفحص العرض result لكل مقبض بدلًا من التخلي عن النتائج المتبقية بعد أول خطأ.
لاختبار بسيط للفشل، غيّر مُدخل B من 4 إلى 1001. سيرفضه العامل قبل الضرب. حدّث الإجابات المتوقعة إلى [9, 25] وأثر التنفيذ المتوقع إلى start A وfinish A وstart B وstart C وfinish C. ينبغي أن يطبع البرنامج الفشل مع مواصلة حساب C. حدّ الإدخال عقد توضيحي اختير لإبقاء العمليات الحسابية صغيرة.
الإلغاء تعاوني. تتحقق قائمة الانتظار من الإلغاء بعد انتظار المهمة السابقة، ويتحقق العامل منه مرة أخرى بعد التنازل. تُلقي Task.checkCancellation() الخطأ CancellationError عند طلب الإلغاء. توثيق checkCancellation في Task
للتحقق من الإلغاء قبل التنفيذ، استدعِ b.cancel() فور إرسال C وقبل حلقة النتائج. مع عمليات الإرسال المتواصلة هذه على الممثل الرئيسي، لا يمكن أن تكون B قد دخلت حسابها بعد. عدّل الإجابات المتوقعة إلى [9, 25] واحذف إدخالي B من أثر التنفيذ المتوقع. ستظل B تنتظر بوابة اكتمال A قبل الإبلاغ عن الإلغاء، ثم تتابع C.
أبقِ بوابات الاكتمال خاصة. فهي تمثل تسجيل السلسلة، بينما تمثل المقابض المعادة الحسابات الفردية. إلغاء مهمة لا يعني أن على الحساب السابق أن يتوقف، أو أن تبدأ الحسابات اللاحقة بينما لا يزال قيد التنفيذ.
إذا كان تطبيقك يتطلب بدلًا من ذلك إيقاف الدفعة كلها بعد فشل واحد، فذلك يتطلب سياسة دفعة صريحة. إن تغيير البوابة لتمرير الأخطاء سيغيّر المهام اللاحقة التي تُنفذ وكيفية الإبلاغ عن نتائجها.
متى ينبغي استخدام تبعيات Foundation بدلًا من ذلك؟
إذا كان تطبيق سطح المكتب يمثّل العمل بالفعل على هيئة كائنات Operation من Foundation، فأنشئ تبعية من كل عملية إلى سابقتها قبل إضافتها إلى قائمة الانتظار. تعتبر Foundation التبعية مستوفاة عند انتهاء السابقة، سواء نجحت أم فشلت؛ أما السلوك المعتمد على النجاح فيحتاج إلى معالجة منفصلة للنتيجة. توثيق Operation
توخَّ الحذر عند ربط دوال Swift غير المتزامنة بهذا النموذج. فالعملية المتزامنة التي تبدأ Task ثم تعيد التنفيذ فورًا لا تمثل دورة حياة المهمة كاملة. يجب أن تدير العملية غير المتزامنة المخصصة تنفيذها وحالة انتهائها بدقة، بما في ذلك الإشعارات المطلوبة، كي تنتظر العمليات التابعة العمل المقصود. توثّق Apple هذه المسؤوليات ضمن العمليات غير المتزامنة والإبلاغ عن حالة الانتهاء. توثيق دورة حياة Operation
بالنسبة إلى آلة الحساب المحلية غير المتزامنة المطوّرة هنا، تجعل سلسلة المهام العقد واضحًا: قبول الإرسالات على نحو متزامن، وانتظار اكتمال السابقة، وتنفيذ الحساب بالكامل، وإتاحة نتيجته. حافظ على هذه الحدود عند ربط قائمة الانتظار بإجراءات سطح المكتب، وتحقق من التنفيذ باستخدام آثار البدء والانتهاء إلى جانب القيم المعادة.
المصادر
- MainActor — Apple
- SE-0306: Actors — Swift
- Task — Apple
- OperationQueue — Apple
- result — Apple
- yield() — Apple
- Concurrency — Swift
- value: Nonthrowing task — Apple
- checkCancellation() — Apple
- Operation — Apple