لتشغيل حسابات Swift المكثفة لوحدة المعالجة المركزية بالتوازي، امنح العامل حدًا صريحًا باستخدام @concurrent ووزّع العمل المستقل على مهام فرعية. يحافظ مثال Mac هذا على تحديثات الواجهة في MainActor ويدمج النتائج الاصطناعية دون تعديل مشترك.
لماذا قد تؤدي إضافة المهام إلى تسلسل الحساب رغم ذلك
لنفترض أن تطبيق Mac يتضمن زر Calculate يبدأ حسابًا محليًا كبيرًا. يوفر تضمين الحساب في Task { ... } نقطة دخول غير متزامنة، لكن المهمة المنشأة في سياق العزل الخاص بالممثل الرئيسي قد ترث هذا العزل. وتظل الحلقة المتزامنة الطويلة داخلها مشغّلة للممثل الرئيسي. توضح Apple هذا الفرق في اعتماد التزامن في Swift.
تترك إضافة مجموعة مهام سؤالًا مهمًا بلا إجابة أيضًا: أين تُنفذ الدالة التي تحتوي على الحلقة المكلفة؟ إذا استدعى كل تابع طريقة الحساب نفسها المعزولة بـ @MainActor، فسيُسلسِل هذا الممثل الحساب. تسمح الممثلون لمهمة واحدة في كل مرة بالوصول إلى حالتها المعزولة، كما يشرح فصل التزامن في دليل لغة Swift.
في Swift 6.2 وما بعده، ضع العلامة @concurrent على نقطة الدخول غير المتزامنة للحساب. فهي تنقل التنفيذ صراحةً بعيدًا عن ممثل المتصل. عند تفعيل ميزة NonisolatedNonsendingByDefault، تعمل الدالة غير المعزولة غير المتزامنة العادية على ممثل المتصل بدلًا من ذلك؛ وكان السلوك السابق مختلفًا. يوثّق المقترح SE-0461 المنفذ كلًا من حدود الميزة والسمة الصريحة.
ينتج السلوك المطلوب عن خيارين منفصلين: عزل العامل يسمح بالتنفيذ بعيدًا عن MainActor، وتوفر المهام الفرعية المتعددة عملًا يمكن جدولته بشكل مستقل. يحدد وقت التشغيل الخيوط المتاحة التي تنفذ تلك المهام. لا يضمن عدد المهام ولا @concurrent عددًا معينًا من الأنوية التي تنفذ العمل في الوقت نفسه. تصف Apple نموذج الجدولة هذا في اعتماد التزامن في Swift.
تعريف حساب يمكن تقسيمه على نحو صحيح
يتيح المثال أدناه لمستخدم Mac بدء حساب اصطناعي أو إلغاءه، ثم عرض المجموع الاختباري النهائي. جميع المدخلات ثوابت أو أعداد صحيحة مولّدة. للحساب عقد قيمة محدد كما يلي:
- زيارة كل فهرس صحيح في
0..<countمرة واحدة بالضبط. - تحويل كل فهرس إلى
UInt64وتطبيقx = (x &* 1_664_525) &+ 1_013_904_223عليه تكرارًا للعدد المطلوب من الجولات. - جمع القيم الناتجة باستخدام الجمع مع الالتفاف.
- إرجاع صفر للمدخل الفارغ، ورفض الأعداد السالبة للجولات أو العدد، ورفض حدّ للعاملين غير موجب.
- إطلاق خطأ عند رصد الإلغاء بدلًا من عرض مجموع اختباري غير مكتمل على أنه نتيجة مكتملة.
الحساب مع الالتفاف مقصود هنا. تتسبب عمليات الحساب الصحيحة العادية في Swift بحدوث فخ عند التجاوز؛ أما معاملا &+ و&* فيدعمان سلوك التجاوز صراحةً. راجع المعاملات المتقدمة.
وفق هذا العقد، كل فهرس مستقل، ويجمع الجمع مع الالتفاف المجاميع الجزئية بترديد 2 مرفوعًا للأس 64. لذلك، يحافظ تغيير التجميع أو ترتيب الإكمال على القيمة النهائية نفسها تمامًا. ويترتب هذا الاستنتاج على الحساب المختار. أما حساب الفاصلة العائمة فيحتاج إلى قاعدة صحة خاصة به، لأن إعادة تجميع عمليات الجمع قد تغير التقريب.
يستخدم التقسيم القسمة والباقي: يحصل كل عامل على العدد الأساسي نفسه من الفهارس، ويحصل أول عدد من العاملين على فهرس إضافي واحد. على سبيل المثال، إذا كان العدد 17 وثمة ثلاثة عاملين، فالنطاقات هي 0..<6 و6..<12 و12..<17. وهي تغطي المدخلات كلها مرة واحدة، بما في ذلك الباقي.
إنشاء مثال حساب Mac كامل
استخدم سلسلة أدوات Swift 6.2 أو أحدث وهدف تطبيق SwiftUI على macOS مع هدف نشر macOS 13 أو أحدث. استبدل نقطة دخول التطبيق وطريقة عرض المحتوى المُنشأتين تلقائيًا بالكتلتين التاليتين، وضعهما معًا في ملف Swift واحد. إنهما يشكلان برنامجًا كاملًا؛ وأبقِ على تعريف واحد فقط لتطبيق @main في الهدف.
تُعلن مساحة أسماء الحساب صراحةً على أنها nonisolated، لذلك لا تكتسب أعضاؤها عزلًا مستنتجًا لممثل عام. توثّق Swift هذا الإعلان على مستوى النوع في SE-0449. تستخدم طريقتا الحساب غير المتزامنتان @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
}
}
يلتقط كل تابع حدودًا غير قابلة للتغيير ويعيد عددًا صحيحًا واحدًا. ولا تحدّث total إلا المهمة الأم؛ فلا تضيف التوابع إلى مجموعة مشتركة ولا تعدّل حالة الواجهة. تدعم مجموعات المهام التنفيذ المتزامن للمهام الفرعية، وتتطلب نتائج قابلة للإرسال، وتحصر عمر المهام الفرعية في نطاق المجموعة. راجع توثيق TaskGroup من Apple.
أضف كود الواجهة هذا إلى الملف نفسه:
@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)"
}
}
}
}
تتولى الواجهة ملكية مقبض المهمة الخارجية كي يتمكن زر Cancel من إلغائها. وتظل المهمة معزولة للممثل الرئيسي، وتستدعي الحساب المتزامن، ثم تحدّث الحالة المعروضة بعد انتظار النتيجة. كما يتيح الاحتفاظ بالمقبض إلغاء المهمة عند اختفاء العرض. يوضح توثيق Task من Apple أن التخلص من المقبض لا يلغي المهمة.
يبدأ حد العاملين من activeProcessorCount في Foundation، الذي يُبلغ عن أنوية المعالجة النشطة المتاحة على الحاسوب. تعامل معه كخيار أولي لضبط العدد يحتاج إلى قياس، لا كعدد مهام مثالي مضمون. المعنى الموثق له وارد في activeProcessorCount.
التحقق من الصحة قبل مقارنة الأداء
ابدأ بالمدخل التوضيحي الصغير: غيّر الاستدعاء إلى count: 17 وrounds: 4 وworkerLimit: 3. المجموع الاختباري المتوقع هو 18149427060254280572. أعد التجربة باستخدام workerLimit: 1؛ ويجب أن تتطابق القيمة. تحقّق أيضًا من count: 0، إذ يجب أن يعيد صفرًا، ومن عدد موجب أصغر من حد العاملين، إذ يجب أن يظل مشمولًا كل فهرس.
تم التحقق من المجموع الاختباري التوضيحي وتغطية النطاقات باستخدام إعادة تنفيذ صغيرة للحساب مع الالتفاف في Python. لم تتوفر مترجمة Swift في بيئة البحث، لذلك لم يُجمّع برنامج Swift ولم يُنفذ هنا.
بعد ذلك، أعد المدخل الأكبر وقارن بين البُنى المحسّنة باستخدام حدود عاملين قدرها واحد واثنان وعدد أنوية المعالجة النشطة. أبقِ العدد والجولات دون تغيير، واحتفظ بالمجموع الاختباري الناتج، وقِس عدة مرات. هذا إجراء قياس مقترح؛ ولم تُذكر أي أزمنة أو مكاسب في السرعة.
يستخدم التشغيل بعامل واحد حد التزامن نفسه وبنية مجموعة المهام نفسها، ما يجعله مقارنة مفيدة لهذا التنفيذ. قد لا تبرر أعباء العمل الصغيرة تكلفة التقسيم والجدولة. وإذا اختلفت تكاليف الفهارس اختلافًا كبيرًا في حساب واقعي، فقد تجعل النطاقات المتساوية الحجم عاملًا واحدًا يستغرق وقتًا أطول؛ ومن التجارب التالية المعقولة استخدام أجزاء أصغر مع عدد محدود من المهام قيد التنفيذ.
في حساب ينتج مصفوفة، أرفق فهرس الجزء بكل نتيجة واجمع الأجزاء بترتيب الإدخال. قد تُنفذ توابع مجموعة المهام بأي ترتيب، وتعرض المجموعة النتائج المكتملة فور توفرها. لا يواجه المجموع الاختباري مشكلات الترتيب إلا لأن الاختزال المختار لا يعتمد على الترتيب. راجع TaskGroup.
إظهار مشكلات الإلغاء والعزل
الإلغاء تعاوني. يؤدي استدعاء cancel() إلى تعليم المهمة، لكن على كود الحساب رصد هذه الحالة. تمنح عمليات التحقق في الحلقتين العامل فرصًا لإطلاق خطأ، بما في ذلك عندما يكون لفهرس واحد جولات كثيرة. هذه الفواصل خيارات تنفيذية، وليست ضمانًا لمدة زمنية محددة للاستجابة للإلغاء. توثّق Apple هذه المسؤولية في Task.
حلقة النتائج التي تطلق الأخطاء مهمة أيضًا. إذا أطلق تابع خطأ وانتشر إلى خارج جسم المجموعة، فستُلغى التوابع المتبقية. مع ذلك، تنتظر المجموعة انتهاءها، لذا تظل عمليات التحقق من الإلغاء ضرورية فيها. يوثّق تنفيذ مكتبة Swift القياسية هذا السلوك في TaskGroup.swift.
إذا ظلت الواجهة تتوقف، فتتبّع مسار الاستدعاء المكلف كاملًا. ابحث عن عامل يستدعي طريقة للممثل الرئيسي، أو يصل إلى خصائص معزولة لممثل، أو ينفذ تحضيرًا كبيرًا قبل الوصول إلى حد التزامن. مرّر قيم الأعداد الصحيحة المطلوبة إلى العامل، وأبقِ الحلقات الطويلة داخل ذلك الحد.
إذا رفضت المترجمة @concurrent، فتحقق من إصدار سلسلة الأدوات. نُفذت السمة في Swift 6.2 وتنطبق على الدوال غير المتزامنة. بالنسبة إلى سلسلة أدوات أقدم، يوثّق SE-0461 نموذج التنفيذ غير المتزامن غير المعزول السابق؛ استخدم الوثائق المطابقة لتلك المترجمة بدلًا من افتراض أن جميع الإصدارات تمنح nonisolated async السلوك نفسه.
لهذه المهمة على Mac، البنية المفيدة بسيطة: يتولى الممثل الرئيسي حالة الواجهة، وتتلقى العاملات المتزامنة قيمًا مستقلة، وتجمع المهمة الأم النتائج المكتملة. حافظ على عقد القيمة عند ضبط أحجام الأجزاء، وتحقق من تطابق المجموع الاختباري واستجابة الواجهة قبل الإبقاء على أي تغيير في الأداء.
المصادر
- اعتماد التزامن في Swift — Apple.
- تشغيل الدوال غير المعزولة غير المتزامنة على ممثل المتصل افتراضيًا — Swift.org، Swift Evolution.
- السماح لـ nonisolated بمنع استنتاج الممثل العام — Swift.org، Swift Evolution.
- التزامن — Swift.org، لغة برمجة Swift.
- المعاملات المتقدمة — Swift.org، لغة برمجة Swift.
- Task — وثائق مطوري Apple.
- TaskGroup — وثائق مطوري Apple.
- activeProcessorCount — وثائق مطوري Apple.
- TaskGroup.swift — Swift.org، مكتبة Swift القياسية.