دليل سهل جداً
لتزامن Swift
افهم أخيراً async/await والمهام ولماذا المترجم يصرخ عليك.
شكر كبير لـ Matt Massicotte لجعل تزامن Swift مفهوماً. من إعداد Pedro Piñera، مؤسس مشارك لـ Tuist. وجدت مشكلة؟ افتح issue أو أرسل PR.
الكود غير المتزامن: async/await
معظم ما تفعله التطبيقات هو الانتظار. جلب البيانات من خادم - انتظر الرد. قراءة ملف من القرص - انتظر البايتات. استعلام قاعدة بيانات - انتظر النتائج.
قبل نظام التزامن في Swift، كنت تعبر عن هذا الانتظار باستخدام callbacks أو delegates أو Combine. كلها تعمل، لكن الـ callbacks المتداخلة تصبح صعبة المتابعة، وCombine له منحنى تعليمي حاد.
async/await يعطي Swift طريقة جديدة للتعامل مع الانتظار. بدلاً من الـ callbacks، تكتب كوداً يبدو متسلسلاً - يتوقف مؤقتاً، ينتظر، ويستأنف. خلف الكواليس، يدير runtime الـ Swift هذه التوقفات بكفاءة. لكن جعل تطبيقك يبقى متجاوباً أثناء الانتظار يعتمد على أين يعمل الكود، وهو ما سنغطيه لاحقاً.
الدالة غير المتزامنة هي دالة قد تحتاج إلى التوقف مؤقتاً. تضع علامة async عليها، وعندما تستدعيها، تستخدم await لتقول "توقف هنا حتى ينتهي هذا":
func fetchUser(id: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, _) = try await URLSession.shared.data(from: url) // يتعلق هنا
return try JSONDecoder().decode(User.self, from: data)
}
// استدعاؤها
let user = try await fetchUser(id: 123)
// الكود هنا يعمل بعد اكتمال fetchUser
كودك يتوقف مؤقتاً عند كل await - هذا يسمى التعليق. عندما ينتهي العمل، يستأنف كودك من حيث توقف. التعليق يعطي Swift الفرصة للقيام بعمل آخر أثناء الانتظار.
الانتظار لـ عدة أشياء
ماذا لو احتجت جلب عدة أشياء؟ يمكنك انتظارها واحدة تلو الأخرى:
let avatar = try await fetchImage("avatar.jpg")
let banner = try await fetchImage("banner.jpg")
let bio = try await fetchBio()
لكن هذا بطيء - كل واحدة تنتظر السابقة لتنتهي. استخدم async let لتشغيلها بالتوازي:
func loadProfile() async throws -> Profile {
async let avatar = fetchImage("avatar.jpg")
async let banner = fetchImage("banner.jpg")
async let bio = fetchBio()
// الثلاثة جميعاً يجلبون بالتوازي!
return Profile(
avatar: try await avatar,
banner: try await banner,
bio: try await bio
)
}
كل async let يبدأ فوراً. الـ await يجمع النتائج.
await تحتاج async
يمكنك استخدام await فقط داخل دالة async.
إدارة العمل: المهام
المهمة هي وحدة عمل غير متزامن يمكنك إدارتها. كتبت دوال async، لكن المهمة هي ما يشغلها فعلاً. إنها كيف تبدأ كوداً async من كود متزامن، وتعطيك التحكم في ذلك العمل: انتظر نتيجته، ألغه، أو اتركه يعمل في الخلفية.
لنقل أنك تبني شاشة ملف شخصي. حمّل الصورة عندما تظهر الواجهة باستخدام معدّل .task، الذي يُلغى تلقائياً عندما تختفي الواجهة:
struct ProfileView: View {
@State private var avatar: Image?
var body: some View {
Group {
if let avatar {
avatar
} else {
ProgressView()
}
}
.task { avatar = await downloadAvatar() }
}
}
إذا كان المستخدمون يمكنهم التبديل بين الملفات الشخصية، استخدم .task(id:) لإعادة التحميل عندما يتغير الاختيار:
struct ProfileView: View {
var userID: String
@State private var avatar: Image?
var body: some View {
Group {
if let avatar {
avatar
} else {
ProgressView()
}
}
.task(id: userID) { avatar = await downloadAvatar(for: userID) }
}
}
عندما ينقر المستخدم على "حفظ"، أنشئ مهمة يدوياً:
Button("Save") {
Task { await saveProfile() }
}
الوصول لنتائج المهام
عندما تنشئ Task، تحصل على مقبض. استخدم .value للانتظار واسترجاع النتيجة:
let handle = Task {
return await fetchUserData()
}
let userData = await handle.value // يتوقف حتى تكتمل المهمة
هذا مفيد عندما تحتاج النتيجة لاحقاً، أو عندما تريد تخزين مقبض المهمة وانتظاره في مكان آخر.
ماذا لو احتجت تحميل الصورة والسيرة والإحصائيات كلها مرة واحدة؟ استخدم TaskGroup لجلبها بالتوازي:
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { avatar = try await downloadAvatar(for: userID) }
group.addTask { bio = try await fetchBio(for: userID) }
group.addTask { stats = try await fetchStats(for: userID) }
try await group.waitForAll()
}
المهام داخل المجموعة هي مهام فرعية، مرتبطة بالأب. بعض الأشياء لتعرفها:
- الإلغاء ينتشر: ألغِ الأب، وجميع الأبناء يُلغون أيضاً
- الأخطاء: خطأ مُلقى يلغي الأشقاء ويُعاد إلقاؤه، لكن فقط عندما تستهلك النتائج بـ
next()أوwaitForAll()أو التكرار - ترتيب الاكتمال: النتائج تصل عندما تنتهي المهام، ليس بالترتيب الذي أضفتها به
- ينتظر الجميع: المجموعة لا ترجع حتى يكتمل كل طفل أو يُلغى
هذا هو التزامن المنظم: عمل منظم في شجرة سهلة الفهم والتنظيف.
أين تعمل الأشياء: من الخيوط إلى نطاقات العزل
حتى الآن تحدثنا عن متى يعمل الكود (async/await) وكيف ننظمه (المهام). الآن: أين يعمل، وكيف نبقيه آمناً؟
معظم التطبيقات فقط تنتظر
معظم كود التطبيقات مرتبط بالإدخال/الإخراج. تجلب بيانات من شبكة، تنتظر رداً، تفك تشفيرها، وتعرضها. إذا كان لديك عدة عمليات I/O للتنسيق، تلجأ إلى المهام ومجموعات المهام. العمل الفعلي على المعالج ضئيل. الخيط الرئيسي يمكنه التعامل مع هذا لأن await يعلق بدون حجب.
لكن عاجلاً أو آجلاً، سيكون لديك عمل مرتبط بالمعالج: تحليل ملف JSON ضخم، معالجة صور، تشغيل حسابات معقدة. هذا العمل لا ينتظر أي شيء خارجي. يحتاج فقط دورات معالج. إذا شغلته على الخيط الرئيسي، واجهتك تتجمد. هنا يصبح "أين يعمل الكود" مهماً فعلاً.
العالم القديم: خيارات كثيرة، بدون أمان
قبل نظام التزامن في Swift، كان لديك عدة طرق لإدارة التنفيذ:
| الطريقة | ما تفعله | المقايضات |
|---|---|---|
| Thread | تحكم مباشر بالخيط | منخفض المستوى، عرضة للأخطاء، نادراً ما تحتاجه |
| GCD | طوابير إرسال مع closures | بسيط لكن بدون إلغاء، سهل التسبب في انفجار الخيوط |
| OperationQueue | تبعيات المهام، إلغاء، KVO | تحكم أكثر لكن مطول وثقيل |
| Combine | تدفقات تفاعلية | ممتاز لتدفقات الأحداث، منحنى تعليمي حاد |
كل هذه عملت، لكن الأمان كان عليك بالكامل. المترجم لم يستطع المساعدة إذا نسيت الإرسال إلى main، أو إذا طابوران وصلا لنفس البيانات في وقت واحد.
المشكلة: سباقات البيانات
سباق البيانات يحدث عندما يصل خيطان لنفس الذاكرة في نفس الوقت، وواحد منهم على الأقل يكتب:
var count = 0
DispatchQueue.global().async { count += 1 }
DispatchQueue.global().async { count += 1 }
// سلوك غير محدد: تعطل، فساد ذاكرة، أو قيمة خاطئة
سباقات البيانات هي سلوك غير محدد. يمكنها التعطل، إفساد الذاكرة، أو إنتاج نتائج خاطئة بصمت. تطبيقك يعمل جيداً في الاختبار، ثم يتعطل عشوائياً في الإنتاج. الأدوات التقليدية مثل الأقفال والـ semaphores تساعد، لكنها يدوية وعرضة للأخطاء.
التزامن يضخم المشكلة
كلما كان تطبيقك أكثر تزامناً، كلما أصبحت سباقات البيانات أكثر احتمالاً. تطبيق iOS بسيط قد يفلت مع أمان خيوط متساهل. خادم ويب يتعامل مع آلاف الطلبات المتزامنة سيتعطل باستمرار. هذا لماذا أمان Swift في وقت الترجمة يهم أكثر في البيئات عالية التزامن.
التحول: من الخيوط إلى العزل
نموذج التزامن في Swift يسأل سؤالاً مختلفاً. بدلاً من "على أي خيط يجب أن يعمل هذا؟"، يسأل: "من المسموح له بالوصول لهذه البيانات؟"
هذا هو العزل. بدلاً من إرسال العمل يدوياً للخيوط، تعلن حدوداً حول البيانات. المترجم يفرض هذه الحدود في وقت البناء، ليس وقت التشغيل.
خلف الكواليس
التزامن في Swift مبني فوق libdispatch (نفس runtime مثل GCD). الفرق هو طبقة وقت الترجمة: الـ actors والعزل يُفرضان من المترجم، بينما الـ runtime يتعامل مع الجدولة على تجمع خيوط تعاوني محدود بعدد أنوية معالجك.
نطاقات العزل الثلاثة
1. MainActor
@MainActor هو actor عالمي يمثل نطاق عزل الخيط الرئيسي. إنه خاص لأن أطر واجهة المستخدم (UIKit، AppKit، SwiftUI) تتطلب الوصول للخيط الرئيسي.
@MainActor
class ViewModel {
var items: [Item] = [] // محمي بعزل MainActor
}
عندما تضع علامة @MainActor على شيء، أنت لا تقول "أرسل هذا للخيط الرئيسي." أنت تقول "هذا ينتمي لنطاق عزل الـ main actor." المترجم يفرض أن أي شيء يصل إليه يجب أن يكون على MainActor أو يستخدم await لعبور الحدود.
عند الشك، استخدم @MainActor
لمعظم التطبيقات، وضع علامة @MainActor على ViewModels هو الاختيار الصحيح. المخاوف من الأداء عادة مبالغ فيها. ابدأ هنا، حسّن فقط إذا قست مشاكل فعلية.
2. Actors
الـ actor يحمي حالته القابلة للتغيير. يضمن أن قطعة واحدة فقط من الكود يمكنها الوصول لبياناته في وقت واحد:
actor BankAccount {
var balance: Double = 0
func deposit(_ amount: Double) {
balance += amount // آمن: الـ actor يضمن الوصول الحصري
}
}
// من الخارج، يجب أن تنتظر لعبور الحدود
await account.deposit(100)
الـ Actors ليست خيوطاً. الـ actor هو حدود عزل. runtime الـ Swift يقرر أي خيط ينفذ كود الـ actor فعلاً. أنت لا تتحكم بذلك، ولا تحتاج لذلك.
3. Nonisolated
الكود الموسوم بـ nonisolated يخرج من عزل الـ actor. يمكن استدعاؤه من أي مكان بدون await، لكنه لا يستطيع الوصول لحالة الـ actor المحمية:
actor BankAccount {
var balance: Double = 0
nonisolated func bankName() -> String {
"Acme Bank" // لا وصول لحالة الـ actor، آمن للاستدعاء من أي مكان
}
}
let name = account.bankName() // لا حاجة لـ await
Approachable Concurrency: احتكاك أقل
Approachable Concurrency يبسط النموذج الذهني من خلال إعدادين في Xcode:
SWIFT_DEFAULT_ACTOR_ISOLATION=MainActor: كل شيء يعمل على MainActor إلا إذا قلت غير ذلكSWIFT_APPROACHABLE_CONCURRENCY=YES: دوالnonisolatedغير المتزامنة تبقى على actor المستدعي بدلاً من القفز لخيط خلفي
مشاريع Xcode 26 الجديدة تفعّل كليهما افتراضياً. عندما تحتاج عملاً مكثفاً على المعالج بعيداً عن الخيط الرئيسي، استخدم @concurrent.
// يعمل على MainActor (الافتراضي)
func updateUI() async { }
// يعمل على خيط خلفي (اختياري)
@concurrent func processLargeFile() async { }
مبنى المكاتب
فكر في تطبيقك كمبنى مكاتب. كل نطاق عزل هو مكتب خاص بقفل على الباب. شخص واحد فقط يمكنه أن يكون بالداخل في وقت واحد، يعمل مع المستندات في ذلك المكتب.
MainActorهو مكتب الاستقبال - حيث تحدث جميع تفاعلات العملاء. يوجد واحد فقط، ويتعامل مع كل ما يراه المستخدم.- أنواع
actorهي مكاتب الأقسام - المحاسبة، القانونية، الموارد البشرية. كل يحمي مستنداته الحساسة الخاصة. - الكود
nonisolatedهو الممر - مساحة مشتركة يمكن لأي شخص المشي فيها، لكن لا توجد مستندات خاصة هناك.
لا يمكنك فقط الاقتحام لمكتب شخص آخر. تطرق (await) وتنتظر حتى يسمحوا لك بالدخول.
ما الذي يمكنه عبور نطاقات العزل: Sendable
نطاقات العزل تحمي البيانات، لكن في النهاية تحتاج لتمرير البيانات بينها. عندما تفعل، Swift يتحقق إذا كان ذلك آمناً.
فكر في الأمر: إذا مررت مرجعاً لفئة قابلة للتغيير من actor لآخر، كلا الـ actors يمكنهما تعديلها في نفس الوقت. هذا بالضبط سباق البيانات الذي نحاول منعه. لذا Swift يحتاج أن يعرف: هل هذه البيانات يمكن مشاركتها بأمان؟
الجواب هو بروتوكول Sendable. إنه علامة تخبر المترجم "هذا النوع آمن للتمرير عبر حدود العزل":
- أنواع Sendable يمكنها العبور بأمان (أنواع القيمة، البيانات غير القابلة للتغيير، الـ actors)
- أنواع Non-Sendable لا يمكنها (الفئات ذات الحالة القابلة للتغيير)
// Sendable - إنه نوع قيمة، كل مكان يحصل على نسخة
struct User: Sendable {
let id: Int
let name: String
}
// Non-Sendable - إنها فئة ذات حالة قابلة للتغيير
class Counter {
var count = 0 // مكانان يعدلان هذا = كارثة
}
جعل الأنواع Sendable
Swift يستنتج تلقائياً Sendable للعديد من الأنواع:
- Structs و enums مع خصائص
Sendableفقط هي ضمنياًSendable - الـ Actors دائماً
Sendableلأنها تحمي حالتها الخاصة - أنواع
@MainActorهيSendableلأن MainActor يسلسل الوصول
للفئات، الأمر أصعب. فئة يمكنها المطابقة لـ Sendable فقط إذا كانت final وجميع خصائصها المخزنة غير قابلة للتغيير:
final class APIConfig: Sendable {
let baseURL: URL // غير قابل للتغيير
let timeout: Double // غير قابل للتغيير
}
إذا كان لديك فئة آمنة للخيوط بوسائل أخرى (أقفال، atomics)، يمكنك استخدام @unchecked Sendable لتخبر المترجم "ثق بي":
final class ThreadSafeCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Data] = [:]
}
@unchecked Sendable هو وعد
المترجم لن يتحقق من أمان الخيوط. إذا كنت مخطئاً، ستحصل على سباقات بيانات. استخدمه بحذر.
Approachable Concurrency: احتكاك أقل
مع Approachable Concurrency، أخطاء Sendable تصبح أندر بكثير:
- إذا كان الكود لا يعبر حدود العزل، لا تحتاج Sendable
- الدوال غير المتزامنة تبقى على actor المستدعي بدلاً من القفز لخيط خلفي
- المترجم أذكى في اكتشاف متى تُستخدم القيم بأمان
فعّله بتعيين SWIFT_DEFAULT_ACTOR_ISOLATION إلى MainActor وSWIFT_APPROACHABLE_CONCURRENCY إلى YES. مشاريع Xcode 26 الجديدة تفعّل كليهما افتراضياً. عندما تحتاج التوازي، ضع علامة @concurrent على الدوال وعندها فكر في Sendable.
النسخ مقابل المستندات الأصلية
عودة لمبنى المكاتب. عندما تحتاج مشاركة معلومات بين الأقسام:
- النسخ آمنة - إذا صنع القانونية نسخة من مستند وأرسلها للمحاسبة، كلاهما لديه نسخته الخاصة. يمكنهم الكتابة عليها، تعديلها، ما شاءوا. لا تضارب.
- العقود الأصلية الموقعة يجب أن تبقى مكانها - إذا كان بإمكان قسمين كليهما تعديل الأصل، تحدث الفوضى. من لديه النسخة الحقيقية؟
أنواع Sendable مثل النسخ: آمنة للمشاركة لأن كل مكان يحصل على نسخته المستقلة (أنواع القيمة) أو لأنها غير قابلة للتغيير (لا أحد يستطيع تعديلها). أنواع Non-Sendable مثل العقود الأصلية: تمريرها يخلق إمكانية تعديلات متضاربة.
كيف يُورَث العزل
رأيت أن نطاقات العزل تحمي البيانات، وSendable يتحكم فيما يعبر بينها. لكن كيف ينتهي الكود في نطاق عزل في الأصل؟
عندما تستدعي دالة أو تنشئ closure، العزل يتدفق عبر كودك. مع Approachable Concurrency، تطبيقك يبدأ على MainActor، وذلك العزل ينتشر للكود الذي تستدعيه، إلا إذا غيّره شيء صراحةً. فهم هذا التدفق يساعدك على التنبؤ أين يعمل الكود ولماذا المترجم أحياناً يشتكي.
استدعاءات الدوال
عندما تستدعي دالة، عزلها يحدد أين تعمل:
@MainActor func updateUI() { } // دائماً تعمل على MainActor
func helper() { } // ترث عزل المستدعي
@concurrent func crunch() async { } // صراحةً تعمل بعيداً عن الـ actor
مع Approachable Concurrency، معظم كودك يرث عزل MainActor. الدالة تعمل حيث يعمل المستدعي، إلا إذا خرجت صراحةً.
الـ Closures
الـ Closures ترث العزل من السياق الذي عُرّفت فيه:
@MainActor
class ViewModel {
func setup() {
let closure = {
// ترث MainActor من ViewModel
self.updateUI() // آمن، نفس العزل
}
closure()
}
}
هذا لماذا closures الـ Button في SwiftUI يمكنها تحديث @State بأمان: ترث عزل MainActor من الواجهة.
المهام
Task { } ترث عزل الـ actor من حيث أُنشئت:
@MainActor
class ViewModel {
func doWork() {
Task {
// ترث عزل MainActor
self.updateUI() // آمن، لا حاجة لـ await
}
}
}
هذا عادةً ما تريده. المهمة تعمل على نفس الـ actor مثل الكود الذي أنشأها.
كسر الوراثة: Task.detached
أحياناً تريد مهمة لا ترث أي سياق:
@MainActor
class ViewModel {
func doHeavyWork() {
Task.detached {
// لا عزل actor، تعمل على التجمع التعاوني
let result = await self.expensiveCalculation()
await MainActor.run {
self.data = result // قفز صريح للخلف
}
}
}
}
Task و Task.detached نمط سيء
المهام التي تجدولها بـ Task { ... } غير مُدارة. لا توجد طريقة لإلغائها أو معرفة متى تنتهي، إن انتهت أصلاً. لا توجد طريقة للوصول لقيمة الإرجاع أو معرفة إذا واجهت خطأ. في غالبية الحالات، سيكون من الأفضل استخدام مهام مُدارة بواسطة .task أو TaskGroup، كما هو موضح في قسم "الأخطاء الشائعة".
Task.detached يجب أن يكون ملاذك الأخير. المهام المنفصلة لا ترث الأولوية، القيم المحلية للمهمة، أو سياق الـ actor. إذا كنت تحتاج عملاً مكثفاً على المعالج بعيداً عن الـ main actor، ضع علامة @concurrent على الدالة بدلاً من ذلك.
الحفاظ على العزل في أدوات async
أحياناً تكتب دالة async عامة تقبل closure - غلاف، مساعد إعادة المحاولة، نطاق معاملة. المستدعي يمرر closure، ودالتك تنفذها. بسيط، أليس كذلك؟
func measure<T>(
_ label: String,
block: () async throws -> T
) async rethrows -> T {
let start = ContinuousClock.now
let result = try await block()
print("\(label): \(ContinuousClock.now - start)")
return result
}
لكن عندما تستدعي هذا من سياق @MainActor، Swift يشتكي:
ماذا يحدث؟ الـ closure الخاص بك يلتقط حالة من MainActor، لكن measure هي nonisolated. Swift يرى closure غير Sendable يعبر حدود العزل - بالضبط ما صُمم لمنعه.
الحل الأبسط هو nonisolated(nonsending). هذا يخبر Swift أن الدالة يجب أن تبقى على أي منفذ استدعاها:
nonisolated(nonsending)
func measure<T>(
_ label: String,
block: () async throws -> T
) async rethrows -> T {
let start = ContinuousClock.now
let result = try await block()
print("\(label): \(ContinuousClock.now - start)")
return result
}
الآن الدالة بأكملها تعمل على منفذ المستدعي. استدعها من MainActor، تبقى على MainActor. استدعها من actor مخصص، تبقى هناك. الـ closure لا يعبر حدود العزل أبداً، لذا لا حاجة لفحص Sendable.
متى تستخدم كل نهج
nonisolated(nonsending) - الخيار البسيط. فقط أضف السمة. استخدمه عندما تحتاج فقط البقاء على منفذ المستدعي.
isolation: isolated (any Actor)? = #isolation - الخيار الصريح. يضيف معاملاً يعطيك الوصول لمثيل الـ actor. استخدمه عندما تحتاج تمرير سياق العزل لدوال أخرى أو فحص أي actor أنت عليه.
إذا كنت تحتاج وصولاً صريحاً للـ actor، استخدم معامل #isolation بدلاً من ذلك:
func measure<T>(
isolation: isolated (any Actor)? = #isolation,
_ label: String,
block: () async throws -> T
) async rethrows -> T {
let start = ContinuousClock.now
let result = try await block()
print("\(label): \(ContinuousClock.now - start)")
return result
}
كلا النهجين ضروريان لبناء أدوات async تبدو طبيعية عند الاستخدام. بدونهما، سيحتاج المستدعون لجعل closures الخاصة بهم @Sendable أو القفز عبر حلقات لإرضاء المترجم.
المشي عبر المبنى
عندما تكون في مكتب الاستقبال (MainActor)، وتستدعي شخصاً ليساعدك، يأتي إلى مكتبك. يرث موقعك. إذا أنشأت مهمة ("اذهب افعل هذا لي")، ذلك المساعد يبدأ في مكتبك أيضاً.
الطريقة الوحيدة لينتهي شخص في مكتب مختلف هي إذا ذهب صراحةً هناك: "أحتاج العمل في المحاسبة لهذا" (actor)، أو "سأتعامل مع هذا في المكتب الخلفي" (@concurrent).
جمع كل شيء معاً
لنتراجع ونرى كيف تتناسب جميع القطع.
التزامن في Swift قد يشعر بالكثير من المفاهيم: async/await، Task، الـ actors، MainActor، Sendable، نطاقات العزل. لكن هناك فكرة واحدة فقط في المركز: العزل يُورَث افتراضياً.
مع Approachable Concurrency مفعّل، تطبيقك يبدأ على MainActor. هذه نقطة بدايتك. من هناك:
- كل دالة تستدعيها ترث ذلك العزل
- كل closure تنشئها تلتقط ذلك العزل
- كل
Task { }تطلقها ترث ذلك العزل
لا تحتاج لوضع تعليقات توضيحية على أي شيء. لا تحتاج للتفكير في الخيوط. كودك يعمل على MainActor، والعزل ينتشر عبر برنامجك تلقائياً.
عندما تحتاج للخروج من تلك الوراثة، تفعل ذلك صراحةً:
@concurrentتقول "شغّل هذا على خيط خلفي"actorيقول "هذا النوع له نطاق عزل خاص به"Task.detached { }يقول "ابدأ من جديد، لا ترث شيئاً"
وعندما تمرر بيانات بين نطاقات العزل، Swift يتحقق أنها آمنة. هذا ما Sendable لأجله: وضع علامة على الأنواع التي يمكنها العبور بأمان عبر الحدود.
هذا كل شيء. هذا النموذج بالكامل:
- العزل ينتشر من
MainActorعبر كودك - تخرج صراحةً عندما تحتاج عمل خلفي أو حالة منفصلة
- Sendable يحرس الحدود عندما تعبر البيانات بين النطاقات
عندما يشتكي المترجم، يخبرك أن إحدى هذه القواعد انتُهكت. تتبع الوراثة: من أين جاء العزل؟ أين يحاول الكود أن يعمل؟ ما البيانات التي تعبر حدوداً؟ الجواب عادةً واضح بمجرد أن تسأل السؤال الصحيح.
إلى أين من هنا
الأخبار الجيدة: لا تحتاج لإتقان كل شيء دفعة واحدة.
معظم التطبيقات تحتاج فقط الأساسيات. ضع علامة على ViewModels بـ @MainActor، استخدم async/await لاستدعاءات الشبكة، وأنشئ Task { } عندما تحتاج بدء عمل async من نقر زر. هذا كل شيء. هذا يتعامل مع 80% من التطبيقات الحقيقية. المترجم سيخبرك إذا احتجت المزيد.
عندما تحتاج عملاً متوازياً، استخدم async let لجلب عدة أشياء مرة واحدة، أو TaskGroup عندما يكون عدد المهام ديناميكياً. تعلم التعامل مع الإلغاء بلطف. هذا يغطي التطبيقات ذات تحميل البيانات المعقد أو الميزات في الوقت الفعلي.
الأنماط المتقدمة تأتي لاحقاً، إذا أبداً. actors مخصصة للحالة القابلة للتغيير المشتركة، @concurrent للمعالجة المكثفة على المعالج، فهم عميق لـ Sendable. هذا كود الأطر، Swift من جانب الخادم، تطبيقات سطح المكتب المعقدة. معظم المطورين لا يحتاجون هذا المستوى أبداً.
ابدأ بسيطاً
لا تحسّن لمشاكل ليست لديك. ابدأ بالأساسيات، أطلق تطبيقك، وأضف التعقيد فقط عندما تواجه مشاكل حقيقية. المترجم سيرشدك.
احذر: الأخطاء الشائعة
التفكير أن async = خلفية
// هذا لا يزال يحجب الخيط الرئيسي!
@MainActor
func slowFunction() async {
let result = expensiveCalculation() // عمل متزامن = حجب
data = result
}
async تعني "يمكن أن يتوقف مؤقتاً." العمل الفعلي لا يزال يعمل أينما يعمل. استخدم @concurrent (Swift 6.2) أو Task.detached للعمل المكثف على المعالج.
إنشاء actors كثيرة جداً
// مفرط الهندسة
actor NetworkManager { }
actor CacheManager { }
actor DataManager { }
// أفضل - معظم الأشياء يمكنها العيش على MainActor
@MainActor
class AppState { }
تحتاج actor مخصص فقط عندما يكون لديك حالة قابلة للتغيير مشتركة لا يمكنها العيش على MainActor. قاعدة Matt Massicotte: أدخل actor فقط عندما (1) لديك حالة non-Sendable، (2) العمليات على تلك الحالة يجب أن تكون ذرية، و(3) تلك العمليات لا يمكنها العمل على actor موجود. إذا لم تستطع تبريره، استخدم @MainActor بدلاً من ذلك.
جعل كل شيء Sendable
ليس كل شيء يحتاج لعبور الحدود. إذا كنت تضيف @unchecked Sendable في كل مكان، تراجع واسأل إذا كانت البيانات فعلاً تحتاج للانتقال بين نطاقات العزل.
استخدام MainActor.run عندما لا تحتاجه
// غير ضروري
Task {
let data = await fetchData()
await MainActor.run {
self.data = data
}
}
// أفضل - فقط اجعل الدالة @MainActor
@MainActor
func loadData() async {
self.data = await fetchData()
}
MainActor.run نادراً ما يكون الحل الصحيح. إذا كنت تحتاج عزل MainActor، ضع تعليق @MainActor على الدالة بدلاً من ذلك. أوضح والمترجم يمكنه مساعدتك أكثر. شاهد رأي Matt في هذا.
حجب تجمع الخيوط التعاوني
// لا تفعل هذا أبداً - يخاطر بالجمود
func badIdea() async {
let semaphore = DispatchSemaphore(value: 0)
Task {
await doWork()
semaphore.signal()
}
semaphore.wait() // يحجب خيطاً تعاونياً!
}
تجمع الخيوط التعاوني في Swift له خيوط محدودة. حجب واحد بـ DispatchSemaphore أو DispatchGroup.wait() أو استدعاءات مماثلة يمكن أن يسبب جموداً. إذا كنت تحتاج ربط كود متزامن وغير متزامن، استخدم async let أو أعد الهيكلة للبقاء غير متزامن بالكامل.
إنشاء مهام غير مُدارة
المهام التي تنشئها يدوياً بـ Task { ... } أو Task.detached { ... } غير مُدارة. بعد إنشاء مهام غير مُدارة، لا يمكنك التحكم بها. لا يمكنك إلغاؤها إذا تم إلغاء المهمة التي بدأتها منها. لا يمكنك معرفة إذا أنهت عملها، إذا ألقت خطأ، أو جمع قيمة الإرجاع. بدء مثل هذه المهمة يشبه رمي زجاجة في البحر آملاً أن توصل رسالتها لوجهتها، دون رؤية تلك الزجاجة مرة أخرى.
مبنى المكاتب
Task يشبه تكليف موظف بعمل. الموظف يتعامل مع الطلب (بما في ذلك الانتظار لمكاتب أخرى) بينما تستمر في عملك الفوري.
بعد إرسال العمل للموظف، ليس لديك وسيلة للتواصل معها. لا يمكنك إخبارها بالتوقف عن العمل أو معرفة إذا انتهت وما كانت نتيجة ذلك العمل.
ما تريده فعلاً هو إعطاء الموظف جهاز اتصال لاسلكي للتواصل معها أثناء تعاملها مع الطلب. بجهاز الاتصال، يمكنك إخبارها بالتوقف، أو يمكنها إخبارك عندما تواجه خطأ، أو يمكنها الإبلاغ عن نتيجة الطلب الذي أعطيتها إياه.
بدلاً من إنشاء مهام غير مُدارة، استخدم تزامن Swift للحفاظ على التحكم في المهام الفرعية التي تنشئها. استخدم TaskGroup لإدارة (مجموعة من) المهام الفرعية. Swift توفر عدة دوال withTaskGroup() { group in ... } للمساعدة في إنشاء مجموعات المهام.
func doWork() async {
// هذا سيرجع عندما ترجع جميع المهام الفرعية، تلقي خطأ، أو يتم إلغاؤها
let result = try await withThrowingTaskGroup() { group in
group.addTask {
try await self.performAsyncOperation1()
}
group.addTask {
try await self.performAsyncOperation2()
}
// انتظر واجمع نتائج المهام هنا
}
}
func performAsyncOperation1() async throws -> Int {
return 1
}
func performAsyncOperation2() async throws -> Int {
return 2
}
لجمع نتائج المهام الفرعية للمجموعة، يمكنك استخدام حلقة for-await-in:
var sum = 0
for await result in group {
sum += result
}
// sum == 3
يمكنك معرفة المزيد عن TaskGroup في توثيق Swift.
ملاحظة حول المهام وSwiftUI.
عند كتابة واجهة مستخدم، غالباً تريد بدء مهام غير متزامنة من سياق متزامن. مثلاً، تريد تحميل صورة بشكل غير متزامن كاستجابة للمس عنصر واجهة. بدء مهام غير متزامنة من سياق متزامن غير ممكن في Swift. لهذا ترى حلولاً تتضمن Task { ... }، مما يُدخل مهاماً غير مُدارة.
لا يمكنك استخدام TaskGroup من مُعدّل SwiftUI متزامن لأن withTaskGroup() دالة async أيضاً وكذلك دوالها المرتبطة.
كبديل، SwiftUI يوفر مُعدّلاً غير متزامن يمكنك استخدامه لبدء عمليات غير متزامنة. المُعدّل .task { }، الذي ذكرناه سابقاً، يقبل دالة () async -> Void، مثالية لاستدعاء دوال async أخرى. متاح على كل View. يُفعّل قبل ظهور الواجهة والمهام التي ينشئها مُدارة ومرتبطة بدورة حياة الواجهة، مما يعني أن المهام تُلغى عندما تختفي الواجهة.
عودة لمثال اللمس-لتحميل-صورة: بدلاً من إنشاء مهمة غير مُدارة لاستدعاء دالة loadImage() غير المتزامنة من دالة .onTap() { ... } المتزامنة، يمكنك تبديل علَم عند لفتة اللمس واستخدام المُعدّل task(id:) لتحميل الصور بشكل غير متزامن عندما تتغير قيمة id (العلَم).
هنا مثال:
struct ContentView: View {
@State private var shouldLoadImage = false
var body: some View {
Button("اضغط هنا!") {
// بدّل العلَم
shouldLoadImage = !shouldLoadImage
}
// الـ View تدير المهمة الفرعية
// تبدأ قبل عرض الواجهة
// وتتوقف عندما تختفي الواجهة
.task(id: shouldLoadImage) {
// عندما تتغير قيمة العلَم، SwiftUI يعيد تشغيل المهمة
guard shouldLoadImage else { return }
await loadImage()
}
}
}
ورقة الغش: مرجع سريع
| الكلمة المفتاحية | ما تفعله |
|---|---|
async |
الدالة يمكنها التوقف مؤقتاً |
await |
توقف هنا حتى ينتهي |
Task { } |
ابدأ عملاً async، يرث السياق |
Task.detached { } |
ابدأ عملاً async، بدون سياق موروث |
@MainActor |
يعمل على الخيط الرئيسي |
actor |
نوع بحالة قابلة للتغيير معزولة |
nonisolated |
يخرج من عزل الـ actor |
nonisolated(nonsending) |
البقاء على منفذ المستدعي |
Sendable |
آمن للتمرير بين نطاقات العزل |
@concurrent |
دائماً يعمل في الخلفية (Swift 6.2+) |
#isolation |
التقاط عزل المستدعي كمعامل |
async let |
ابدأ عملاً متوازياً |
TaskGroup |
عمل متوازي ديناميكي |
قراءة إضافية
مدونة Matt Massicotte (موصى به بشدة)
- مسرد تزامن Swift - المصطلحات الأساسية
- مقدمة للعزل - المفهوم الأساسي
- متى يجب استخدام actor؟ - إرشادات عملية
- أنواع Non-Sendable رائعة أيضاً - لماذا الأبسط أفضل
موارد Apple الرسمية
أدوات
- Tuist - طوّر أسرع مع فرق ومشاريع أكبر
مهارة وكيل الذكاء الاصطناعي
هل تريد أن يفهم مساعد البرمجة بالذكاء الاصطناعي Swift Concurrency؟ نقدم ملف SKILL.md الذي يحزم هذه النماذج الذهنية لوكلاء الذكاء الاصطناعي مثل Claude Code و Codex و Amp و OpenCode وغيرها.
مهارات أخرى
ما هي المهارة؟
المهارة هي ملف markdown يعلّم وكلاء البرمجة بالذكاء الاصطناعي معرفة متخصصة. عندما تضيف مهارة Swift Concurrency إلى وكيلك، فإنه يطبق هذه المفاهيم تلقائياً عند مساعدتك في كتابة كود Swift غير متزامن.
كيفية الاستخدام
اختر وكيلك ونفّذ الأوامر:
# مهارة شخصية (جميع مشاريعك)
mkdir -p ~/.claude/skills/swift-concurrency
curl -o ~/.claude/skills/swift-concurrency/SKILL.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# مهارة المشروع (هذا المشروع فقط)
mkdir -p .claude/skills/swift-concurrency
curl -o .claude/skills/swift-concurrency/SKILL.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# تعليمات عامة (جميع مشاريعك)
curl -o ~/.codex/AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# تعليمات المشروع (هذا المشروع فقط)
curl -o AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# تعليمات المشروع (موصى به)
curl -o AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# قواعد عامة (جميع مشاريعك)
mkdir -p ~/.config/opencode
curl -o ~/.config/opencode/AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
# قواعد المشروع (هذا المشروع فقط)
curl -o AGENTS.md https://fuckingapproachableswiftconcurrency.com/SKILL.md
تتضمن المهارة تشبيه مبنى المكاتب، وأنماط العزل، ودليل Sendable، والأخطاء الشائعة، وجداول المرجع السريع. سيستخدم وكيلك هذه المعرفة تلقائياً عند العمل مع كود Swift Concurrency.