سلسلة Swift Concurrency 06| المشاكل الشائعة في التزامن Swift: حالات السباق والطلبات المتكررة وارتباك الحالة
المشكلة الحقيقية هي أن هذه المشاكل غالبًا ما تظهر على شكل انقطاعات متفرقة في العمل بدلاً من الأعطال الواضحة.
الأمر الأكثر إحباطًا بشأن أخطاء التزامن هو أنها غالبًا لا تبدو وكأنها أخطاء.
غالبًا ما يتجلى على الإنترنت في هذه الأسئلة الغامضة:
- قال المستخدم “في بعض الأحيان يومض”
- الاختبار يقول “أحيانًا تظهر البيانات القديمة”
- قال المنتج “لقد قطعت الفلتر للتو، لماذا قفز مرة أخرى؟”
- لا يوجد أي عطل واضح في السجل، ولكن حالة الصفحة خاطئة.
بمعنى آخر، تبدو العديد من مشكلات التزامن أشبه بـ “استثناءات عمل عرضية” أكثر من كونها “معطلة تقنيًا بشكل واضح”.
لذلك، في هذه المقالة، لا أريد أن أتحدث فقط عن تعريف المصطلحات، ولكن التركيز بشكل مباشر على سيناريو صفحة القائمة الأكثر واقعية وتقسيم الأنواع الثلاثة الأكثر شيوعًا من المشكلات:
- المنافسة
- كرر الطلب
- حالة من الارتباك
وكيف تنمو في الكود الحقيقي.
1. قم أولاً بإلقاء نظرة على صفحة حقيقية جدًا بحيث لا يمكن أن تكون أكثر واقعية.
لنفترض أن هناك صفحة قائمة مقالات تدعم هذه العمليات:
- التحميل التلقائي عند دخول الصفحة لأول مرة
- اسحب للأسفل للتحديث
- تبديل الفئات
- أدخل البحث عن الكلمات الرئيسية
- انقر فوق “إعادة المحاولة”
تتم كتابة العديد من المشاريع بهذه الطريقة في البداية:
@MainActor
final class ArticlesViewModel: ObservableObject {
@Published var items: [Article] = []
@Published var isLoading = false
@Published var errorMessage: String?
@Published var selectedCategory: String = "all"
@Published var keyword: String = ""
let repository: ArticlesRepository
init(repository: ArticlesRepository) {
self.repository = repository
}
func onAppear() {
Task {
await load()
}
}
func refresh() {
Task {
await load()
}
}
func retry() {
Task {
await load()
}
}
func categoryChanged(to value: String) {
selectedCategory = value
Task {
await load()
}
}
func keywordChanged(to value: String) {
keyword = value
Task {
await load()
}
}
func load() async {
isLoading = true
errorMessage = nil
do {
items = try await repository.fetchArticles(
category: selectedCategory,
keyword: keyword
)
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
}
عند كتابة هذا الكود لأول مرة، عادة ما يعتقد الجميع أنه “سلس جدًا”:
- نعم
async/await - الكود واضح ومباشر
- كل مدخل يعمل
ولكن طالما أن الصفحة مستخدمة فعليًا، فستظهر مشكلات التزامن قريبًا.
2. النوع الأول من المشكلة: حالة السباق هي أمر افتراضي غير موجود.
لا يزال هذا الرمز.
مشكلتها الأساسية ليست أنها تفتح الكثير من Task، ولكن أنها تحدث افتراضيًا بالترتيب الذي تريده:
- الطلب المرسل أولاً سيتم إرجاعه أولاً.
- عندما يعود الطلب القديم، لم تتغير شروط التصفية الحالية.
- تتوافق بداية التحميل ونهايته دائمًا مع واحد لواحد
لكن الأنظمة غير المتزامنة لا تضمن للفريق هذه الأوامر.
على سبيل المثال، يعمل المستخدم على النحو التالي:
- أدخل إلى الصفحة واطلب أ للإصدار
- قم بالتبديل فورًا إلى فئة “iOS” واطلب من B الإرسال
- أدخل الكلمة الأساسية
swiftمرة أخرى لطلب إصدار C
في هذا الوقت، إذا كان أمر الإرجاع هو:
- C يعود أولاً
- عد بعد أ
- يعود B أخيرًا
وفقًا للكود الحالي، سيتم تغيير النتائج الثلاثة إلى items.
بمعنى آخر، ما يتم عرضه في الصفحة النهائية يعتمد على من يعود أخيرًا، وليس من يتوافق مع نية المستخدم الحالية.
هذه هي حالة السباق الأكثر شيوعًا:
يعتمد الكود بشكل سري على الطلب، لكن الطلب غير مقيد على الإطلاق.
3. النوع الثاني من المشاكل: السبب الجذري للطلبات المتكررة هو عادة عدم إغلاق المدخل.
بالنظر إلى ViewModel أعلاه، هناك خمسة إدخالات على الأقل ستؤدي إلى تشغيل load():
onAppearrefreshretrycategoryChangedkeywordChanged
كل مدخل له Task خاص به.
وهذا بالتأكيد قانوني من الناحية النحوية، لكنه يعني من الناحية الهندسية:
- لا توجد نقطة جدولة موحدة للمهام المماثلة
- لا أحد يعرف ما إذا كانت هناك بالفعل مهمة مماثلة قيد التشغيل
- عند ظهور مهام جديدة، فإن المهام القديمة ليس لها مصير واضح
ثم لم تعد “الطلبات المتكررة” عرضية، ولكنها نتاج طبيعي للهيكل.
لذا، في إدارة التزامن، نادرًا ما أسأل:
“لماذا يوجد طلب إضافي هنا؟”
كثيرا ما أسأل:
“كم عدد المداخل الموجودة لنفس النوع من المهام؟ هل هناك أي علاقات بديلة بينهما؟”
إذا لم تتمكن من الإجابة على هذين السؤالين، فإن الطلبات المتكررة تكاد تكون حتمية.
4. النوع الثالث من المشاكل: الحالة مضطربة، غالبًا لأن النتائج منتهية الصلاحية لا تزال مؤهلة للكتابة.
الموقف الشائع هو أنه طالما تم إرجاع الطلب بنجاح، فيجب قبول النتيجة.
عادةً ما يكون هذا أمرًا جيدًا في الأنظمة المتزامنة، ولكنه غالبًا ما يكون خاطئًا في الأنظمة المتزامنة.
لأن المشكلة الأكثر أهمية في السيناريو المتزامن هي:
**هل لا تزال هذه النتيجة تعتبر نتيجة صالحة للصفحة الحالية؟ **
على سبيل المثال:
- تم تحويل الصفحة الحالية إلى
keyword = "swift" - النتيجة من الطلب القديم
keyword = ""
والنتيجة حقيقية وناجحة وبالصيغة الصحيحة ولكنها انتهت صلاحيتها. إذا كان لا يزال مسموحًا بكتابة واجهة المستخدم، فستكون الحالة خاطئة.
لذلك، في النظام المتزامن، “النتيجة صحيحة” و"النتيجة صالحة" هما شيئان مختلفان. ظاهريًا، تبدو العديد من مشكلات الصفحات وكأنها نتائج خاطئة، لكنها في الواقع أقرب إلى عدم القدرة على الحكم على ما إذا كانت لا تزال مؤهلة للتنفيذ.
5. لا تتعجل في استخدام الأدوات المعقدة أولاً. الخطوة الأولى هي إغلاق المهام المماثلة.
أكثر ما يحتاجه الكود أعلاه هو القيام بشيء بسيط جدًا أولاً:
** إعطاء المهام المماثلة مدخلاً موحدًا. **
على سبيل المثال، قم أولاً بتحميل القائمة بالشكل التالي:
@MainActor
final class ArticlesViewModel: ObservableObject {
@Published private(set) var state: ViewState = .idle
@Published private(set) var items: [Article] = []
@Published var selectedCategory: String = "all"
@Published var keyword: String = ""
private let repository: ArticlesRepository
private var loadTask: Task<Void, Never>?
init(repository: ArticlesRepository) {
self.repository = repository
}
func reload() {
let request = RequestContext(
category: selectedCategory,
keyword: keyword
)
loadTask?.cancel()
loadTask = Task {
await performLoad(request: request)
}
}
private func performLoad(request: RequestContext) async {
state = .loading
do {
let result = try await repository.fetchArticles(
category: request.category,
keyword: request.keyword
)
guard !Task.isCancelled else { return }
guard request.category == selectedCategory,
request.keyword == keyword else { return }
items = result
state = .loaded
} catch is CancellationError {
// 取消不更新页面
} catch {
guard !Task.isCancelled else { return }
state = .failed(error.localizedDescription)
}
}
}
يقوم هذا الكود بعدة أشياء بالغة الأهمية:
- توجد نقطة تعليق واحدة فقط لمهام التحميل المماثلة
loadTask - عند وصول مهمة جديدة، سيتم إلغاء المهمة القديمة أولاً
- تجميد “السياق الحالي” إلى
RequestContextعند إرسال الطلب - بعد إرجاع النتيجة، سيتم التحقق مما إذا كانت لا تزال مطابقة للصفحة الحالية
لاحظ أن المهم هنا هو أن تصبح علاقات المهام واضحة.
6. “تجميد سياق الطلب” أمر بالغ الأهمية
تتحدث العديد من المقالات المتزامنة عن إلغاء المهام، ولكن لا يوجد تركيز كافٍ على “لقطة السياق”. لكن في أعمال الصفحة، الأمر مهم جدًا.
على سبيل المثال، عند الطلب:
selectedCategory = "ios"-keyword = "swift"
ثم لا ينبغي أن تقرأ هاتان القيمتان أحدث القيم في ViewModel الحالي ديناميكيًا بعد انتهاء الطلب. وإلا فسوف تحصل في كثير من الأحيان على حالة غريبة جدًا:
- عند إرسال الطلب، فهو عبارة عن مجموعة من المعلمات
- يتم استخدام مجموعة أخرى من المعلمات عند التحقق من النتائج
لذا فإن المبدأ العملي للغاية هو:
عند بدء مهمة غير متزامنة، قم بتجميد سياق العمل الذي تعتمد عليه المهمة حقًا.
وبهذه الطريقة، سيكون هناك أساس واضح للحكم على “ما إذا كانت هذه النتيجة لا تزال هي النتيجة الحالية” لاحقًا.
7. تنتهي العديد من أخطاء التزامن بـ “عدد كبير جدًا من إدخالات كتابة الحالة”
من المواقف الشائعة أنه عند مواجهة مشكلة التزامن، ستفكر على الفور في:
- هل تريد قفله؟
- هل تريد أن تكون ممثلاً؟
- هل تريد تبديل المواضيع؟
بالطبع تكون هذه المشكلات مهمة في بعض الأحيان، ولكن في السيناريوهات على مستوى الصفحة، تكون المشكلات الأكثر شيوعًا هي في الواقع:
- هناك أماكن كثيرة جدًا لكتابة
items - يمكن تغيير العديد من الأماكن
isLoading - كثرة المداخل يمكن إرسال الطلبات مباشرة
بمجرد أن تتناثر إدخالات الكتابة الخاصة بالحالة، حتى لو لم تكن هناك منافسة حقيقية للبيانات، ستحدث ظاهرة “التركيبة خاطئة”.
لذلك عندما أقوم بهذا النوع من استكشاف الأخطاء وإصلاحها، عادةً ما أطرح الأسئلة التالية أولاً:
- ما هي الرموز التي لها صلاحية تغيير هذه الحالة؟
- ما هي المهام التي لها الحق في إنهاء التحميل الحالي
- النتائج التي لها الحق في الكتابة فوق القائمة الحالية
بمجرد عدم معالجة هذه المشكلات، عادة ما يكون الأمر مجرد مسألة وقت قبل ظهور الأخطاء.
8. تسلسل تطوري أقرب إلى المشروع الحقيقي
إذا كنت تريد حقًا حل هذا النوع من المشكلات، أقترح عليك التطوير بهذا الترتيب بدلاً من تقديم الكثير من الآليات في البداية:
1. أغلق المدخل للمهام المشابهة
أولاً، دع “تحميل القائمة” يكون له مدخل موحد واحد فقط، بدلاً من إرسال طلب خاص به لكل حدث واجهة مستخدم.
2. توضيح علاقة استبدال المهمة
ما هي المهام التي يجب أن تكون متزامنة والتي يجب أن تلغي المهام القديمة وتحتفظ فقط بالمهام الأخيرة.
3. تجميد سياق الطلب
قم بتجميع معلمات العمل الرئيسية التي يتم الاعتماد عليها عند تقديم الطلبات في كائن واضح.
4. إضافة حكم الصلاحية إلى النتيجة
ليست كل النتائج التي تم إرجاعها بنجاح مؤهلة لتغيير الصفحة الحالية.
5. أخيرًا، فكر في عزل الدولة المشتركة الأكثر تعقيدًا
على سبيل المثال، ذاكرة التخزين المؤقت المشتركة عبر الصفحات، وتنسيق الموارد عبر الوحدات، ثم انظر إلى الممثل والمنسق الموحد والحلول الأخرى.
يعد هذا الترتيب أكثر استقرارًا لأنه يحل علاقة التزامن التجاري أولاً، بدلاً من تقديم مفردات تقنية أكثر تعقيدًا أولاً.
9. الاستنتاج: جوهر معظم مشكلات التزامن في الأعمال هو “عدم وجود نماذج لعلاقات المهام”
يبدو أن حالات العرق والطلبات المكررة وارتباك الحالة تمثل ثلاث مشكلات، ولكن الأسباب الجذرية الفعلية غالبًا ما تكون متقاربة جدًا:
- من هو نفس مهمة من، لا النمذجة
- مهام جديدة قادمة، ماذا تفعل بالمهام القديمة، لا يوجد نمذجة
- هل النتيجة لا تزال صالحة؟ لا يوجد النمذجة.
- أين يمكنني كتابة حالتي دون إغلاقها؟
لذا، لإعادة صياغة هذا المقال بطريقة مختصرة، أود أن أقول:
يبدو أن معظم مشكلات التزامن في الأعمال غير كفؤة في بناء جملة التزامن، ولكنها في الواقع أقرب إلى الفشل في وضع نموذج واضح لعلاقات المهام، وصلاحية النتائج، وأذونات الكتابة الخاصة بالولاية.
بمجرد أن تصبح هذه الأشياء الثلاثة واضحة، فإن الكثير من “الارتباك العرضي” سوف يختفي بسهولة أكبر مما تعتقد.
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