دليل Swift للتزامن
بطريقة سهلة جداً
افهم أخيراً async/await و actors و Sendable. نماذج ذهنية واضحة، بدون مصطلحات معقدة.
شكر كبير لـ Matt Massicotte لجعل التزامن في Swift مفهوماً. من إعداد Pedro Piñera. وجدت مشكلة؟ [email protected]
في تقليد fuckingblocksyntax.com و fuckingifcaseletsyntax.com
وسّع تطويرك مع Tuist
الحقيقة الصريحة
لا توجد ورقة غش للتزامن في Swift. كل إجابة "فقط افعل X" خاطئة في بعض السياقات.
لكن هذه هي الأخبار الجيدة: بمجرد أن تفهم العزل (قراءة 5 دقائق)، كل شيء يصبح واضحاً. أخطاء المترجم تبدأ في أن تكون منطقية. تتوقف عن محاربة النظام وتبدأ في العمل معه.
يستهدف هذا الدليل Swift 6+. معظم المفاهيم تنطبق على Swift 5.5+، لكن Swift 6 يفرض فحصاً أكثر صرامة للتزامن.
الشيء الوحيد الذي تحتاج إلى فهمه
العزل هو المفتاح لكل شيء. إنه إجابة Swift على السؤال: من يُسمح له بلمس هذه البيانات الآن؟
مبنى المكاتب
فكر في تطبيقك كـ مبنى مكاتب. كل مكتب هو نطاق عزل - مساحة خاصة حيث يمكن لشخص واحد فقط العمل في وقت واحد. لا يمكنك فقط الدخول إلى مكتب شخص آخر والبدء في إعادة ترتيب مكتبه.
سنبني على هذه الاستعارة طوال الدليل.
لماذا ليس فقط الخيوط؟
لعقود، كتبنا الكود المتزامن بالتفكير في الخيوط. المشكلة؟ الخيوط لا تمنعك من إطلاق النار على قدمك. يمكن لخيطين الوصول إلى نفس البيانات في نفس الوقت، مما يتسبب في سباقات البيانات - أخطاء تتعطل بشكل عشوائي ويكاد يكون من المستحيل إعادة إنتاجها.
على الهاتف، قد تفلت من ذلك. على الخادم الذي يتعامل مع آلاف الطلبات المتزامنة، تصبح سباقات البيانات أمراً مؤكداً - عادة ما تظهر في الإنتاج، يوم الجمعة. مع توسع Swift إلى الخوادم وبيئات أخرى عالية التزامن، "الأمل في الأفضل" لا يكفي.
النهج القديم كان دفاعياً: استخدام الأقفال، طوابير الإرسال، الأمل في أنك لم تفوت نقطة.
نهج Swift مختلف: اجعل سباقات البيانات مستحيلة في وقت الترجمة. بدلاً من السؤال "على أي خيط هذا؟"، يسأل Swift "من يُسمح له بلمس هذه البيانات الآن؟" هذا هو العزل.
كيف تتعامل اللغات الأخرى مع هذا
| اللغة | النهج | متى تكتشف الأخطاء |
|---|---|---|
| Swift | العزل + Sendable | وقت الترجمة |
| Rust | الملكية + مدقق الاستعارة | وقت الترجمة |
| Go | القنوات + كاشف السباق | وقت التشغيل (مع الأدوات) |
| Java/Kotlin | synchronized، الأقفال |
وقت التشغيل (التعطل) |
| JavaScript | حلقة أحداث أحادية الخيط | يتم تجنبه تماماً |
| C/C++ | أقفال يدوية | وقت التشغيل (سلوك غير محدد) |
Swift و Rust هما اللغتان الرئيسيتان الوحيدتان اللتان تكتشفان سباقات البيانات في وقت الترجمة. المقايضة؟ منحنى تعليمي أكثر انحداراً في البداية. لكن بمجرد أن تفهم النموذج، المترجم يدعمك.
تلك الأخطاء المزعجة حول Sendable وعزل الـ actor؟ إنها تكتشف الأخطاء التي كانت ستكون تعطلات صامتة من قبل.
نطاقات العزل
الآن بعد أن فهمت العزل (المكاتب الخاصة)، لنلقِ نظرة على الأنواع المختلفة من المكاتب في مبنى Swift.
مبنى المكاتب
- مكتب الاستقبال (
MainActor) - حيث تحدث جميع تفاعلات العملاء. يوجد واحد فقط، ويتعامل مع كل ما يراه المستخدم. - مكاتب الأقسام (
actor) - المحاسبة، القانونية، الموارد البشرية. كل قسم له مكتبه الخاص الذي يحمي بياناته الحساسة الخاصة. - الممرات والمناطق المشتركة (
nonisolated) - مساحات مشتركة يمكن لأي شخص المشي من خلالها. لا توجد بيانات خاصة هنا.
MainActor: مكتب الاستقبال
MainActor هو نطاق عزل خاص يعمل على الخيط الرئيسي. إنه المكان الذي يحدث فيه كل عمل واجهة المستخدم.
@MainActor
@Observable
class ViewModel {
var items: [Item] = [] // حالة واجهة المستخدم تعيش هنا
func refresh() async {
let newItems = await fetchItems()
self.items = newItems // آمن - نحن على MainActor
}
}
عند الشك، استخدم MainActor
بالنسبة لمعظم التطبيقات، وضع علامة على ViewModels والفئات المتعلقة بواجهة المستخدم بـ @MainActor هو الاختيار الصحيح. المخاوف المتعلقة بالأداء عادة ما تكون مبالغ فيها - ابدأ هنا، قم بالتحسين فقط إذا قست مشاكل فعلية.
Actors: مكاتب الأقسام
actor يشبه مكتب القسم - يحمي بياناته الخاصة ويسمح فقط بزائر واحد في كل مرة.
actor BankAccount {
var balance: Double = 0
func deposit(_ amount: Double) {
balance += amount // آمن! متصل واحد فقط في كل مرة
}
}
بدون actors، يقرأ خيطان الرصيد = 100، كلاهما يضيف 50، كلاهما يكتب 150 - فقدت 50 دولاراً. مع actors، يقوم Swift تلقائياً بوضع الوصول في قائمة الانتظار وكلا الإيداعين يكتملان بشكل صحيح.
لا تفرط في استخدام actors
تحتاج إلى actor مخصص فقط عندما تكون جميع هذه الشروط الأربعة صحيحة:
- لديك حالة قابلة للتغيير غير Sendable (غير آمنة للخيوط)
- أماكن متعددة تحتاج إلى الوصول إليها
- يجب أن تكون العمليات على تلك الحالة ذرية
- لا يمكنها العيش فقط على MainActor
إذا كان أي شرط خاطئاً، فمن المحتمل أنك لا تحتاج إلى actor. يمكن لمعظم حالات واجهة المستخدم العيش على @MainActor. اقرأ المزيد عن متى تستخدم actors.
Nonisolated: الممرات
الكود الموسوم بـ nonisolated يشبه الممرات - لا ينتمي إلى أي مكتب ويمكن الوصول إليه من أي مكان.
actor UserSession {
let userId: String // غير قابل للتغيير - آمن للقراءة من أي مكان
var lastActivity: Date // قابل للتغيير - يحتاج إلى حماية actor
nonisolated var displayId: String {
"User: \(userId)" // يقرأ فقط البيانات غير القابلة للتغيير
}
}
// الاستخدام - لا حاجة لـ await لـ nonisolated
let session = UserSession(userId: "123")
print(session.displayId) // يعمل بشكل متزامن!
استخدم nonisolated للخصائص المحسوبة التي تقرأ فقط البيانات غير القابلة للتغيير.
كيف ينتشر العزل
عندما تضع علامة على نوع بعزل actor، ماذا يحدث لأساليبه؟ ماذا عن الإغلاقات؟ فهم كيفية انتشار العزل هو مفتاح تجنب المفاجآت.
مبنى المكاتب
عندما يتم توظيفك في قسم، تعمل في مكتب ذلك القسم افتراضياً. إذا قام قسم التسويق بتوظيفك، فأنت لا تظهر بشكل عشوائي في المحاسبة.
وبالمثل، عندما يتم تعريف دالة داخل فئة @MainActor، فإنها ترث ذلك العزل. إنها "تعمل في نفس المكتب" مثل والدها.
الفئات ترث عزلها
@MainActor
class ViewModel {
var count = 0 // معزول بـ MainActor
func increment() { // أيضاً معزول بـ MainActor
count += 1
}
}
كل شيء داخل الفئة يرث @MainActor. لا تحتاج إلى وضع علامة على كل أسلوب.
المهام ترث السياق (عادةً)
@MainActor
class ViewModel {
func doWork() {
Task {
// هذا يرث MainActor!
self.updateUI() // آمن، لا حاجة لـ await
}
}
}
Task { } الذي تم إنشاؤه من سياق @MainActor يبقى على MainActor. هذا عادة ما تريده.
Task.detached يكسر الميراث
@MainActor
class ViewModel {
func doWork() {
Task.detached {
// لم يعد على MainActor!
await self.updateUI() // تحتاج await الآن
}
}
}
مبنى المكاتب
Task.detached يشبه توظيف مقاول خارجي. ليس لديهم بطاقة لمكتبك - يعملون من مساحتهم الخاصة ويجب أن يمروا عبر القنوات المناسبة للوصول إلى أشيائك.
Task.detached عادة ما يكون خاطئاً
في معظم الأحيان، تريد Task عادياً. المهام المنفصلة لا ترث الأولوية، أو القيم المحلية للمهمة، أو سياق actor. استخدمها فقط عندما تحتاج صراحةً إلى ذلك الفصل.
ما الذي يمكن أن يعبر الحدود
الآن بعد أن عرفت نطاقات العزل (المكاتب) وكيف تنتشر، السؤال التالي هو: ماذا يمكنك تمرير بينها؟
مبنى المكاتب
ليس كل شيء يمكن أن يغادر المكتب:
- النسخ آمنة للمشاركة - إذا قام القسم القانوني بعمل نسخة من مستند وإرساله إلى المحاسبة، فكلاهما لديه نسخته الخاصة. لا يوجد تضارب.
- العقود الأصلية الموقعة يجب أن تبقى في مكانها - إذا كان بإمكان قسمين كليهما تعديل الأصل، ينتج الفوضى.
بمصطلحات Swift: أنواع Sendable هي نسخ (آمنة للمشاركة)، أنواع non-Sendable هي الأصول (يجب أن تبقى في مكتب واحد).
Sendable: آمن للمشاركة
هذه الأنواع يمكن أن تعبر حدود العزل بأمان:
// الهياكل مع بيانات غير قابلة للتغيير - مثل النسخ
struct User: Sendable {
let id: Int
let name: String
}
// Actors تحمي نفسها - يتعاملون مع زوارهم الخاصين
actor BankAccount { } // تلقائياً Sendable
تلقائياً Sendable:
- أنواع القيمة (structs، enums) مع خصائص Sendable
- Actors (يحمون أنفسهم)
- فئات غير قابلة للتغيير (
final classمع خصائصletفقط)
Non-Sendable: يجب أن يبقى في مكانه
هذه الأنواع لا يمكن أن تعبر الحدود بأمان:
// فئات ذات حالة قابلة للتغيير - مثل المستندات الأصلية
class Counter {
var count = 0 // مكتبان يعدلان هذا = كارثة
}
لماذا هذا هو التمييز الرئيسي؟ لأن كل خطأ مترجم ستواجهه يتلخص في: "أنت تحاول إرسال نوع non-Sendable عبر حدود العزل."
عندما يشتكي المترجم
إذا قال Swift أن شيئاً ما ليس Sendable، لديك خيارات:
- اجعله نوع قيمة - استخدم
structبدلاً منclass - اعزله - أبقه على
@MainActorحتى لا يحتاج إلى العبور - أبقه non-Sendable - فقط لا تمرره بين المكاتب
- الملاذ الأخير:
@unchecked Sendable- أنت تعد أنه آمن (كن حذراً)
ابدأ بـ non-Sendable
يدافع Matt Massicotte عن البدء بأنواع عادية، non-Sendable. أضف Sendable فقط عندما تحتاج إلى عبور الحدود. يبقى نوع non-Sendable بسيطاً ويتجنب صداع المطابقة.
كيفية عبور الحدود
تفهم نطاقات العزل، تعرف ما يمكن أن يعبرها. الآن: كيف تتواصل فعلياً بين المكاتب؟
مبنى المكاتب
لا يمكنك فقط الدخول إلى مكتب آخر. ترسل طلباً وتنتظر استجابة. قد تعمل على أشياء أخرى أثناء الانتظار، لكنك تحتاج إلى تلك الاستجابة قبل أن تتمكن من المتابعة.
هذا هو async/await - إرسال طلب إلى نطاق عزل آخر والتوقف مؤقتاً حتى تحصل على إجابة.
كلمة await
عندما تستدعي دالة على actor آخر، تحتاج إلى await:
actor DataStore {
var items: [Item] = []
func add(_ item: Item) {
items.append(item)
}
}
@MainActor
class ViewModel {
let store = DataStore()
func addItem(_ item: Item) async {
await store.add(item) // طلب إلى مكتب آخر
updateUI() // عودة إلى مكتبنا
}
}
await تعني: "أرسل هذا الطلب وتوقف مؤقتاً حتى ينتهي. قد أقوم بعمل آخر أثناء الانتظار."
التعليق، وليس الحظر
مفهوم خاطئ شائع
يفترض العديد من المطورين أن إضافة async تجعل الكود يعمل في الخلفية. إنها لا تفعل. الكلمة الرئيسية async تعني فقط أن الدالة يمكن أن تتوقف مؤقتاً. لا تقول شيئاً عن أين يعمل.
الفكرة الرئيسية هي الفرق بين الحظر و التعليق:
- الحظر: تجلس في غرفة الانتظار تحدق في الجدار. لا شيء آخر يحدث.
- التعليق: تترك رقم هاتفك وتقوم بالمهام. سيتصلون عندما يكونون جاهزين.
// الخيط يجلس خاملاً، لا يفعل شيئاً لمدة 5 ثوانٍ
Thread.sleep(forTimeInterval: 5)
// يتم تحرير الخيط للقيام بعمل آخر أثناء الانتظار
try await Task.sleep(for: .seconds(5))
بدء العمل غير المتزامن من الكود المتزامن
في بعض الأحيان تكون في كود متزامن وتحتاج إلى استدعاء شيء غير متزامن. استخدم Task:
@MainActor
class ViewModel {
func buttonTapped() { // دالة متزامنة
Task {
await loadData() // الآن يمكننا استخدام await
}
}
}
مبنى المكاتب
Task يشبه تعيين العمل لموظف. يتعامل الموظف مع الطلب (بما في ذلك الانتظار للمكاتب الأخرى) بينما تواصل عملك الفوري.
الأنماط التي تعمل
نمط طلب الشبكة
@MainActor
@Observable
class ViewModel {
var users: [User] = []
var isLoading = false
func fetchUsers() async {
isLoading = true
// هذا يتوقف مؤقتاً - الخيط حر للقيام بعمل آخر
let users = await networkService.getUsers()
// عودة على MainActor تلقائياً
self.users = users
isLoading = false
}
}
لا DispatchQueue.main.async. سمة @MainActor تتعامل معها.
العمل المتوازي مع async let
func loadProfile() async -> Profile {
async let avatar = loadImage("avatar.jpg")
async let banner = loadImage("banner.jpg")
async let details = loadUserDetails()
// الثلاثة جميعاً يعملون بالتوازي!
return Profile(
avatar: await avatar,
banner: await banner,
details: await details
)
}
منع النقر المزدوج
هذا النمط يأتي من دليل Matt Massicotte حول الأنظمة ذات الحالة:
@MainActor
class ButtonViewModel {
private var isLoading = false
func buttonTapped() {
// احمِ بشكل متزامن قبل أي عمل غير متزامن
guard !isLoading else { return }
isLoading = true
Task {
await doExpensiveWork()
isLoading = false
}
}
}
حرج: يجب أن يكون الحارس متزامناً
إذا وضعت الحارس داخل Task بعد await، هناك نافذة حيث يمكن لنقرتين على الزر أن يبدآ العمل كلاهما. تعلم المزيد عن الترتيب والتزامن.
الأخطاء الشائعة التي يجب تجنبها
هذه هي الأخطاء الشائعة التي يرتكبها حتى المطورون المتمرسون:
التفكير في أن async = الخلفية
مبنى المكاتب
إضافة async لا تنقلك إلى مكتب مختلف. أنت لا تزال في مكتب الاستقبال - يمكنك فقط الانتظار للتسليمات الآن دون التجمد في مكانك.
// هذا لا يزال يحظر الخيط الرئيسي!
@MainActor
func slowFunction() async {
let result = expensiveCalculation() // متزامن = حظر
data = result
}
إذا كنت بحاجة إلى عمل في مكتب آخر، أرسله هناك صراحةً:
func slowFunction() async {
let result = await Task.detached {
expensiveCalculation() // الآن في مكتب مختلف
}.value
await MainActor.run { data = result }
}
إنشاء actors كثيرة جداً
مبنى المكاتب
إنشاء مكتب جديد لكل قطعة من البيانات يعني أعمال ورقية لا نهاية لها للتواصل بينها. يمكن أن يحدث معظم عملك في مكتب الاستقبال.
// مفرط الهندسة - كل استدعاء يتطلب المشي بين المكاتب
actor NetworkManager { }
actor CacheManager { }
actor DataManager { }
// أفضل - معظم الأشياء يمكن أن تعيش في مكتب الاستقبال
@MainActor
class AppState { }
استخدام MainActor.run في كل مكان
مبنى المكاتب
إذا كنت تستمر في المشي إلى مكتب الاستقبال لكل شيء صغير، فقط اعمل هناك. اجعله جزءاً من وصفك الوظيفي، وليس مهمة مستمرة.
// لا تفعل هذا - تمشي باستمرار إلى مكتب الاستقبال
await MainActor.run { doMainActorStuff() }
// افعل هذا - فقط اعمل في مكتب الاستقبال
@MainActor func doMainActorStuff() { }
جعل كل شيء Sendable
ليس كل شيء يحتاج إلى أن يكون Sendable. إذا كنت تضيف @unchecked Sendable في كل مكان، فأنت تصنع نسخاً من الأشياء التي لا تحتاج إلى مغادرة المكتب.
تجاهل تحذيرات المترجم
كل تحذير من المترجم حول Sendable هو الحارس الأمني يخبرك أن شيئاً ما ليس آمناً للحمل بين المكاتب. لا تتجاهلها - افهمها.
أخطاء المترجم الشائعة
هذه هي رسائل الخطأ الفعلية التي سترى. كل واحد هو المترجم يحميك من سباق البيانات.
"Sending 'self.foo' risks causing data races"
مبنى المكاتب
أنت تحاول حمل مستند أصلي إلى مكتب آخر. إما اصنع نسخة (Sendable) أو أبقه في مكان واحد.
الإصلاح 1: استخدم struct بدلاً من class
الإصلاح 2: أبقه على actor واحد:
@MainActor
class MyClass {
var foo: SomeType // يبقى في مكتب الاستقبال
}
"Non-sendable type cannot cross actor boundary"
مبنى المكاتب
أنت تحاول حمل أصل بين المكاتب. الحارس الأمني أوقفك.
الإصلاح 1: اجعله struct:
// قبل: class (non-Sendable)
class User { var name: String }
// بعد: struct (Sendable)
struct User: Sendable { let name: String }
الإصلاح 2: اعزله إلى actor واحد:
@MainActor
class User { var name: String }
"Actor-isolated property cannot be referenced"
مبنى المكاتب
أنت تحاول الوصول إلى خزانة ملفات مكتب آخر دون المرور عبر القنوات المناسبة.
الإصلاح: استخدم await:
// خطأ - الوصول مباشرة
let value = myActor.balance
// صحيح - طلب مناسب
let value = await myActor.balance
"Call to main actor-isolated method in synchronous context"
مبنى المكاتب
أنت تحاول استخدام مكتب الاستقبال دون الانتظار في الطابور.
الإصلاح 1: اجعل المتصل @MainActor:
@MainActor
func doSomething() {
updateUI() // نفس العزل، لا حاجة لـ await
}
الإصلاح 2: استخدم await:
func doSomething() async {
await updateUI()
}
ثلاثة مستويات من التزامن في Swift
لا تحتاج إلى تعلم كل شيء دفعة واحدة. تقدم عبر هذه المستويات:
مبنى المكاتب
فكر في الأمر كنمو شركة. لا تبدأ بمقر من 50 طابقاً - تبدأ بمكتب.
هذه المستويات ليست حدوداً صارمة - أجزاء مختلفة من تطبيقك قد تحتاج مستويات مختلفة. تطبيق في الغالب من المستوى 1 قد يحتوي على ميزة واحدة تحتاج إلى أنماط المستوى 2. هذا جيد. استخدم النهج الأبسط الذي يعمل لكل قطعة.
المستوى 1: الشركة الناشئة
الجميع يعمل في مكتب الاستقبال. بسيط، مباشر، بدون بيروقراطية.
- استخدم
async/awaitلاستدعاءات الشبكة - ضع علامة على فئات واجهة المستخدم بـ
@MainActor - استخدم معدّل
.taskفي SwiftUI
هذا يتعامل مع 80٪ من التطبيقات. التطبيقات مثل Things، Bear، Flighty، أو Day One تقع على الأرجح في هذه الفئة - تطبيقات تجلب البيانات وتعرضها في المقام الأول.
المستوى 2: الشركة المتنامية
تحتاج إلى التعامل مع أشياء متعددة في وقت واحد. حان الوقت للمشاريع المتوازية وتنسيق الفرق.
- استخدم
async letللعمل المتوازي - استخدم
TaskGroupللتوازي الديناميكي - افهم إلغاء المهام
التطبيقات مثل Ivory/Ice Cubes (عملاء Mastodon يديرون خطوطاً زمنية متعددة وتحديثات مباشرة)، Overcast (تنسيق التنزيلات والتشغيل والمزامنة في الخلفية)، أو Slack (المراسلة في الوقت الفعلي عبر قنوات متعددة) قد تستخدم هذه الأنماط لميزات معينة.
المستوى 3: المؤسسة
أقسام مخصصة بسياساتها الخاصة. اتصال معقد بين المكاتب.
- إنشاء actors مخصصة للحالة المشتركة
- فهم عميق لـ Sendable
- منفذون مخصصون
التطبيقات مثل Xcode، Final Cut Pro، أو أطر Swift من جانب الخادم مثل Vapor و Hummingbird تحتاج على الأرجح إلى هذه الأنماط - حالة مشتركة معقدة، آلاف الاتصالات المتزامنة، أو كود على مستوى الإطار يبني عليه الآخرون.
ابدأ بسيطاً
معظم التطبيقات لا تحتاج أبداً إلى المستوى 3. لا تبني مؤسسة عندما تكفي شركة ناشئة.
المسرد: المزيد من الكلمات الرئيسية التي ستواجهها
بالإضافة إلى المفاهيم الأساسية، إليك الكلمات الرئيسية الأخرى لتزامن Swift التي سترى في البرية:
| الكلمة الرئيسية | ما تعنيه |
|---|---|
nonisolated |
يلغي عزل actor - يعمل بدون حماية |
isolated |
يعلن صراحةً أن معلمة تعمل في سياق actor |
@Sendable |
يضع علامة على إغلاق كآمن للتمرير عبر حدود العزل |
Task.detached |
ينشئ مهمة منفصلة تماماً عن السياق الحالي |
AsyncSequence |
تسلسل يمكنك التكرار عليه باستخدام for await |
AsyncStream |
طريقة لربط الكود المعتمد على callback بـ async sequences |
withCheckedContinuation |
يربط معالجات الإكمال بـ async/await |
Task.isCancelled |
تحقق مما إذا تم إلغاء المهمة الحالية |
@preconcurrency |
يكتم تحذيرات التزامن للكود القديم |
GlobalActor |
بروتوكول لإنشاء actors مخصصة مثل MainActor |
متى تستخدم كل واحد
nonisolated - قراءة الخصائص المحسوبة
افتراضياً، كل شيء داخل actor معزول - تحتاج إلى await للوصول إليه. لكن في بعض الأحيان لديك خصائص آمنة بطبيعتها للقراءة: ثوابت let غير قابلة للتغيير، أو خصائص محسوبة تستمد فقط القيم من بيانات آمنة أخرى. وضع علامة على هذه بـ nonisolated يسمح للمتصلين بالوصول إليها بشكل متزامن، مما يتجنب النفقات غير الضرورية للـ async.
actor UserSession {
let userId: String // غير قابل للتغيير، آمن للقراءة
var lastActivity: Date // قابل للتغيير، يحتاج حماية
// يمكن استدعاء هذا بدون await
nonisolated var displayId: String {
"User: \(userId)" // يقرأ فقط البيانات غير القابلة للتغيير
}
}
// الاستخدام
let session = UserSession(userId: "123")
print(session.displayId) // لا حاجة لـ await!
@Sendable - الإغلاقات التي تعبر الحدود
عندما يهرب إغلاق ليعمل لاحقاً أو على نطاق عزل مختلف، يحتاج Swift إلى ضمان أنه لن يسبب سباقات البيانات. سمة @Sendable تضع علامة على الإغلاقات الآمنة للتمرير عبر الحدود - لا يمكنها التقاط حالة قابلة للتغيير بشكل غير آمن. Swift غالباً ما يستنتج هذا تلقائياً (مثل مع Task.detached)، لكن في بعض الأحيان تحتاج إلى إعلانها صراحةً عند تصميم APIs تقبل الإغلاقات.
@MainActor
class ViewModel {
var items: [Item] = []
func processInBackground() {
Task.detached {
// هذا الإغلاق يعبر من مهمة منفصلة إلى MainActor
// يجب أن يكون @Sendable (Swift يستنتج هذا)
let processed = await self.heavyProcessing()
await MainActor.run {
self.items = processed
}
}
}
}
// @Sendable صريح عند الحاجة
func runLater(_ work: @Sendable @escaping () -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
work()
}
}
withCheckedContinuation - ربط APIs القديمة
العديد من APIs الأقدم تستخدم معالجات الإكمال بدلاً من async/await. بدلاً من إعادة كتابتها بالكامل، يمكنك لفها باستخدام withCheckedContinuation. هذه الدالة تعلق المهمة الحالية، تعطيك كائن استمرارية، وتستأنف عندما تستدعي continuation.resume(). البديل "checked" يلتقط أخطاء البرمجة مثل الاستئناف مرتين أو عدم الاستئناف على الإطلاق.
// API قديمة معتمدة على callback
func fetchUser(id: String, completion: @escaping (User?) -> Void) {
// ... استدعاء الشبكة مع callback
}
// ملفوفة كـ async
func fetchUser(id: String) async -> User? {
await withCheckedContinuation { continuation in
fetchUser(id: id) { user in
continuation.resume(returning: user) // يربط مرة أخرى!
}
}
}
للدوال التي ترمي، استخدم withCheckedThrowingContinuation:
func fetchUserThrowing(id: String) async throws -> User {
try await withCheckedThrowingContinuation { continuation in
fetchUser(id: id) { result in
switch result {
case .success(let user):
continuation.resume(returning: user)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
AsyncStream - ربط مصادر الأحداث
بينما تتعامل withCheckedContinuation مع callbacks لمرة واحدة، تقدم العديد من APIs قيماً متعددة بمرور الوقت - أساليب delegate، NotificationCenter، أو أنظمة أحداث مخصصة. AsyncStream يربط هذه بـ AsyncSequence في Swift، مما يتيح لك استخدام حلقات for await. تنشئ تياراً، تخزن استمراريته، وتستدعي yield() في كل مرة تصل قيمة جديدة.
class LocationTracker: NSObject, CLLocationManagerDelegate {
private var continuation: AsyncStream<CLLocation>.Continuation?
var locations: AsyncStream<CLLocation> {
AsyncStream { continuation in
self.continuation = continuation
}
}
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
for location in locations {
continuation?.yield(location)
}
}
}
// الاستخدام
let tracker = LocationTracker()
for await location in tracker.locations {
print("موقع جديد: \(location)")
}
Task.isCancelled - الإلغاء التعاوني
يستخدم Swift الإلغاء التعاوني - عندما يتم إلغاء مهمة، لا تتوقف على الفور. بدلاً من ذلك، يتم تعيين علامة، ومسؤوليتك التحقق منها بشكل دوري. هذا يمنحك السيطرة على التنظيف والنتائج الجزئية. استخدم Task.checkCancellation() لرمي على الفور، أو تحقق من Task.isCancelled عندما تريد التعامل مع الإلغاء بلطف (مثل إرجاع نتائج جزئية).
func processLargeDataset(_ items: [Item]) async throws -> [Result] {
var results: [Result] = []
for item in items {
// تحقق قبل كل عملية مكلفة
try Task.checkCancellation() // يرمي إذا تم الإلغاء
// أو تحقق بدون رمي
if Task.isCancelled {
return results // إرجاع النتائج الجزئية
}
let result = await process(item)
results.append(result)
}
return results
}
Task.detached - الهروب من السياق الحالي
Task { } عادي يرث سياق actor الحالي - إذا كنت على @MainActor، تعمل المهمة على @MainActor. في بعض الأحيان هذا ليس ما تريده، خاصة للعمل المكثف على المعالج الذي سيحظر واجهة المستخدم. Task.detached ينشئ مهمة بدون سياق موروث، تعمل على منفذ في الخلفية. استخدمها بشكل متحفظ رغم ذلك - في معظم الأحيان، Task عادي مع نقاط await مناسبة كافٍ وأسهل للتفكير فيه.
@MainActor
class ImageProcessor {
func processImage(_ image: UIImage) {
// لا تفعل: هذا لا يزال يرث سياق MainActor
Task {
let filtered = applyFilters(image) // يحظر main!
}
// افعل: مهمة منفصلة تعمل بشكل مستقل
Task.detached(priority: .userInitiated) {
let filtered = await self.applyFilters(image)
await MainActor.run {
self.displayImage(filtered)
}
}
}
}
Task.detached عادة ما يكون خاطئاً
في معظم الأحيان، تريد Task عادياً. المهام المنفصلة لا ترث الأولوية، أو القيم المحلية للمهمة، أو سياق actor. استخدمها فقط عندما تحتاج صراحةً إلى ذلك الفصل.
@preconcurrency - التعايش مع الكود القديم
اكتم التحذيرات عند استيراد الوحدات التي لم يتم تحديثها بعد للتزامن:
// كتم التحذيرات من هذا الاستيراد
@preconcurrency import OldFramework
// أو على مطابقة بروتوكول
class MyDelegate: @preconcurrency SomeOldDelegate {
// لن يحذر بشأن متطلبات non-Sendable
}
@preconcurrency مؤقت
استخدمه كجسر أثناء تحديث الكود. الهدف هو إزالته في النهاية والحصول على مطابقة Sendable مناسبة.
قراءة إضافية
يقطر هذا الدليل أفضل الموارد حول تزامن Swift.
مدونة Matt Massicotte (موصى به بشدة)
- مسرد التزامن في Swift - المصطلحات الأساسية
- مقدمة للعزل - المفهوم الأساسي
- متى يجب عليك استخدام actor؟ - إرشادات عملية
- أنواع Non-Sendable رائعة أيضاً - لماذا الأبسط أفضل
- عبور الحدود - العمل مع أنواع non-Sendable
- أنماط التزامن في Swift المشكلة - ما يجب تجنبه
- ارتكاب الأخطاء مع التزامن في Swift - التعلم من الأخطاء