Kotlin / التزامن والتكامل والتصميم المترابط / الدرس 17
حماية الحالة المشتركة بالحد الذري المناسب
ميّز الرؤية والتحديث الذري والقيود المركبة وأقفال الكوروتينات والملكية المبنية على القيم غير القابلة للتعديل.
في هذه الصفحة
أهداف الدرس
- اشرح لماذا لا يجعل مرجع volatile أو تصريح val عملية الزيادة ذرية.
- اختر مزامنة تحيط بقيد العمل كله، لا بكل حقل على حدة فقط.
يحتوي التحديث المركب عدة خطوات
تقرأ زيادة عداد مشترك قيمته، وتحسب بديلا، ثم تكتبه من حيث المفهوم. قد يقرأ عاملان القيمة القديمة نفسها فتضيع إحدى الزيادتين. يستطيع حقل volatile توفير ضمانات الرؤية ذات الصلة، لكنه لا يحوّل العملية المركبة إلى معاملة ذرية. وتمنع val إعادة إسناد المرجع، لا تعديل الكائن الذي يشير إليه. تواجه الكوروتينات على موزعات متعددة الخيوط تسابقات الذاكرة المشتركة العادية أيضا. فسّر القيد المطلوب وكل مسار للوصول قبل تقرير ملاءمة متغير ذري أو حصر التنفيذ أو Mutex أو نموذج ملكية آخر.
استخدم الذرية للقيد الذي تغطيه فعلا
يوفّر AtomicInteger على JVM عمليات ذرية لحالة عدد صحيح واحدة. تستطيع incrementAndGet التعبير بأمان عن زيادة عداد واحدة وفقا لعقد الواجهة، لكن عدادين ذريين مستقلين لا يجعلان قاعدة عمل تجمعهما ذرية. إذا وجب تغيير المقاعد المتاحة والحجوزات النشطة معا، فقد تتيح حماية كل منهما مستقلا رؤية تركيبة وسيطة غير صالحة أو فقد التنسيق. المثال متتابع عمدا ويعرض نتيجة العملية فقط؛ وليس اختبار ضغط أو قياس أداء أو دليلا على تطبيق حجز أكبر.
import java.util.concurrent.atomic.AtomicInteger
fun main() {
val completed = AtomicInteger(0)
println(completed.incrementAndGet())
println(completed.addAndGet(2))
println(completed.get())
}
1
3
3
المخرجات المتوقعة هي 1 ثم 3 ثم 3. الشيفرة متتابعة وتحتاج واجهات JVM، ولم تُترجم. تأتي ضمانات الذرية من عقد الواجهة الموثق، لا من هذا المثال غير المنفذ.
احم كل وصول مرتبط ببروتوكول واحد متفق عليه
يوفّر Mutex للكوروتينات استبعادا متبادلا يمكنه التعليق. وتحرر withLock القفل عند الخروج من الكتلة، بما يشمل المسارات الاستثنائية، وفقا لعقد المكتبة. يجب أن يتبع كل وصول منافس للحالة المحمية البروتوكول نفسه؛ فقد يبطل كاتب واحد بلا قفل الحجة كاملة. اجعل المنطقة الحرجة مركزة، وتجنب انتظار مورد خارجي أو تنفيذ عمل غير مرتبط أثناء حمل القفل. ينسق Mutex الذاكرة داخل سياق العملية المقصود، ولا يتحول إلى قفل قاعدة موزع أو يحمي مثيل تطبيق آخر يملك كائن قفل منفصلا.
انتظر الأبناء قبل قراءة الحالة النهائية المحمية
يطلق البرنامج المقترح كوروتينين ابنين صغيرين، ويجري كل منهما زيادتين تحت حماية Mutex واحد. ينتظر نطاق الكوروتينات المحيط انتهاء الابنين قبل القراءة النهائية، ولذلك يتوقع النموذج العدد أربعة. هو مثال صغير عمدا دون إدخال وإخراج أو توليد حمل. قد يستخدم تصميم حقيقي حصر التنفيذ عند مالك واحد بدلا من القفل، بشرط اتباع كل وصول لملكيته ودورة حياته. لا تنقل كل عملية صغيرة بين السياقات لمجرد ادعاء الحصر؛ بل قيّم الصحة وكلفة الانتقال معا. لم تُنفّذ تجربة تزامن هنا.
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
fun main() = runBlocking {
val mutex = Mutex()
var count = 0
coroutineScope {
repeat(2) {
launch(Dispatchers.Default) {
repeat(2) { mutex.withLock { count += 1 } }
}
}
}
println(count)
}
4
المخرج المتوقع 4 إذا بُني البرنامج ونُفّذ مع kotlinx.coroutines متوافقة. تأتي القراءة النهائية بعد اكتمال النطاق. لم يُختبر في بيئة التأليف مترجم أو موزّع أو سباق أو توقيت.
قلّل الحالة المشتركة عندما يسمح النموذج
يمكن للقيم غير القابلة للتعديل ذات دلالة اللقطة، والملكية عبر الرسائل، تقليل مساحة الحاجة إلى المزامنة، لكنهما ما زالا يحتاجان نشرا صحيحا ودورة حياة ودلالات أحداث مناسبة. ليس data class ذو قوائم قابلة للتعديل ثابتا بعمق، وقد يكشف منظور قراءة محتوى يتغير عبر مرجع آخر. اختبر القيد الفعلي باستراتيجية تزامن معزولة، بما يشمل الإلغاء والفشل أثناء امتلاك الموارد. اذكر النتائج المسموحة والبيئة الدقيقة بدلا من الاعتماد على تشغيل محظوظ. تقدم الدورة أمثلة مصدر محللة فقط، دون ادعاء صحة تزامن أو إنتاجية أو أمان إنتاج.
الأخطاء الشائعة وإصلاحها
يفقد عداد volatile بعض الزيادات.
- لماذا يحدث؟
- لم تجعل ضمانات الرؤية عملية القراءة والتعديل والكتابة المركبة ذرية.
- كيف تصلحه؟
- استخدم عملية ذرية مناسبة أو احم التحديث كاملا.
- كيف تتجنب تكراره؟
- نمذج العمليات المركبة، ولا تكتف بمراجعة معدلات الحقول.
يخالف حقلان ذريان قاعدة مشتركة بينهما.
- لماذا يحدث؟
- يمتد القيد عبر حالة أوسع مما تحميه أي عملية منفردة.
- كيف تصلحه؟
- احم الانتقال المركب أو أعد تصميمه تحت مالك واحد.
- كيف تتجنب تكراره؟
- سمّ قيد العمل قبل اختيار وسيلة المزامنة.
يتجاوز مثيل تطبيق ثان السعة رغم Mutex محلي.
- لماذا يحدث؟
- عومل التنسيق داخل العملية كأنه حماية بين مثيلات مستقلة.
- كيف تصلحه؟
- افرض التخصيص عند حد التخزين المشترك المرجعي.
- كيف تتجنب تكراره؟
- وثّق بدقة نطاق كل قفل وكل مالك للحالة.
اختبر فهمك
1. هل يصبح data class الذي يحتوي val reservations: MutableList<String> غير قابل للتعديل بعمق؟
اعرض الحل المشروح
لا. لا يمكن إعادة إسناد الخاصية عبر ذلك التصريح، لكن محتويات القائمة قابلة للتغيير، وقد ترى المراجع الأخرى تلك التغييرات أو تسببها. استخدم ملكية مستقرة للعناصر والحاوية أو تمثيلا غير قابل للتعديل عندما يجب مشاركة لقطات بأمان. ويظل نسخ data class سطحيا أيضا ما لم تُنفّذ سياسة أعمق.
2. لماذا لا يثبت تشغيل ناجح واحد لعداد غير آمن صحته؟
اعرض الحل المشروح
يعتمد ظهور التسابق على الجدولة، وقد لا يظهر في ذلك التشغيل. تتطلب الصحة حجة مزامنة تغطي كل وصول مع اختبارات معزولة مناسبة، لا مجرد مصادفة الرقم النهائي المتوقع مرة واحدة. يمكن للاختبار كشف عيوب، لكنه لا يحول عملية مركبة غير محمية إلى عملية ذرية.
أهم ما تعلمته
- يجب أن تغطي المزامنة القيد كله وجميع مسارات الوصول إليه.
- لا تزيل سهولة صياغة اللغة حدود الذاكرة المشتركة أو الحالة الموزعة.