سلسلة Swift Concurrency 03|كيفية فهم المهمة وTask.detached وMainActor
غالبًا ما يظهرون معًا، لكنهم يجيبون على ثلاثة أسئلة على التوالي: "من أين تأتي المهمة، ومن يرتبط بها، ومن يغير واجهة المستخدم؟"
عندما اتصلت لأول مرة بـ Swift Concurrency، كنت في حيرة من أمري بشأن Task وTask.detached وMainActor:
- كل ما يتعلق غير متزامن
- غالبًا ما تظهر في نفس الجزء من التعليمات البرمجية
- كل شيء يبدو مثل “دع الكود يعمل في مكان ما”
لذا فإن الطريقة الأكثر شيوعًا للتعلم هي حفظ التعاريف. ومع ذلك، إذا كنت تعتمد فقط على التعريفات لتذكر هذه المفاهيم الثلاثة، فمن السهل أن تصاب بالارتباك أثناء تعلمها. ولأنها تبدو وكأنها مفاهيم متشابهة، فإنها في الواقع تجيب على ثلاثة أنواع مختلفة تمامًا من الأسئلة.
الطريقة الأكثر عملية لفهم ذلك هي:
Task: كيف تبدأ المهمةTask.detached: مدى ارتباط المهمة بالسياق الحاليMainActor: في أي دلالات معزولة يجب تنفيذ هذا المنطق؟
ما عليك سوى تفكيك هذه الأبعاد الثلاثة وسيختفي الكثير من الالتباس على الفور.
1. يحل Task: كيف يمكنني الدخول إلى العملية غير المتزامنة من هنا؟
سيناريوهات الاستخدام الأكثر طبيعية لـ Task هي:
الرمز الحالي ليس
async، لكني بحاجة إلى إدخال عملية غير متزامنة الآن.
هذا شائع جدًا في نظام iOS:
- زر رد الاتصال ليس
async - أسلوب وكيل UIKit ليس
async - حدث معين في دورة الحياة المتزامنة يؤدي فجأة إلى تشغيل طلب غير متزامن
على سبيل المثال:
Button("刷新") {
Task {
await viewModel.reload()
}
}
معنى Task هنا واضح جدًا: جسر بوابة تفاعل متزامنة إلى العالم غير المتزامن.
لذا فإن مفتاح Task هو “تم إنشاء حدود المهمة.”
بمجرد كتابتها يعني:
- هنا يبدأ العمل غير المتزامن بدورة حياة مستقلة
- يجوز إلغاؤه
- وسوف تنتهي في مرحلة ما
- عواقبها في نهاية المطاف يجب أن يتعامل معها شخص ما
يوضح هذا أيضًا أنه لا يمكن فهم Task على أنه “يمكن استخدام طبقة واحدة لـ await”.
2. الخصائص الحقيقية لـ Task العادي: عادة ما يستمر لفترة من العمل غير المتزامن في السياق الحالي
الموقف الشائع هو فهم Task العادي أيضًا “بشكل مستقل”، معتقدًا أنه بمجرد إنشائه، لا علاقة له بالسياق الحالي.
الفهم الأكثر دقة في الهندسة يجب أن يكون:
** غالبًا ما تكون Task الشائعة مهمة تمتد من السياق الحالي. **
وهذا يعني أنه غالباً ما يرث جزءاً من البيئة الحالية، مثل:
- دلالات الممثل الحالي
- سياق المهمة الحالية
- إلغاء بعض العلاقات
- الاتجاهات ذات الأولوية الحالية
لذا فإن الأمر أشبه بـ “إدخال غير متزامن أعلى المنطق الحالي” بدلاً من “إنشاء عالم جديد خارج المسار تمامًا”.
من المهم أن تفهم هذا، لأنك ستفهم لاحقًا ما هو بالضبط “فك الارتباط” عن Task.detached.
3. Task.detached هو بيان مستقل أقوى
تصف العديد من المقالات Task.detached بأنه إصدار “أكثر تقدمًا”، مما قد يؤدي بسهولة إلى التحيز في الممارسة العملية.
إنه أكثر خطورة.
والسبب واضح ومباشر: فهو في جوهره من غير المرجح أن يرث السياق الحالي.
أي أنه عند الكتابة:
Task.detached {
...
}
والحقيقة أنها تعبر عن:
- لا يريد هذا العمل أن يرتبط بشكل طبيعي بالسياق الحالي
- أريدها أن تكون أكثر استقلالية
- أنا على استعداد لتحمل المزيد من مسؤوليات دورة الحياة والعزلة بنفسي
وهذا أمر معقول في بعض السيناريوهات، مثل:
- القيام بأعمال تنظيف الخلفية التي لا علاقة لها بالصفحة الحالية
- القيام بمهام معينة تتطلب بوضوح الانفصال عن دلالات الممثل الحالي
- تتطلب أطر عمل معينة أو طبقات بنية أساسية معينة بشكل صريح مهام مستقلة
لكن في مجال الصفحات، ما يريده معظم الناس حقًا هو شيء أكثر قابلية للإدارة.
وغالبًا ما يجعل detached الإدارة صعبة.
4. من السهل إساءة استخدام Task.detached
لأن أول شعور يعطيه للناس هو “الحرية”.
لكن في الأنظمة المتزامنة، غالبًا ما يكون ثمن الحرية هو فيضان المسؤوليات.
بمجرد استخدام Task.detached، سيتعين عليك قريبًا الإجابة على هذه الأسئلة مرة أخرى:
- ومن يملكها الآن؟
- هل يجب أن يستمر الأمر عند إتلاف الصفحة؟
- إذا تم إلغاء المهمة الخارجية، فهل ستستمر في العمل؟
- هل يمكن تغيير الوضع الحالي مباشرة بعد عودته؟
إذا لم تتم الإجابة على أي من هذه الأسئلة بشكل واضح، فعادةً ما ينتهي الأمر بـ Task.detached بالهروب من مسؤولية المهمة.
لذا فإن المبدأ الافتراضي الخاص بي بسيط:
- استخدم
Taskالعادي لطبقات الصفحة وViewModel أولاً. - ضع في اعتبارك
Task.detachedفقط إذا كنت تعرف بوضوح شديد “لماذا من الضروري الابتعاد عن السياق الحالي”
5. MainActor ليس مفهوم “بدء مهمة” على الإطلاق.
هذه هي النقطة التي تحتاج إلى توضيح أكثر دقة.
يناقش Task وTask.detached:
كيف يتم إنشاء مهمة غير متزامنة ومدى ارتباطها بالسياق الحالي.
يناقش MainActor:
تحت أي دلالات عزلة يجب تنفيذ جزء معين من التعليمات البرمجية.
إنها ليست “إصدارًا رئيسيًا للمهمة”، كما أنها ليست “مهمة تستخدم خصيصًا لتحديث واجهة المستخدم”. إنه يخبر المترجم والمتصل بشكل أساسي:
- ينتمي هذا المنطق إلى مجال عزل الفاعل الرئيسي
- يرتبط بقوة بواجهة المستخدم
- لا يمكن تعديلها بشكل عشوائي في أي سياق متزامن
لذلك، كان تركيز MainActor دائمًا على “القيود”.
6. يجب أن تؤخذ الرموز المتعلقة بواجهة المستخدم على محمل الجد MainActor
الموقف الشائع هو التفكير: على أي حال، في النهاية، نقوم فقط بتعيين قيمة على الصفحة، لذلك لا ينبغي أن يكون الأمر بهذه الخطورة.
المشكلة هي أن حالة واجهة المستخدم المعقدة حقًا لا يتم تعيين قيمة لها مرة واحدة فقط.
غالبًا ما تحتوي الصفحات الحقيقية على هذه الأشياء:
- حالة التحميل
- قائمة البيانات
- حالة فارغة
- موجه الخطأ
- تم تعطيل بعض الأزرار المحلية
بمجرد كتابة هذه القيم من خلال نتائج غير متزامنة متعددة في نقاط زمنية مختلفة، بدون حدود MainActor واضحة، ستتراكم المشكلة ببطء إلى:
- تومض الصفحة من حين لآخر
- بعض تحديثات الحالة بترتيب غريب
- ينتقل ViewModel ذهابًا وإيابًا بين منطق الخلفية ومنطق واجهة المستخدم
ولذلك، فإن قيمة MainActor لا تهدف فقط إلى “منع أخطاء مؤشر الترابط”، ولكن أيضًا لإنشاء حدود ملكية واضحة لحالات واجهة المستخدم.
7. تسلسل حكم أقرب إلى القتال الفعلي
إذا لم تتمكن من معرفة المفهوم الذي يجب استخدامه عند كتابة التعليمات البرمجية، فيمكنك أولاً أن تسأل نفسك بالترتيب التالي:
1. هل أنا في سياق غير متزامن وأحتاج إلى الدخول في عملية غير متزامنة؟
إذا كان الأمر كذلك، ففكر في Task أولاً.
2. هل أحتاج حقًا إلى وجود هذه المهمة بشكل مستقل عن السياق الحالي؟
فكر فقط في Task.detached إذا كانت الإجابة واضحة جدًا.
إذا كان “يشعر بمزيد من الحرية”، فلا ينبغي عادةً استخدامه.
3. هل قراءة التعليمات البرمجية وكتابتها لواجهة المستخدم مرتبطة بشدة بالحالة؟
إذا كان الأمر كذلك، فيجب عليك التفكير بجدية في MainActor بدلاً من انتظار حدوث خطأ ما.
يعد تسلسل الحكم هذا أكثر فائدة من حفظ تعريفات واجهة برمجة التطبيقات (API) لأنه يتوافق بشكل مباشر مع المشكلات الحقيقية التي تواجهها عند كتابة كود العمل.
8. كيف تبدو الرائحة الكريهة الأكثر شيوعًا؟
الأنواع الثلاثة الأكثر شيوعًا للروائح الكريهة التي أراها هي:
1. في حالة عدم توفر await، قم بتعبئة Task أولاً
سيؤدي هذا إلى تجزئة إدخالات المهمة في المشروع أكثر فأكثر، وفي النهاية لن يتمكن أحد من معرفة من يملك هذه المهام.
2. لا تفهم وراثة السياق واستخدم Task.detached دائمًا
يبدو هذا “مستقلًا” جدًا، ولكنه في الواقع غالبًا ما يدفع بمشكلة دورة الحياة إلى أبعد من ذلك.
3. يعد ViewModel مسؤولاً عن كل من معالجة الخلفية وإعادة كتابة واجهة المستخدم، ولكن لا يوجد حدود واضحة لـ MainActor
قد لا يتعطل هذا النوع من التعليمات البرمجية بالضرورة على المدى القصير، ولكنه عرضة بشكل خاص لتراكم أخطاء الحالة المخفية على المدى الطويل.
9. الخلاصة: ليسا في نفس البعد
ولو كان علي أن أتذكر هذه المفاهيم الثلاثة في جملة واحدة لقلت:
Task: أريد الآن إنشاء مهمة غير متزامنةTask.detached: أريد إنشاء مهمة غير متزامنة أكثر استقلالية وأقل توارثًا عن السياق الحالي.MainActor: يجب تنفيذ هذا المنطق ضمن حدود عزل الممثل الرئيسي
يجيبون على أسئلة مختلفة في الأنظمة المتزامنة:
- أين تبدأ المهمة؟
- مدى إحكام ربط المهمة بالسياق الحالي
- ما هي الدول التي يجب عزلها من قبل الفاعل الرئيسي
وطالما تم فصل هذه الأبعاد الثلاثة، عند كتابة كود Swift Concurrency لاحقًا، لن يتم الخلط بين “بدء مهمة” و"العودة إلى الموضوع الرئيسي" مع نفس الشيء.
What to read next
Want more posts about Swift Concurrency?
Posts in the same category are usually the best next step for reading more on this topic.
View same categoryWant to keep following #Swift Concurrency?
Tags are useful for related tools, specific problems, and similar troubleshooting notes.
View same tagWant to explore another direction?
If you are not sure what to read next, return to the homepage and start from categories, topics, or latest updates.
Back home