تم تحسين الإصدارات 3.0 من نظام التشغيل Android والإصدارات الأحدث منه لتتوافق مع التصاميم المعمارية للمعالجات المتعددة. يتناول هذا المستند المشكلات التي قد تظهر عند كتابة تعليمات برمجية متعددة السلاسل للأنظمة المتماثلة متعددة المعالجة في اللغات C وC++ وJava (والتي يشار إليها في ما بعد باسم "Java" والإيجاز). فهي مخصّصة كدليل تمهيدي لمطوّري تطبيقات Android، وليس كدليل مناقشة حول هذا الموضوع.
مقدّمة
يشير الاختصار SMP إلى "المعالج المتعدد المتماثل". يصف التصميم في وحدة معالجة مركزية بها نواتان أو أكثر يتشاركان في الوصول إلى الذاكرة الرئيسية. حتى قبل بضع سنوات، كانت جميع أجهزة Android تستخدم معالجًا واحدًا (UP).
كانت معظم أجهزة Android، إن لم تكن كلها، تحتوي دائمًا على وحدات معالجة مركزية متعددة، ولكن في السابق، كان يتم استخدام وحدة واحدة فقط منها لتشغيل التطبيقات بينما تدير الوحدات الأخرى أجزاء مختلفة من مكونات الجهاز (مثل الراديو). قد يكون لكل وحدات المعالجة المركزية بُنى مختلفة، فإن البرامج التي تعمل عليها لم تتمكن من استخدام الذاكرة الرئيسية للتواصل مع آخر.
إنّ معظم أجهزة Android التي يتم بيعها اليوم مبنية على تصاميم SMP، ما يجعل الأمور أكثر تعقيدًا بالنسبة إلى مطوّري البرامج. ظروف السباق في أحد البرامج ذات سلاسل المحادثات المتعددة، قد لا تتسبب في حدوث مشكلات مرئية في أي معالج أحادي، ولكنها قد تفشل بانتظام عندما تكون هناك سلسلتان أو أكثر من سلاسلك تعمل في وقت واحد على أنوية مختلفة. والأكثر من ذلك أن الرمز قد يكون أكثر أو أقل عرضة للإخفاقات عند تشغيله على هياكل معالج البيانات، أو حتى في عمليات التنفيذ المختلفة لنفس الهندسة المعمارية. قد ينكسر الرمز الذي تم اختباره بدقة على معالج x86 من خلال معالجات ARM. قد تبدأ التعليمة البرمجية بالفشل عند إعادة التجميع باستخدام برنامج تجميع أكثر حداثة.
ستشرح بقية هذا المستند السبب وتخبرك بما عليك فعله للتأكد من أن التعليمات البرمجية تتصرف بشكل صحيح.
نماذج اتساق الذاكرة: ما هي أوجه اختلاف بروتوكولات مساحة التخزين (SMP)؟
تقدّم هذه النظرة العامة اللامعة والسريعة حول موضوع معقد. ستستغرق بعض المناطق وغير كاملة، ولكن لا ينبغي أن يكون أي منها مضللاً أو خاطئًا. أثناء ستراها في القسم التالي، فعادةً ما تكون التفاصيل هنا غير مهمة.
اطّلِع على مراجع إضافية في نهاية المستند للحصول على إشارات إلى مراجع أكثر شمولاً حول الموضوع.
تصف نماذج اتساق الذاكرة، أو غالبًا "نماذج الذاكرة" فقط يضمن لغة البرمجة أو بنية الأجهزة في الوصول إلى الذاكرة. على سبيل المثال: إذا كتبت قيمة للعنوان أ، ثم كتبت قيمة للعنوان ب، أن حدوث عمليات الكتابة هذه في كل وحدة معالجة مركزية (CPU) طلبك.
النموذج الذي اعتاد عليه معظم المبرمجين هو تسلسلي الاتساق، الموضّح على النحو التالي (إعلان غاراتشورلو):
- يبدو أنّ جميع عمليات الذاكرة يتم تنفيذها واحدة تلو الأخرى
- يبدو أنّ جميع العمليات في سلسلة محادثات واحدة يتم تنفيذها بالترتيب الموضّح باستخدام برنامج معالج البيانات هذا.
لنفترض مؤقتًا أن لدينا برنامج تجميع أو مترجمًا بسيطًا للغاية بدون أي مفاجآت: إنّه يترجم المهام في رمز المصدر لتحميل التعليمات وتخزينها بسهولة في بالترتيب المقابل، مع تقديم تعليمات واحدة لكل مستوى وصول. سنفترض أيضًا أنه البساطة التي يتم تنفيذها على كل سلسلة محادثات على معالِجها الخاص.
إذا ألقيت نظرة على جزء من التعليمات البرمجية ولاحظت أنه يقوم ببعض القراءات والكتابة من في بنية CPU (وحدة معالجة مركزية) متسقة بشكل متسلسل، فأنت تعلم أن الكود ستنفذ عمليات القراءة والكتابة بالترتيب المتوقع. من المحتمل أن إلا أن وحدة المعالجة المركزية (CPU) تعيد ترتيب التعليمات وتؤخر عمليات القراءة والكتابة، ولكن أنّه لا يمكن استخدام الرموز البرمجية على الجهاز لمعرفة أنّ وحدة المعالجة المركزية (CPU) تنفّذ أي إجراء بخلاف تنفيذ التعليمات بطريقة مباشرة. (سنتجاهل إدخال/إخراج لبرنامج تشغيل الجهاز تم تخصيصه للذاكرة)
ولتوضيح هذه النقاط، من المفيد اعتبار مقتطفات صغيرة من التعليمات البرمجية، ويُشار إليها عادةً باسم اختبارات الحاسم.
إليك مثال بسيط، مع تشغيل التعليمة البرمجية في سلسلتين:
سلسلة المحادثات 1 | سلسلة المحادثات 2 |
---|---|
A = 3 |
reg0 = B |
في هذه الأمثلة وجميع الأمثلة المحكية المستقبلية، يتم تمثيل مواقع الذاكرة تبدأ الأحرف الكبيرة (A, B, C) وسجلات وحدة المعالجة المركزية بـ "reg". الذاكرة كلها الصفر في البداية. يتم تنفيذ التعليمات من الأعلى إلى الأسفل. هنا، سلسلة المحادثات 1 بتخزين القيمة 3 في الموقع A، ثم القيمة 5 في الموقع B. سلسلة المحادثات 2 تقوم بتحميل القيمة من الموقع B إلى reg0، ثم تُحمّل القيمة من الموقع A إلى reg1. (لاحظ أننا نكتب بترتيب واحد ونقرأ آخر).
من المفترض أن يتم تنفيذ سلسلة التعليمات 1 وسلسلة المحادثات 2 على وحدات معالجة مركزية (CPU) مختلفة. إِنْتَ يجب أن يضعوا دائمًا هذا الافتراض عند التفكير في التعليمات البرمجية متعددة السلاسل.
يضمن الاتساق التسلسلي أنه بعد انتهاء سلسلتَي المحادثات قيد التنفيذ، ستكون السجلات في إحدى الولايات التالية:
السجلات (Register) | الولايات |
---|---|
reg0=5, reg1=3 | ممكن (تم تشغيل سلسلة المحادثات 1 أولاً) |
reg0=0, reg1=0 | ممكن (تم تشغيل سلسلة المحادثات 2 أولاً) |
reg0=0, reg1=3 | ممكن (تنفيذ متزامن) |
reg0=5, reg1=0 | مطلقًا |
للدخول في موقف نرى فيه B=5 قبل أن نرى المتجر إلى A، إما القراءات أو الكتابة يجب أن تحدث خارج الترتيب. في متسقًا بشكل تسلسلي، لا يمكن أن يحدث ذلك.
تكون المعالجات الأحادية، بما في ذلك x86 وARM، متسقة تسلسليًا عادةً. يبدو أن سلاسل المحادثات تتم بطريقة متداخلة، حيث تقوم نواة نظام التشغيل بتحويل نواة نظام التشغيل. بينهما. معظم أنظمة SMP، بما في ذلك x86 وARM، غير متسقة بشكل تسلسلي. على سبيل المثال، من الشائع على التخزين المؤقت للمخازن في طريقها إلى الذاكرة، حتى لا تصل مباشرةً إلى الذاكرة وتصبح مرئية للنواة الأخرى.
تختلف التفاصيل بشكل كبير. على سبيل المثال، x86، ولكن ليس بشكل تسلسلي ثابتًا، لا يزال يضمن أن reg0 = 5 وreg1 = 0 يظل مستحيلاً. يتم تخزين المتاجر مؤقتًا، ولكن يتم الاحتفاظ بترتيبها. ومن ناحية أخرى، لا تفعل ARM. لا يتم الحفاظ على ترتيب المتاجر التي تم تخزينها مؤقتًا، وقد لا تصل المتاجر إلى جميع النوى الأخرى في الوقت نفسه. هذه الاختلافات مهمة لتجميع المبرمجين. ومع ذلك، كما سنرى أدناه، يمكن للمبرمجين C أو C++ أو Java كما يجب البرمجة بطريقة تخفي هذه الفروق المعمارية.
حتى الآن، افترضنا بشكل غير واقعي أن الأجهزة هي التي يعيد ترتيب التعليمات. في الواقع، يقوم برنامج التجميع أيضًا بإعادة ترتيب التعليمات تحسين الأداء. في مثالنا، قد يقرر برنامج التحويل البرمجي أن البعض التعليمة البرمجية في سلسلة المحادثات 2 كانت بحاجة إلى قيمة reg1 قبل أن تحتاج إلى reg0، وبالتالي يتم تحميل reg1 أولاً. أو ربما تكون بعض التعليمات البرمجية السابقة قد حمّلت A مسبقًا، ولا يزال برنامج التحويل البرمجي إعادة استخدام تلك القيمة بدلاً من تحميل A مرة أخرى. في كلتا الحالتين، قد تتم إعادة ترتيب التحميلات إلى reg0 وreg1.
إعادة ترتيب الوصول إلى مواقع الذاكرة المختلفة، سواء في الأجهزة أو في المحول البرمجي، لأن ذلك لا يؤثر في تنفيذ سلسلة محادثات واحدة، يمكن أن يؤدي ذلك إلى تحسين الأداء بشكل كبير. كما سنرى، مع القليل من العناية، فيمكننا أيضًا منعها من التأثير في نتائج البرامج متعددة السلاسل.
ونظرًا لأن برامج التجميع يمكنها أيضًا إعادة ترتيب عمليات الوصول إلى الذاكرة، فإن هذه المشكلة تتعلق وليس جديدًا بالنسبة إلى الشركات الصغيرة والمتوسطة الحجم. حتى في المعالج الأحادي، يمكن للمحول البرمجي إعادة ترتيب التحميلات إلى reg0 وreg1 في المثال، ويمكن جدولة سلسلة المحادثات 1 بين من التعليمات المُعاد ترتيبها. ولكن إذا حدث عدم إعادة ترتيب المجمِّع لدينا، فقد أن تلاحظ هذه المشكلة أبدًا. في معظم الشركات التي تستخدم معالجات ARM SMP، حتى بدون برنامج التحويل البرمجي إعادة الترتيب، ربما تظهر إعادة الطلب، ربما بعد عدد عمليات التنفيذ الناجحة. ما لم تكن البرمجة في نظام التجميع فإن الشركات الناشئة تساعد على زيادة احتمالية ظهور المشكلات التي هناك طوال الوقت.
البرمجة الخالية من سباق البيانات
لحسن الحظ، هناك عادة طريقة سهلة لتجنب التفكير في أي من هذه التفاصيل. إذا اتّبعت بعض القواعد المباشرة، يكون من الآمن عادةً لحذف القسم السابق بالكامل باستثناء "الاتساق التسلسلي" الجزء. وقد تظهر المضاعفات الأخرى في حال تنتهك هذه القواعد عن طريق الخطأ.
تشجّع لغات البرمجة الحديثة على ما يُعرف باسم "المحتوى الخالي من سباق البيانات" أسلوب البرمجة. طالما أنّك تضمن عدم إدخال "مسابقات بيانات"، وتتجنّب حفنة من البنى التي تخبر المُجمِّع بخلاف ذلك، يضمن المُجمِّع والأجهزة تقديم نتائج متسقة تسلسليًا. لن يؤدي هذا الإجراء إلى يعني حقًا أنهم يتجنبون إعادة ترتيب الوصول إلى الذاكرة. ويعني ذلك أنّه في حال اتّباع القواعد، لن تتمكّن من معرفة أنّه تتم إعادة ترتيب عمليات الوصول إلى الذاكرة. يشبه ذلك إلى حد كبير إخبارك بأن السجق لذيذ شهية للطعام، طالما أنك تتعهد بعدم زيارة مصنع للنقانق. سباقات البيانات هي ما تكشف الحقيقة القبيحة عن الذاكرة لإعادة الترتيب.
ما هو "سباق البيانات"؟
يحدث سباق البيانات عندما يتم الوصول إلى سلسلتَي محادثات على الأقل في الوقت نفسه. نفس البيانات العادية، ويقوم أحدهما على الأقل بتعديلها. حسب "عادي البيانات" نعني شيئًا ليس كائن مزامنة على وجه التحديد وهي مخصصة للتواصل الموضوعي. كائنات المزامنة، متغيرات الحالة، Java والكائنات المتطايرة أو الأجسام الذرية C++ ليست بيانات عادية، ويمكن الوصول إليها المسموح لها بالسباق. في الواقع، يتم استخدامها لمنع سباقات البيانات في الأخرى.
لتحديد ما إذا كانت سلسلتا محادثات تصلان إلى نفس الشيء في وقت واحد
موقع الذاكرة، فيمكننا تجاهل مناقشة إعادة ترتيب الذاكرة من أعلاه،
افتراض الاتساق التسلسلي. لا يتضمّن البرنامج التالي تنافسًا على البيانات
إذا كانت A
وB
متغيّرَين منطقيَّين عاديين
يكونان خطأً في البداية:
سلسلة المحادثات 1 | سلسلة المحادثات 2 |
---|---|
if (A) B = true |
if (B) A = true |
وبما أنه لا تتم إعادة ترتيب العمليات، فسيتم تقييم كلا الشرطين إلى خطأ،
لا يتم تحديث أي من المتغيرين على الإطلاق. وبالتالي لا يمكن أن يكون هناك سباق بيانات. تتوفر
لا داعي للتفكير فيما قد يحدث إذا كان التحميل من A
والتخزين في B
في
تمت إعادة ترتيب سلسلة المحادثات 1 بطريقة ما. لا يُسمح للمحول البرمجي بإعادة ترتيب سلسلة التعليمات
1 عن طريق إعادة كتابته باعتباره "B = true; if (!A) B = false
". سيكون ذلك
مثل إعداد النقانق في منتصف المدينة في ضوء النهار الواسع.
يتم تعريف سباقات البيانات رسميًا على الأنواع الأساسية المضمنة مثل الأعداد الصحيحة
المراجع أو المؤشرات. جارٍ التعيين إلى int
في الوقت نفسه
ومن الواضح أن قراءته في مؤشر ترابط آخر هو سباق للبيانات. لكن كلاً من لغة C++
ومكتبة قياسية
فإن مكتبات مجموعات Java تتم كتابتها للسماح لك أيضًا بالتفكير في
سباقات البيانات على مستوى المكتبة. ويعدون بعدم إدخال سباقات البيانات
ما لم تكن هناك عمليات وصول متزامنة إلى الحاوية ذاتها، واحدة على الأقل من
والذي يقوم بتحديثه. يؤدي تعديل set<T>
في سلسلة محادثات واحدة مع
قراءته في سلسلة محادثات أخرى في الوقت نفسه إلى السماح للمكتبة ببدء
تنافس بيانات، وبالتالي يمكن اعتباره بشكل غير رسمي "تنافس بيانات على مستوى المكتبة".
بالمقابل، يتم تعديل set<T>
واحدة في سلسلة محادثات واحدة أثناء القراءة
مختلفة في الأخرى، لا تؤدي إلى سباق بيانات، لأن
المكتبة بعدم إدخال سباق بيانات (منخفض المستوى) في هذه الحالة.
عادةً ما لا يؤدي الوصول المتزامن إلى حقول مختلفة في بنية بيانات إلى حدوث تداخل في البيانات. ومع ذلك، هناك استثناء واحد مهم هذه القاعدة: يتم التعامل مع التسلسلات المتجاورة لحقول البت في C أو C++ "موقع الذاكرة" واحد. الوصول إلى أي حقل بت بمثل هذا التسلسل الوصول إليها جميعًا لأغراض تحديد وجود سباق بيانات. يعكس هذا عدم قدرة الأجهزة الشائعة لتعديل وحدات بت فردية بدون قراءة وحدات البت المجاورة وإعادة كتابتها. ليس لدى مبرمجي Java أي مخاوف مشابهة.
تجنب سباقات البيانات
توفِّر لغات البرمجة الحديثة عددًا من آليات المزامنة لتجنُّب تعارض البيانات. الأدوات الأساسية هي:
- الأقفال أو كتم الصوت
- يمكن استخدام
- موانِع الشدّ (C++11
std::mutex
أوpthread_mutex_t
) أوsynchronized
الكتل في Java لضمان عدم تنفيذ قسم معيّن من الرمز البرمجي بشكل متزامن مع أقسام أخرى من الرمز البرمجي التي تُدخل البيانات نفسها. سنشير إلى هذه المرافق وغيرها من المرافق المشابهة بشكل عام باسم "القفال". الحصول على قفل معين باستمرار قبل الدخول إلى ملف بنية البيانات وإصدارها بعد ذلك، مما يمنع سباقات البيانات عند الوصول هيكل البيانات. كما أنه يضمن أن تكون التحديثات وعمليات الوصول بسيطة، أي لا أي تحديث آخر على هيكل البيانات في المنتصف. هذا يستحق العناء هي الأداة الأكثر شيوعًا لمنع سباقات البيانات. استخدام Javasynchronized
كتل أو رموز C++lock_guard
أوunique_lock
التأكد من فتح الأقفال بشكل صحيح في حدث استثناء. - موانِع الشدّ (C++11
- المتغيّرات المتغيرة/الذرية
- توفّر Java
volatile
حقلاً يتيح الوصول المتزامن. دون إدخال سباقات البيانات. منذ عام 2011، قدم دعم C وC++atomic
متغيّرًا وحقلًا يتضمّنان دلالات متشابهة وهي عادة ما يكون أكثر صعوبة من الأقفال، لأنها تضمن فقط تكون حالات الوصول الفردية إلى متغير واحد ذرية. (في لغة C++ ، يتم اتباع هذا يمتد إلى العمليات البسيطة للقراءة والتعديل والكتابة، مثل الزيادات. لغة Java يتطلب استدعاء طريقة خاصة لذلك). على عكس الأقفال، لا يمكن لمتغيّراتvolatile
أوatomic
تنفيذ تُستخدم مباشرةً لمنع تداخل سلاسل التعليمات البرمجية الأخرى مع تسلسلات الرموز الأطول.
من المهم ملاحظة أن هناك اختلافًا كبيرًا في volatile
المعاني في لغة C++ وJava. في C++، لا يمنع volatile
تتبع
البيانات، على الرغم من أنّ الرموز البرمجية القديمة غالبًا ما تستخدمه كحل بديل لعدم توفّر مثيلات
atomic
. لم يعُد هذا الإجراء مقترحًا. بوصة
C++، استخدم atomic<T>
للمتغيرات التي يمكن أن تكون متزامنة
يتم الوصول إليها بواسطة سلاسل محادثات متعددة. إن C++ volatile
مخصصة
سجلات الجهاز وما إلى ذلك.
متغيّرات C/C++ atomic
أو متغيّرات Java volatile
ويمكن استخدامها لمنع سباقات البيانات على المتغيرات الأخرى. إذا كانت السمة flag
المعلَنة على أنّها من النوع atomic<bool>
أو atomic_bool
(C/C++ ) أو volatile boolean
(Java)،
وفي البداية بشكل خاطئ، فإن المقتطف التالي خالٍ من البيانات:
سلسلة المحادثات 1 | سلسلة المحادثات 2 |
---|---|
A = ...
|
while (!flag) {}
|
وبما أنّ سلسلة المحادثات 2 تنتظر حتى يتم ضبط flag
، سيكون إذن الوصول إلى
يجب أن تحدث A
في سلسلة المحادثات 2 بعد تنفيذ سلسلة الإجراءات، ولا بالتزامن مع،
التعيين إلى "A
" في سلسلة المحادثات 1. وبالتالي لا يوجد سباق بيانات في
A
لا يتم احتساب السباق على flag
على أنّه تسابق بيانات،
لأنّ عمليات الوصول إلى الذاكرة العشوائية/الذرية ليست "عمليات وصول عادية إلى الذاكرة".
يجب إجراء عملية التنفيذ لمنع أو إخفاء عملية إعادة ترتيب الذاكرة بما يكفي لجعل التعليمة البرمجية مثل الاختبار الأساسي السابق يتصرف كما هو متوقع. يؤدي هذا عادةً إلى وصول البيانات المتقلبة أو الذرية إلى الذاكرة أكثر تكلفة بكثير من عمليات الوصول العادية.
رغم أن المثال السابق خالٍ من سباق البيانات، إلا أن القفل مع
Object.wait()
في Java أو متغيرات الحالة في C/C++ عادةً
لتوفير حل أفضل لا يتضمن الانتظار في حلقة أثناء
لاستنزاف طاقة البطارية.
عند ظهور إعادة ترتيب الذاكرة
إنّ البرمجة الخالية من سباق البيانات تغنينا عادةً عن التعامل بشكل صريح مع مشكلات إعادة ترتيب الوصول إلى الذاكرة. ومع ذلك، هناك العديد من الحالات في أي عمليات إعادة ترتيب تصبح مرئية:- إذا كان برنامجك يتضمّن خطأً يؤدي إلى تسابق غير مقصود للبيانات،
يمكن أن تصبح عمليات التحويل في المُجمِّع والأجهزة مرئية، وقد يكون سلوك
برنامجك مفاجئًا. على سبيل المثال، إذا نسينا الإعلان عن
قيمة
flag
في المثال السابق، قد ترى سلسلة المحادثات 2A
غير المهيأ. أو قد يقرر المُجمِّع أنّه لا يمكن تغيير العلامة أثناء حلقة "الخيط 2"، ويحوّل البرنامج إلىسلسلة المحادثات 1 سلسلة المحادثات 2 A = ...
flag = truereg0 = علم; بينما (!reg0) {}
... = Aflag
صحيح. - توفّر C++ مرافق لتخفيف
التماسك التسلسلي بشكل صريح حتى في حال عدم حدوث أي تنافس. العمليات الذرية
يمكن استخدام وسيطات
memory_order_
... واضحة. وبالمثل، توفّر حزمةjava.util.concurrent.atomic
مجموعة محدودة بشكل أكبر من المرافق المشابهة، لا سيماlazySet()
. وJava يستخدم المبرمجون أحيانًا سباقات البيانات المقصودة لتحقيق تأثير مماثل. تقدم كل هذه التحسينات في الأداء بشكل عام والتكلفة في تعقيد البرمجة. سنتناولها بشكل موجز فقط أدناه. - تم كتابة بعض رموز C وC++ بأسلوب قديم، وليس متناسقًا تمامًا
مع معايير اللغة الحالية، حيث يتم استخدام متغيّرات
volatile
بدلاً من متغيّراتatomic
، ولا يُسمح صراحةً بترتيب الذاكرة من خلال إدراج ما يُعرف باسم الحواجز أو الموانع. وهذا يتطلب استنتاجًا صريحًا بشأن إمكانية الوصول إلى البيانات إعادة ترتيب نماذج ذاكرة الأجهزة وفهمها. لا يزال أسلوب الترميز على هذا النحو مستخدمًا في نواة Linux. يجب عدم في تطبيقات Android الجديدة، ولن يتم تناولها بمزيد من التفصيل هنا.
التدرّب على القراءة
قد يكون تصحيح مشكلات اتساق الذاكرة أمرًا صعبًا للغاية. إذا كانت هناك
قفل أو سبب بيان atomic
أو volatile
بعض التعليمات البرمجية لقراءة البيانات القديمة، فقد لا تتمكن من
تعرَّف على السبب من خلال فحص عمليات تفريغ الذاكرة باستخدام برنامج تصحيح الأخطاء. بحلول الوقت الذي يمكنك فيه
استعلامًا لبرنامج تصحيح الأخطاء، فربما تكون جميع نوى وحدة المعالجة المركزية (CPU) قد رصدت المجموعة الكاملة من
البيانات، وستظهر محتويات الذاكرة وسجلات وحدة المعالجة المركزية (CPU)
حالة "مستحيلة".
الإجراءات التي يجب تجنُّبها في C
ونقدم هنا بعض الأمثلة على التعليمات البرمجية غير الصحيحة، إلى جانب طرق بسيطة وإصلاحها. قبل إجراء ذلك، علينا مناقشة استخدام إحدى ميزات اللغة الأساسية.
لغة C/C++ و"البيانات المتقلبة"
تعد تعريفات C وC++ volatile
أداة ذات أغراض خاصة للغاية.
وهي تمنع المجمِّع من إعادة ترتيب البيانات المتغيّرة أو إزالتها.
عمليات الدخول. ويمكن أن يكون ذلك مفيدًا في حالة وصول الرمز البرمجي إلى مسجِّلات الأجهزة،
ذاكرة يتم تعيينها إلى أكثر من موقع، أو فيما يتعلق
setjmp
ولكن لغة C وC++ volatile
، على عكس Java
volatile
، ليست مصمّمة للتواصل عبر سلاسل المحادثات.
في C وC++، قد تتم إعادة ترتيب عمليات الوصول إلى volatile
البيانات مع الوصول إلى البيانات غير القابلة للتغيير، ولا تتوفّر ضمانات
للوحدة. وبالتالي، لا يمكن استخدام volatile
لمشاركة البيانات بين مناقشات في رمز قابل للنقل، حتى على معالج أحادي. C volatile
عادةً لا
تمنع إعادة ترتيب الوصول بواسطة الجهاز، لذا فإنها في حد ذاتها أقل فائدة في
بيئات SMP متعددة السلاسل. هذا هو سبب دعم C11 وC++11
atomic
عناصر يجب عليك استخدامها بدلاً من ذلك.
لا يزال الكثير من رموز C وC++ القديمة يسيء استخدام volatile
لسلسلة المحادثات
التواصل. غالبًا ما يعمل هذا بشكل صحيح مع البيانات التي تناسب
في آلة تسجيل آلي، شريطة أن يتم استخدامه إما مع إطارات مخصصة أو في حالات
الذي لا يكون فيه ترتيب الذاكرة مهمًا. لكن ليس مضمونًا أن ينجح
بشكل صحيح مع برامج التجميع المستقبلية.
أمثلة
في معظم الحالات، يكون من الأفضل استخدام قفل (مثل
pthread_mutex_t
أو C+11 std::mutex
) بدلاً من
التشغيل الذري، ولكننا سنستخدم الأخير لتوضيح كيف يمكن
في موقف عملي.
MyThing* gGlobalThing = NULL; // Wrong! See below. void initGlobalThing() // runs in Thread 1 { MyStruct* thing = malloc(sizeof(*thing)); memset(thing, 0, sizeof(*thing)); thing->x = 5; thing->y = 10; /* initialization complete, publish */ gGlobalThing = thing; } void useGlobalThing() // runs in Thread 2 { if (gGlobalThing != NULL) { int i = gGlobalThing->x; // could be 5, 0, or uninitialized data ... } }
الفكرة هنا هي أننا نقوم بتخصيص هيكل وتهيئة حقوله ثم نهاية "نشر" لها بتخزينها في متغير عمومي. في هذه المرحلة، أي مؤشر ترابط آخر يمكنه رؤيتها، ولكن لا بأس بما أنّه تم إعدادها بالكامل، أليس كذلك؟
تكمن المشكلة في أنّه يمكن رصد متاجر "gGlobalThing
"
قبل تهيئة الحقول، ويحدث هذا عادةً بسبب أن المحول البرمجي أو
أعاد المعالج ترتيب المتاجر إلى gGlobalThing
thing->x
يمكن لسلسلة محادثات أخرى أن تقرأها من thing->x
.
ترى 5 أو 0 أو حتى بيانات غير مهيأة.
المشكلة الأساسية هنا هي سباق بيانات على gGlobalThing
.
في حال اتّصال Thread 1 بـ "initGlobalThing()
" أثناء الاتصال بشبكة Thread 2
المكالمات useGlobalThing()
، يمكن أن يكون gGlobalThing
التي تتم قراءتها أثناء الكتابة.
ويمكن حلّ هذه المشكلة من خلال الإعلان عن السمة gGlobalThing
باعتبارها
ذري. في لغة C++11:
atomic<MyThing*> gGlobalThing(NULL);
يضمن ذلك أن تكون عمليات الكتابة مرئية لسلاسل المحادثات الأخرى.
بالترتيب الصحيح. كما أنه يضمن منع حدوث بعض الأخطاء الأخرى
والأوضاع المسموح بها بخلاف ذلك، ولكن من غير المحتمل حدوثها على أرض الواقع
أجهزة Android. فعلى سبيل المثال، يضمن عدم تمكننا من رؤية
مؤشر gGlobalThing
الذي تمت كتابته جزئيًا فقط.
الإجراءات التي يجب تجنُّبها في Java
لم نناقش بعض ميزات لغة Java ذات الصلة، لذلك نظرة سريعة عليها أولاً.
لا تتطلّب Java من الناحية الفنية أن يكون الرمز خاليًا من تعارض البيانات. وهناك كمية صغيرة من رمز Java مكتوب بعناية شديدة يعمل بشكل صحيح في حال حدوث تعارض في البيانات. ومع ذلك، فإنّ كتابة هذه التعليمات البرمجية هو أمر صعب للغاية، وسنناقشه بشكل موجز أدناه فقط. لتحسين الأمور والأسوأ من ذلك، فإن الخبراء الذين حددوا معنى هذه التعليمات لم يعودوا يعتقدون المواصفات الصحيحة. (تتناسب المواصفات مع المحتوى الخالي من سباق البيانات الرمز).
في الوقت الحالي، نلتزم بالنموذج الخالي من سباق البيانات، والذي توفر له Java
هي في الأساس نفس الضمانات مثل C وC++. مرة أخرى، توفر اللغة
بعض الأساسيات التي تخفف بشكل صريح من الاتساق التسلسلي، ولا سيما
lazySet()
وweakCompareAndSet()
مكالمة
في java.util.concurrent.atomic
.
وكما هو الحال مع C وC++، سنتجاهل هذه الأنواع في الوقت الحالي.
لغة "مزامنة" Java و"متقلبة" كلمات رئيسية
توفر الكلمة الرئيسية "متزامن" القفل المدمج للغة Java الآلية. لكل عنصر "مراقبة" مرتبطة يمكن استخدامها لتقديم الوصول الحصري المتبادل. إذا حاولت مؤشّرَا تسلسل تنفيذ "مزامنة" العنصر نفسه، سينتظر أحدهما حتى ينتهي الآخر.
كما ذكرنا أعلاه، فإن volatile T
في Java هي تناظرية
atomic<T>
لـ C++11. عمليات الوصول المتزامنة إلى
يُسمح بحقلَين (volatile
)، ولا تؤدي إلى سباقات البيانات.
مع تجاهل lazySet()
وآخرين. وسباقات البيانات، فإن مهمة جهاز Java الافتراضي هو
والتأكد من أن النتيجة لا تزال تظهر متسقة بشكل تسلسلي.
على وجه التحديد، إذا كتبت سلسلة المحادثات 1 في حقل volatile
تقرأ سلسلة المحادثات 2 بعد ذلك من هذا الحقل ذاته وترى النص
فإن السلسلة 2 تضمن أيضًا أن ترى جميع الكتابة التي أجراها
السلسلة 1. من حيث تأثير الذاكرة، الكتابة إلى
وتكون القيمة المتطايرة مشابهة لإصدار الشاشة
القراءة من المتغير يشبه الحصول على شاشة.
هناك اختلاف واحد ملحوظ عن atomic
في C++:
إذا كتبنا volatile int x;
في Java، تكون قيمة x++
هي نفسها x = x + 1
؛ هو/هي
تُجري عملية تحميل ذري، وتزيد من النتيجة، ثم تستخدم تحليل ذري
المتجر. على عكس C++، فإن الزيادة ككل ليست بسيطة.
يتم توفير عمليات الزيادة الذرّية بدلاً من ذلك من قِبل
java.util.concurrent.atomic
.
أمثلة
إليك تنفيذ بسيط وغير صحيح للعدّاد الأحادي: (Java النظرية والممارسة: إدارة التقلّبات).
class Counter { private int mValue; public int get() { return mValue; } public void incr() { mValue++; } }
لنفترض أنّ get()
وincr()
يتم استدعاؤهما من عدة عوامل
ونرغب في التأكد من أن كل سلسلة محادثات تظهر العدد الحالي
تم الاتصال بـ "get()
". أما المشكلة الأكثر وضوحًا فهي
mValue++
هي في الواقع ثلاث عمليات:
reg = mValue
reg = reg + 1
mValue = reg
في حال تنفيذ سلسلتَي محادثات في incr()
في الوقت نفسه، سيتم تنفيذ سلسلة
قد تفقد التحديثات. لجعل الجزء صغير، يجب الإفصاح عن
incr()
"متزامن".
لا تزال هناك أعطال، لا سيما على منصة SMP. لا يزال هناك سباق بيانات،
حيث يمكن لـ get()
الوصول إلى mValue
بالتزامن مع
incr()
بموجب قواعد Java، يمكن تنفيذ إجراء استدعاء get()
يبدو أنّه تمت إعادة ترتيبها في ما يتعلّق برموز برمجية أخرى. على سبيل المثال، إذا نقرنا على اثنين
العدادات على التوالي، فقد تبدو النتائج غير متسقة
وذلك لأن عمليات الاتصال get()
التي أعدنا طلبها، إما بواسطة الجهاز أو
برنامج التجميع. يمكننا تصحيح المشكلة بإعلاننا أن get()
هو
متزامنة. بعد إجراء هذا التغيير، أصبح الرمز صحيحًا بوضوح.
ومع الأسف، لقد قدّمنا إمكانية التنافس مع قفل الباب، وهو ما قد يؤدي إلى
يمكن أن تعيق الأداء. بدلاً من تحديد get()
على أنّه
مزامَن، يمكننا تحديد mValue
باستخدام "متغيّر عشوائي". (ملاحظة:
يجب أن يستمر استخدام incr()
لsynchronize
لأنّه
mValue++
ليست عملية ذرية واحدة).
يؤدي هذا أيضًا إلى تجنب جميع سباقات البيانات، لذلك يتم الحفاظ على الاتساق التسلسلي.
سيكون incr()
أبطأ إلى حد ما، لأنّه يتسبب في كلّ من تكاليف المعالجة المصاحبة لدخول/خروج المراقبة
والتكاليف المصاحبة للتخزين المؤقت، ولكن
سيكون get()
أسرع، لذا حتى في حال عدم حدوث تعارض، سيكون
هذا الخيار مفيدًا إذا كان عدد عمليات القراءة يفوق عدد عمليات الكتابة بكثير. (راجع أيضًا AtomicInteger
للحصول على طريقة لإكمال
إزالة الجزء المتزامن.)
في ما يلي مثال آخر مشابه في الشكل لأمثلة C السابقة:
class MyGoodies { public int x, y; } class MyClass { static MyGoodies sGoodies; void initGoodies() { // runs in thread 1 MyGoodies goods = new MyGoodies(); goods.x = 5; goods.y = 10; sGoodies = goods; } void useGoodies() { // runs in thread 2 if (sGoodies != null) { int i = sGoodies.x; // could be 5 or 0 .... } } }
توجد نفس مشكلة الرمز C، أي وجود
سباق بيانات في sGoodies
. بالتالي يكون تعيين
قد تتم ملاحظة sGoodies = goods
قبل إعداد
الحقول في goods
. إذا أعلنت عن sGoodies
باستخدام
تمت استعادة كلمة رئيسية واحدة (volatile
) والاتساق التسلسلي، وستعمل
كما هو متوقع.
يُرجى العلم أنّ مرجع sGoodies
نفسه فقط هو مرجع متقلب. تشير رسالة الأشكال البيانية
إلى الحقول الموجودة بداخله. بعد تحديد قيمة السمة sGoodies
volatile
، ويتم الاحتفاظ بترتيب الذاكرة بشكل صحيح، الحقول
لا يمكن الوصول إليها بشكل متزامن. ستُجري العبارة z =
sGoodies.x
تحميلاً متغيِّرًا لـ MyClass.sGoodies
.
يليه حمولة غير متطايرة قيمتها sGoodies.x
. إذا أنشأت مرجعًا محليًاMyGoodies localGoods = sGoodies
، لن يؤدي الإجراء z =
localGoods.x
اللاحق إلى تحميل أيّ بيانات متقلبة.
هناك مصطلح أكثر شيوعًا في برمجة Java هو "المدقق المزدوج" قفلها":
class MyClass { private Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized (this) { if (helper == null) { helper = new Helper(); } } } return helper; } }
والفكرة هي أننا نريد الحصول على مثيل واحد من Helper
كائن مرتبط بمثيل MyClass
. يجب علينا إنشاء
مرة واحدة، لذلك ننشئه ونعيده من خلال getHelper()
مخصَّص
الأخرى. لتجنب تعارُض تنشئ فيه سلسلتان المثيل، نحتاج إلى
مزامنة إنشاء الكائن. ومع ذلك، لا نرغب في دفع النفقات العامة
الحظر "المتزامن" في كل استدعاء، لذا لا نقوم بهذا الجزء إلا إذا
حقل "helper
" فارغ حاليًا.
يتضمّن هذا تداخلًا في البيانات في حقل helper
. يمكن أن تكون
بالتزامن مع helper == null
في سلسلة محادثات أخرى.
ولمعرفة مدى نجاح ذلك،
نفس التعليمات البرمجية مع إعادة كتابتها قليلاً، كما لو تم تجميعها إلى لغة تشبه لغة C
(لقد أضفتُ حقلَين من حقول الأعداد الصحيحة لتمثيل Helper’s
.
نشاط الدالة الإنشائية):
if (helper == null) { synchronized() { if (helper == null) { newHelper = malloc(sizeof(Helper)); newHelper->x = 5; newHelper->y = 10; helper = newHelper; } } return helper; }
لا يوجد شيء يمنع الجهاز أو المحول البرمجي
بدءًا من إعادة ترتيب المتجر إلى "helper
" مع
x
/y
حقل يمكن لسلسلة محادثات أخرى العثور على
قيمة helper
غير خالية ولكن لم يتم ضبط حقولها بعد وهي جاهزة للاستخدام.
لمزيد من التفاصيل والمزيد من أوضاع الإخفاق، يمكنك الاطلاع على مقالة "تم التحقق من
يؤدي هذا الإجراء إلى ظهور رابط يؤدي إلى "بيان التعقيد" في الملحق لمزيد من التفاصيل.
71 ("استخدام التهيئة الكسولة بحكمة") في نموذج effective Java، لـ Josh Bloch
الإصدار الثاني..
هناك طريقتان لحلّ هذه المشكلة:
- افعل الشيء البسيط واحذف الفحص الخارجي. يضمن ذلك ألا
فحص قيمة
helper
خارج كتلة متزامنة. - يُرجى تعريف قيمة الحقل "
helper
" المتغيّرة. من خلال إجراء هذا التغيير الصغير، سيتم في المثال J-3 ستعمل بشكل صحيح على الإصدار 1.5 من Java والإصدارات الأحدث. (قد ترغب في أخذ دقيقة لإقناع نفسك أن هذا صحيح).
في ما يلي رسم توضيحي آخر لسلوك volatile
:
class MyClass { int data1, data2; volatile int vol1, vol2; void setValues() { // runs in Thread 1 data1 = 1; vol1 = 2; data2 = 3; } void useValues() { // runs in Thread 2 if (vol1 == 2) { int l1 = data1; // okay int l2 = data2; // wrong } } }
بالنظر إلى useValues()
، إذا لم ترصد سلسلة المحادثات 2 بعد
عند التحديث إلى vol1
، لن يتمكن البرنامج من معرفة ما إذا كانت data1
أم
تم ضبط data2
حتى الآن. بمجرد أن يرى التحديث إلى
يعلم "vol1
" أنّه يمكن الوصول إلى "data1
" بأمان
وقراءته بشكل صحيح دون بدء سباق البيانات. ومع ذلك،
ولا يمكنه وضع أي افتراضات حول data2
، لأن هذا المتجر كان
تنفيذها بعد التخزين المتقلب.
يُرجى العِلم أنّه لا يمكن استخدام السمة volatile
لمنع إعادة الطلب.
من عمليات الوصول الأخرى إلى الذاكرة
التي تتسرع في ما بينها ليس مضمونًا أن
إنشاء تعليمات حول إنشاء سياج الذاكرة للجهاز. يمكن استخدامها لمنع
سباقات البيانات من خلال تنفيذ التعليمات البرمجية فقط عندما تفي سلسلة محادثات أخرى
شرط معين.
الإجراءات المطلوبة
في لغة C/C++ ، يفضل C++11
مثل std::mutex
. إذا لم يكن كذلك، فاستخدم
عمليات pthread
المقابلة.
وتشمل هذه الإجراءات حواجز الذاكرة المناسبة التي توفّر سلوكًا صحيحًا (متسقًا تسلسليًا
ما لم يتم تحديد غير ذلك)
وفعّالًا على جميع إصدارات نظام Android الأساسي. فاحرص على الاستعانة بها
بشكل صحيح. على سبيل المثال، تذكر أن انتظار متغير الحالة قد يكون كاذبًا
بدون أن يتم الإشارة إليه، وبالتالي يجب أن يظهر في حلقة تكرار.
من الأفضل تجنُّب استخدام الدوالّ الذرية مباشرةً، ما لم تكن بنية البيانات التي تنفّذها بسيطة للغاية، مثل العداد. جارٍ قفل الباب يتطلب فتح قفل كائن pthread عملية واحدة لكل عنصر، وعادةً ما تكون أقل من تكلفة تخزين مؤقت واحدة، إذا لم تكن هناك التنافس، لذا لن توفروا الكثير من خلال استبدال مكالمات الاستبعاد المتبادل العمليات الذرية. تتطلب التصميمات الخالية من القفل لهياكل البيانات غير التافهة مزيد من الاهتمام لضمان تنفيذ عمليات ذات مستوى أعلى في هيكل البيانات تبدو ذرية (ككل، وليس فقط قطعها الذرية الصريحة).
إذا كنت تستخدم العمليات الذرية، فإن تخفيف الترتيب باستخدام
قد تتمكّن memory_order
... أو lazySet()
من تحسين الأداء.
ومزاياه، لكنها تتطلب فهمًا أعمق مما نقلناه حتى الآن.
يستخدم جزء كبير من الرمز الحالي
تم اكتشاف أنها تحتوي على أخطاء بعد حدوثها. تجنَّبها إن أمكن.
إذا كانت حالات الاستخدام لديك لا تتلاءم تمامًا مع أيٍ منها في القسم التالي،
فتأكد من أنك إما خبير أو استشارت أحد الخبراء.
تجنَّب استخدام volatile
في التواصل عبر سلاسل المحادثات في لغة C/C++.
في Java، غالبًا ما يكون أفضل حل لمشكلات التزامن هو
باستخدام فئة المنفعة المناسبة من
حزمة java.util.concurrent
. الكود مكتوب بشكل جيد
اختباره على SMP.
ربما يكون الخيار الأكثر أمانًا هو جعل عناصرك غير قابلة للتغيير. أغراض من فئات مثل سلسلة جافا والعدد الصحيح بيانات لا يمكن تغييرها بمجرد المستخدم، مع تجنب كل احتمالية لسباقات البيانات على تلك الكائنات. يعتبر الكتاب تتضمن Java، الإصدار الثاني تعليمات محددة في "العنصر 15: تقليل قابلية التغيُّر". ملاحظة في وخاصة أهمية إعلان حقول Java "نهائية" (Bloch):
حتى إذا كان الكائن غير قابل للتغيير، فتذكر أن توصيله إلى شخص آخر
تعد سلسلة التعليمات بدون أي نوع من التزامن سباق بيانات. قد يكون هذا الإجراءمقبولاً في Java في بعض الأحيان (راجِع المعلومات أدناه)، ولكنه يتطلب عناية كبيرة، ومن المرجّح أن يؤدي إلى استخدام رمزبرمجي
غير مستقر. إذا لم يكن الأداء بالغ الأهمية، أضف
بيان "volatile
" في C++، يُعدّ إرسال مؤشر أو
إشارة إلى عنصر ثابت بدون مزامنة مناسبة،
مثل أي تنافس على البيانات، خطأً.
في هذه الحالة، من المرجّح أن يؤدي ذلك إلى حدوث أعطال متقطعة، لأنّه،
على سبيل المثال، قد يرصد مؤشر جدول الطرق
في سلسلة المعالجة المستلِمة عدم بدء عملية الإعداد بسبب إعادة ترتيب المتجر.
إذا لم تكن فئة مكتبة حالية أو فئة غير قابلة للتغيير
مناسبة، يجب استخدام بيان synchronized
في Java أو C++
lock_guard
/ unique_lock
للحماية
من الوصول إلى أي حقل يمكن الوصول إليه من خلال أكثر من سلسلة محادثات واحدة. في حال عدم استجابة كائنات المزامنة
بما يناسب حالتك، يجب أن تذكر الحقول المشتركة
volatile
أو atomic
، ولكن عليك توخي الحذر الشديد
فهم التفاعلات بين السلاسل. لن تتضمن هذه البيانات
أن تتجنب أخطاء البرمجة الشائعة المتزامنة، لكنها ستساعدك
تجنب الأخطاء الغامضة المرتبطة بتحسين برامج التحويل البرمجي وبروتوكول SMP
وحوادث السير.
يجب تجنب "النشر" مرجعًا إلى كائن، أي إتاحته للآخرين في الدالة الإنشائية. يكون هذا الأمر أقل أهمية في C++ أو إذا التزمت بنصيحة "عدم حدوث تعارض في البيانات" في Java. ومع ذلك، هذه نصيحة جيدة دائمًا، وتصبح مهمة جدًا إذا تم تنفيذ رمز Java في سياقات أخرى يكون فيها نموذج أمان Java مهمًا، وقد يؤدي الرمز غير الموثوق به إلى حدوث تداخل في البيانات من خلال الوصول إلى مرجع العنصر "المسرَّب". من الضروري أيضًا تجاهل التحذيرات واستخدام بعض أساليب في القسم التالي. راجِع (أساليب الإنشاء الآمنة في Java) للحصول على التفاصيل
مزيد من المعلومات حول طلبات الذاكرة الضعيفة
يوفر لغة C++11 والإصدارات الأحدث آليات واضحة لتخفيف التسلسل
يضمن الاتساق للبرامج الخالية من سباق البيانات. موسيقى فاضحة
memory_order_relaxed
، memory_order_acquire
(عمليات التحميل
فقط) وmemory_order_release
(المتاجر فقط) للوسيطات البسيطة
كل عملية توفر ضمانات أضعف بشدة من النسبة الافتراضية،
ضمني، memory_order_seq_cst
. memory_order_acq_rel
يوفر كلاً من memory_order_acquire
و
memory_order_release
ضمانات لعمليات قراءة تعديل كتابة جوهرية
. memory_order_consume
ليست كافية بعد
محددة جيدًا أو مُنفذة لتكون مفيدة، ويجب تجاهلها في الوقت الحالي.
تشبه طرق lazySet
في Java.util.concurrent.atomic
متاجر memory_order_release
في C++. لغة Java
تُستخدم المتغيرات العادية أحيانًا كبديل
memory_order_relaxed
إذن بالوصول، على الرغم من أنّها في الواقع
حتى أضعف. على عكس C++، لا تتوفّر آلية حقيقية للوصول غير المُرتَّب
إلى المتغيّرات التي تمّ الإعلان عنها على أنّها volatile
.
ويجب تجنُّبها بشكل عام ما لم تكن هناك أسباب ملحة بشأن الأداء واستخدامها. في معماريات الأجهزة ذات الترتيب الضعيف، مثل ARM، يؤدي استخدامها عادةً إلى تقليل عدد دورات الآلة بضع دقائق لكل عملية ذرية. على نظام التشغيل x86، يقتصر تعزيز الأداء على المتاجر، ومن المرجّح أن يكون بشكل ملحوظ. على عكس المألوف، قد تنخفض الفائدة مع زيادة عدد النوى، لأنّ نظام الذاكرة يصبح عاملاً محددًا بشكل أكبر.
إنّ الدلالات الكاملة للعمليات الذرية ذات الترتيب الضعيف معقّدة. بشكل عام، إنها تتطلب على فهم دقيق لقواعد اللغة، والتي لا تدخل هنا. مثلاً:
- يمكن للمحول البرمجي أو الجهاز نقل
memory_order_relaxed
إلى (وليس خارجه) قسم مهم محاط بقفل الاستحواذ والإصدار. هذا يعني أن اثنين قد يظهر متجران (memory_order_relaxed
) غير متوفّرَين. حتى لو كانت مفصولة بأجزاء مهمة - قد يظهر متغير Java عادي، عند إساءة استخدامه كعدّاد مشترك،
إلى سلسلة محادثات أخرى لتقليلها، على الرغم من أنها تتزايد بمقدار سلسلة
سلسلة محادثات أخرى. لكن هذا لا ينطبق على لغة C++ البسيطة
memory_order_relaxed
لذلك كتحذير، هنا نقدم عددًا صغيرًا من التعبيرات الاصطلاحية التي يبدو أنها تغطي العديد من طرق حالات للذرات ذات الترتيب الضعيف. لا ينطبق العديد منها إلا على C++.
أذونات الوصول غير المتعلّقة بالسباقات
من الشائع إلى حد ما أن يكون المتغير ذرّيًا لأنه يحدث أحيانًا
بالتزامن مع الكتابة، ولكن لا تظهر هذه المشكلة لبعض عمليات الوصول.
على سبيل المثال، قد يحتاج المتغيّر
إلى أن يكون ذريًا لأنّه تتم قراءته خارج قسم حرج، ولكن يتم حماية كل تعديلات
بواسطة قفل. في هذه الحالة، فإن القراءة التي تصادف
محميًا بنفس القفل
لأنه لا يمكن أن تكون هناك عمليات كتابة متزامنة. في هذه الحالة،
وصول غير مصنف (تحميل في هذه الحالة)، يمكن إضافة تعليق توضيحي باستخدام
memory_order_relaxed
بدون تغيير صحة رمز C++.
يؤدي تنفيذ القفل إلى فرض ترتيب الذاكرة المطلوب
في ما يتعلق بالوصول من خلال سلاسل المحادثات الأخرى، وmemory_order_relaxed
أنه في الأساس لا حاجة إلى فرض قيود إضافية على الطلب
يتم فرضه على إمكانية الوصول الذري.
لا يوجد تناظر حقيقي لهذا في Java.
لا يُعتمَد على النتيجة لتحديد صحتها
وعندما نستخدم عبء تحميل سريع فقط لتقديم تلميح، لا بأس بشكل عام
عدم فرض أي ترتيب للذاكرة للتحميل. إذا كانت القيمة
غير موثوق بها، كما لا يمكننا استخدام النتيجة بشكل موثوق لاستنتاج أي شيء عن
المتغيرات الأخرى. لذا لا بأس
إذا لم يكن ترتيب الذاكرة مضمونًا، وكانت نسبة التحميل
يتم تقديمه مع الوسيطة memory_order_relaxed
.
من الشائع
مثال على ذلك هو استخدام لغة C++ compare_exchange
لاستبدال x
بشكل ذري بـ f(x)
.
التحميل الأولي لـ x
لحساب f(x)
أن تكون موثوقة. إذا أخطأنا، فإن
لن تنجح عملية compare_exchange
وسنعيد المحاولة.
لا بأس أن يتم استخدام التحميل المبدئي لـ x
وسيطة memory_order_relaxed
؛ ترتيب الذاكرة فقط
للمسائل القانونية الـ compare_exchange
الفعلية.
بيانات معدَّلة جزئيًا ولكنها غير مقروءة
في بعض الأحيان يتم تعديل البيانات بالتوازي من خلال سلاسل متعددة، ولكن
حتى تكتمل العملية الحسابية المتوازية. ومن الأمثلة الجيدة على ذلك العداد الذي يتمّ زيادته بشكلٍ ذري (مثلاً باستخدام fetch_add()
في C++ أو
atomic_fetch_add_explicit()
في C) من خلال سلاسل مهام متعدّدة بشكلٍ موازٍ، ولكن يتم تجاهل نتيجة هذه المكالمات
دائمًا. تتم قراءة القيمة الناتجة في النهاية فقط،
بعد اكتمال جميع التحديثات.
في هذه الحالة، ليست هناك طريقة لمعرفة ما إذا كان يمكن الوصول إلى هذه البيانات أم لا
قد تمت إعادة ترتيبه، وبالتالي قد يستخدم رمز C++ العلامة memory_order_relaxed
الوسيطة.
وتعدّ عدادات الأحداث البسيطة مثالاً شائعًا على ذلك. وبما أنّه شائع جدًا، من المفيد إجراء بعض الملاحظات حول هذه الحالة:
- يؤدي استخدام
memory_order_relaxed
إلى تحسين الأداء، ولكنها قد لا تعالج أهم مشكلة في الأداء، وهي أن كل تحديث يتطلب وصولاً حصريًا إلى سطر ذاكرة التخزين المؤقت الذي يحتوي على العدّاد. هذا النمط ينتج عنها فقدان ذاكرة التخزين المؤقت في كل مرة تصل فيها سلسلة محادثات جديدة إلى العدّاد. إذا كانت التحديثات متكررة وتتناوب بين سلاسل المحادثات، سيكون الأمر أسرع بكثير. لتجنب تحديث العدّاد المشترك في كل مرة، على سبيل المثال، استخدام عدّادات سلاسل المحادثات المحلية وتجميعها في النهاية. - يمكن دمج هذا الأسلوب مع القسم السابق: من الممكن
القيم التقريبية وغير الموثوقة بشكل متزامن أثناء تحديثها،
مع جميع العمليات التي تستخدم
memory_order_relaxed
. لكن من المهم التعامل مع القيم الناتجة على أنها غير موثوقة على الإطلاق. فقط لأن العدد يبدو أنه قد زاد مرة واحدة لا ما يعني أنّه يمكن الاعتماد على سلسلة محادثات أخرى للوصول إلى هذه النقطة الذي تم فيه تنفيذ الزيادة. وقد يحتوي الجزء بدلاً من ذلك على تمت إعادة ترتيبه باستخدام التعليمة البرمجية السابقة. (بالنسبة للحالة المشابهة، ذكرنا إلا أن C++ لا يضمن أن التحميل الثاني لمثل هذا العداد لن إرجاع قيمة أقل من تحميل سابق في نفس سلسلة المحادثات. ما لم يكن بالطبع قد فاض العداد). - من الشائع العثور على رمز يحاول احتساب قيم تقريبية للعداد من خلال إجراء عمليات قراءة وكتابة فردية (أو غير فردية) ذرية، ولكن بدون جعل الزيادة ككل ذرية. الوسيطة المعتادة هي أن هذا "قريب بما فيه الكفاية" لعدّادات الأداء أو ما شابه ذلك لا يحدث ذلك عادةً. عندما تكون التعديلات متكرّرة بما يكفي (وهو ما يهمّك على الأرجح)، يتم عادةً فقدان جزء كبير من الأعداد. ففي جهاز رباعي النواة، قد تفقد عادةً أكثر من نصف الأعداد. (تمرين سهل: إنشاء سيناريو من سلسلتين يكون فيه العدّاد عدد مرات التحديث مليون مرة، غير أن قيمة العدّاد النهائية تساوي واحدًا).
التواصل بسهولة مع العلم
مخزن memory_order_release
(أو عملية القراءة والتعديل والكتابة)
يضمن أنه إذا تم تحميل memory_order_acquire
لاحقًا
(أو عملية القراءة والتعديل والكتابة) تقرأ القيمة المكتوبة، ثم
مراقبة أي مخازن (عادية أو ذرية) تسبق
متجر على "memory_order_release
" وعلى العكس، يمكن لأي عمليات تحميل
الذي يسبق memory_order_release
لن يتم ملاحظة أي
المتاجر التي اتبعت تحميل memory_order_acquire
.
على عكس memory_order_relaxed
، يسمح هذا الإجراء بهذه العمليات البسيطة.
للاستخدام لتوصيل تقدم سلسلة التعليمات إلى أخرى.
على سبيل المثال، يمكننا إعادة كتابة مثال القفل الذي تم التحقق منه مرتين من أعلى في C++ باسم
class MyClass { private: atomic<Helper*> helper {nullptr}; mutex mtx; public: Helper* getHelper() { Helper* myHelper = helper.load(memory_order_acquire); if (myHelper == nullptr) { lock_guard<mutex> lg(mtx); myHelper = helper.load(memory_order_relaxed); if (myHelper == nullptr) { myHelper = new Helper(); helper.store(myHelper, memory_order_release); } } return myHelper; } };
يضمن متجر التحميل والإصدار الذي يتم الحصول عليه أنه إذا وجدنا قيمة
helper
، سنرى أيضًا أنّه قد تم إعداد الحقول بشكل صحيح.
لقد أدرجنا أيضًا الملاحظة السابقة التي مفادها أنه يتم تحميل غير المشاركة
استخدام memory_order_relaxed
.
يمكن أن يمثّل مبرمج Java helper
كـ
java.util.concurrent.atomic.AtomicReference<Helper>
واستخدام lazySet()
كمتجر الإصدارات التحميل
أن العمليات ستستمر في استخدام طلبات get()
العادية.
في كلتا الحالتين، تركَّز التعديل في الأداء على إعداد المسار، والذي من غير المرجح أن يؤثر سلبًا في الأداء. يمكن أن يكون الحلّ الوسط الأسهل للقراءة هو:
Helper* getHelper() { Helper* myHelper = helper.load(memory_order_acquire); if (myHelper != nullptr) { return myHelper; } lock_guard<mutex> lg(mtx); if (helper == nullptr) { helper = new Helper(); } return helper; }
يوفر هذا المسار السريع نفسه، لكن يلجأ إلى الإعداد الافتراضي، متسقة بالتسلسل، على العمليات البطيئة التي لا تؤدي إلى أداء .
حتى هنا، helper.load(memory_order_acquire)
من المحتمل أن ينشئوا الرمز نفسه على الأجهزة الحالية
الهياكل كمرجع عادي (متسق بشكل متسلسل)
helper
إنّ هذا التحسين هو الأكثر فائدةً هنا.
هي إدخال myHelper
للتخلص من
التحميل الثاني، على الرغم من أن برنامج التحويل البرمجي المستقبلي قد يقوم بذلك تلقائيًا.
لا يمنع طلب الشراء أو التحرير المتاجر من الظهور بشكل واضح
متأخرة، ولا تضمن ظهور المتاجر لسلاسل محادثات أخرى.
بترتيب ثابت. ونتيجة لذلك، فهي لا تدعم طريقة حل
ولكن نمط برمجة شائع إلى حد ما يتضح من الاستبعاد المتبادل لديكر
الخوارزمية: تضع جميع سلاسل المحادثات أولاً علامة تشير إلى أنها تريد تنفيذ
شيء ما إذا كانت سلسلة المحادثات t تشير إلى عدم ظهور سلسلة محادثات أخرى
تحاول القيام بشيء ما، فيمكنها المتابعة بأمان، مع العلم أنها
لن يكون هناك أي تداخل. لن يتم إجراء أي سلسلة محادثات أخرى.
من المتابعة، لأنّ علامة t لا تزال مضبوطة. تعذّر تنفيذ هذا الإجراء
إذا تمّ الوصول إلى العلامة باستخدام طلب الاكتساب/الإصدار، لأنّ ذلك لا يتمّ
منع ظهور علامة سلسلة المحادثات للآخرين في وقت متأخر، بعد
العملية بشكل خاطئ. يمنع الإعداد التلقائي memory_order_seq_cst
ذلك.
الحقول غير القابلة للتغيير
إذا تمّت بدء تشغيل حقل عنصر عند الاستخدام الأوّل ولم يتمّ تغييره مطلقًا،
قد يكون من الممكن بدء تشغيله وقراءته لاحقًا باستخدام عمليات وصول مرتبة بشكل ضعيف. في لغة C++ ، يمكن تعريفها على أنّها atomic
.
ويتم الوصول إليه باستخدام memory_order_relaxed
أو في Java،
يمكن تعريفه بدون volatile
والوصول إليه بدون
التدابير الخاصة. ويتطلّب ذلك جميع عمليات تجميد البيانات التالية:
- ينبغي أن يكون بالإمكان تحديد قيمة الحقل نفسه ما إذا كان قد تم إعداده بالفعل. للوصول إلى الحقل، يجب أن تقرأ قيمة اختبار المسار السريع وإرجاعه الحقل مرة واحدة فقط. في Java، الأخير ضروري. حتى لو تم إعداد الاختبارات الميدانية، وقد يقرأ التحميل الثاني القيمة السابقة غير المهيأة. في لغة C++ "القراءة مرة واحدة" والقاعدة هي مجرد ممارسة جيدة.
- يجب أن تكون كل من عمليات الإعداد والتحميلات اللاحقة متكافئة،
في هذه التحديثات الجزئية أن تكون غير مرئية. بالنسبة لـ Java، يمثل حقل
يجب ألا تكون السمة
long
أوdouble
. بالنسبة للغة C++، يلزم تخصيص تخصيص ذري؛ ولن تنجح بنائها في مكانها الصحيح، بنيةatomic
ليست بسيطة. - يجب أن تكون عمليات الإعداد المتكرّرة آمنة، لأنّ سلاسل المحادثات المتعدّدة يجب أن تكون آمنة. القيمة غير المُعدّة بشكل متزامن. في لغة C++، يكون هذا بشكل عام من "الرسومات البسيطة القابلة للنسخ" المتطلبات المفروضة على الجميع الأنواع الذرّية أنواع ذات مؤشرات مملوكة متداخلة تتطلب الصفقة في ولن تكون قابلة للنسخ بطريقة تافهة. بالنسبة لـ Java، تكون بعض أنواع المراجع المقبولة:
- تقتصر مراجع Java على أنواع غير قابلة للتغيير تحتوي على عناصر نهائية فقط الحقول. يجب عدم نشر الدالة الإنشائية من النوع غير القابل للتغيير مرجعًا للكائن. وهي في هذه الحالة قواعد حقل Java النهائية إذا رأى القارئ المرجع، فسيشاهد أيضًا النهائية المهيأة. ليس لـ C++ أي تناظرية لهذه القواعد مؤشرات إلى الكائنات المملوكة غير مقبولة لهذا السبب أيضًا (في بالإضافة إلى انتهاك "سياسة قابلية النسخ بطريقة تافهة" متطلبات الجودة).
الملاحظات الختامية
على الرغم من أن هذا المستند لا يقتصر على مجرد خدش السطح، في التعامل مع أكثر من مجرد إضعاف سطحي. هذا موضوع واسع جدًا وعميق. في ما يلي بعض المجالات التي يمكنك استكشافها بشكل أكبر:
- يتم التعبير عن نماذج ذاكرة Java وC++ الفعلية باستخدام
العلاقة happing-before التي تحدد متى يمكن ضمان إجراءين
أن تحدث بترتيب معين. عندما أشرنا إلى سباق البيانات، فإننا بشكل غير رسمي
عن عمليتَي وصول إلى الذاكرة تحدث "في الوقت نفسه".
ويُعرف ذلك رسميًا بأنّه لا يحدث أحدهما قبل الآخر.
من المفيد معرفة التعريفات الفعلية لما يحدث قبل
ويتزامن مع في نموذج ذاكرة Java أو C++.
على الرغم من أنّ المفهوم البديهي "بشكل متزامن" جيد بشكل عام،
إلا أنّ هذه التعريفات مفيدة، خاصةً إذا كنت تفكر في استخدام عمليات اتّحادية ذات ترتيب ضعيف في C++.
(لا تحدّد مواصفات Java الحالية سوى
lazySet()
بشكل غير رسمي جدًا). - استكشف ما يُسمح به وما لا يُسمح له بالمحولات البرمجية عند إعادة ترتيب التعليمات البرمجية. (تتضمّن مواصفات JSR-133 بعض الأمثلة الرائعة على عمليات التحويل القانونية التي تؤدي إلى نتائج غير متوقّعة).
- تعرف على كيفية كتابة فئات غير قابلة للتغيير في Java وC++. (هناك المزيد من مجرد "عدم تغيير أي شيء بعد البناء").
- استيعاب التوصيات في قسم التزامن في Java، الإصدار الثاني (على سبيل المثال، يجب تجنب استدعاء الطرق التي أن يتم تجاوزه أثناء وجوده داخل كتلة متزامنة).
- اطّلِع على واجهات برمجة التطبيقات
java.util.concurrent
وjava.util.concurrent.atomic
للتعرّف على الميزات المتاحة. ننصحك باستخدام تعليقات توضيحية متزامنة مثل@ThreadSafe
@GuardedBy
(من net.jcip.annotations)
ويحتوي قسم القراءة الإضافية في الملحق على روابط تؤدي إلى والوثائق ومواقع الويب التي ستوضح هذه الموضوعات بشكل أفضل.
الملحق
تنفيذ مخازن المزامنة
(ليس هذا الأمر من الأمور التي سينفّذها معظم المبرمجين، ولكنّ المناقشة مفيدة.)
للأنواع المدمجة الصغيرة من الأجهزة مثل int
والأجهزة المتوافقة مع
بالنسبة إلى Android، تضمن تعليمات التحميل والتخزين العادية أن
ستظهر إما بشكل كامل أو لن تكون مرئية على الإطلاق لشخص آخر
معالج بيانات الجهاز الذي يحمِّل الموقع نفسه. وبالتالي، يتم تقديم بعض المفاهيم الأساسية
حول "الوحدة" مجانًا.
وكما رأينا في السابق، لا يكفي ذلك. من أجل ضمان التسلسل الاتساق نحتاج إليه أيضًا لمنع إعادة ترتيب العمليات، والتأكد أن عمليات الذاكرة أصبحت مرئية للعمليات الأخرى بطريقة متسقة طلبك. يتضح أن الطريقة الأخيرة تلقائية على الأجهزة المتوافقة مع Android شريطة أن نتّخذ خيارات حكمة لفرض الإجراءات السابقة لذلك نتجاهلها إلى حد كبير هنا.
يتم الاحتفاظ بترتيب عمليات الذاكرة من خلال منع إعادة الترتيب بواسطة المحول البرمجي، ومنع إعادة الترتيب بواسطة الجهاز. نحن هنا نركز الأخيرة.
يتم فرض ترتيب الذاكرة على ARMv7 وx86 وMIPS من خلال
"سياج" تعليمات
تمنع بشكل تقريبي التعليمات التي تلي السياج من أن تظهر
قبل التعليمات التي تسبق السياج. (يُشار إلى هذه التعليمات أيضًا عادةً باسم "تعليمات الحواجز"، ولكن قد يؤدي ذلك إلى الخلط بينها وبين الحواجز بأسلوب
pthread_barrier
التي تؤدي إلى نتائج أكثر فعالية
من ذلك). المعنى الدقيق
تعد إرشادات السياج موضوعًا معقدًا إلى حد ما يجب معالجته
والطريقة التي تعتمد بها الضمانات التي توفرها أنواع متعددة ومختلفة من السياج
التفاعل، وكيفية دمج هذه مع ضمانات الطلب الأخرى عادةً
المقدمة من الأجهزة. هذه نظرة عامة عالية المستوى، لذلك
على هذه التفاصيل.
إنّ النوع الأساسي من ضمان الطلب هو توفّره C++.
memory_order_acquire
وmemory_order_release
العمليات البسيطة: عمليات الذاكرة التي تسبق مخزن الإصدارات
يجب أن يظهر بعد تحميل البيانات التي تم اكتسابها. على ARMv7، هذه
تم الفرض من قِبل:
- تقديم تعليمات مناسبة حول السياج قبل تعليمات المتجر يمنع هذا الإجراء إعادة ترتيب جميع عمليات الوصول السابقة إلى الذاكرة من خلال التعليمات الخاصة بالمتجر. (يؤدي ذلك أيضًا إلى منع إعادة الطلب بدون داعٍ باستخدام تعليمات المتجر اللاحقة).
- ومن خلال اتباع تعليمات التحميل مع تعليمات السياج المناسبة، مما يمنع إعادة ترتيب التحميل مع عمليات الوصول اللاحقة. (ومرة أخرى، يتم توفير ترتيب غير مطلوب مع عمليات التحميل السابقة على الأقل).
معًا قد تكفي طلبات C++ للحصول على الطلبات أو إصدارها معًا.
وهي ضرورية، ولكنّها ليست كافية، لاستخدام Java volatile
أو C++ بشكل تسلسلي متّسق atomic
.
لمعرفة ما نحتاجه أيضًا، ضع في اعتبارك جزء خوارزمية ديكر
التي ذكرناها بإيجاز في وقت سابق.
flag1
وflag2
هما C++ atomic
أو متغيرات Java volatile
، وكلاهما يكون false في البداية.
سلسلة المحادثات 1 | سلسلة المحادثات 2 |
---|---|
flag1 = true |
flag2 = true |
يعني الاتساق التسلسلي أن إحدى المهام إلى
يجب تنفيذ flag
n أولاً، وأن يظهر من خلال
اختباره في سلسلة المحادثات الأخرى. وبالتالي، لن نلاحظ أبدًا
سلاسل المحادثات هذه التي تنفّذ "العناصر المهمة" في الوقت نفسه.
لكن السياج المطلوب لطلب الإفراج عن طريق الاستحواذ يضيف فقط
والأسوار في بداية ونهاية كل سلسلة محادثات، إلا أن هذا لا يساعد
هنا. نحتاج أيضًا إلى التأكّد من أنّه إذا كان
تخزين volatile
/atomic
يليه
تحميل volatile
/atomic
، لا تتم إعادة ترتيب الاثنين.
يتم فرض هذا عادةً عن طريق إضافة سياج ليس فقط قبل
متسقًا بشكل متسلسل ولكن بعده أيضًا.
(هذا أيضًا أقوى بكثير من المطلوب، لأن هذا السياج يطلب عادةً
جميع محاولات الوصول السابقة إلى الذاكرة في ما يتعلق بجميع عمليات الوصول اللاحقة إلى الذاكرة).
بدلاً من ذلك، يمكننا ربط السياج الإضافي بالتسلسل عمليات تحميل متسقة. وبما أنّ المتاجر أقلّ تكرارًا، فإنّ الاصطلاح الذي وصفناه هو الأكثر شيوعًا والأكثر استخدامًا على Android.
كما رأينا في قسم سابق، نحتاج إلى إدراج حاجز للتخزين/التحميل بين العمليتين. يشير هذا المصطلح إلى الرمز البرمجي الذي يتم تنفيذه في الجهاز الافتراضي (VM) لإجراء عملية وصول متغيّرة. على النحو التالي:
الحمل المتغيّر | متجر متقلّب |
---|---|
reg = A |
fence for "release" (2) |
توفر بُنى الآلة الحقيقية عادةً أنواعًا متعددة من والأسوار التي ترتيب أنواعًا مختلفة من نقاط الوصول وقد يكون لها تكلفة مختلفة. الاختيار بينهما دقيق ومتأثر إلى التأكّد من أنّ المتاجر مرئية للنواة الأخرى في بترتيب متسق، وأن ترتيب الذاكرة الذي تفرضه تركيبة من الأسوار المتعددة تؤلف بشكل صحيح. لمزيد من التفاصيل، يُرجى الاطّلاع على صفحة جامعة كامبريدج التي تتضمّن عمليات الربط المجمّعة للعمليات الذرّية بالمعالجات الفعلية.
في بعض البُنى الأساسية، لا سيما في x86، يتطلب "اكتساب" و"إصدار" عقبات غير ضرورية، نظرًا لأن الأجهزة دائمًا ما لا تفرض الطلب بشكل كافٍ. وبالتالي، على x86، يتم إنشاء الفاصل الأخير (3) فقط. وبالمثل في x86، فإن atomic read-modify- write العمليات تتضمن ضمنيًا حاجزًا قويًا. ومن ثم لا تكون هذه تتطلب أي أسوار. في ARMv7، يجب استخدام كل حدود الذاكرة التي ناقشناها أعلاه.
ويوفر ARMv8 تعليمات LDAR وSTLR التي فرض متطلبات لغة Java المتغيّرة أو لغة C++ المتسقة بالتسلسل التحميل والمخازن. ويساعد ذلك في تجنُّب القيود غير الضرورية لإعادة الترتيب التي ذكرناها أعلاه. ويستخدم رمز Android 64 بت على معالجات ARM ما يلي: اخترنا أن التركيز على وضع السياج ARMv7 هنا لأنه يسلط المزيد من الضوء على المتطلبات الفعلية.
محتوى إضافي للقراءة
صفحات الويب والوثائق التي توفر قدرًا أكبر من العمق أو الاتساع. كلما كانت البيانات مفيدة بشكل عام المقالات تكون أقرب إلى أعلى القائمة.
- نماذج اتّساق الذاكرة المشتركة: برنامج تعليمي
- كتبت عام 1995 من تأليف "أدفي" Gharachorloo، هذا مكان جيد للبدء إذا كنت ترغب في التعمق أكثر في نماذج اتساق الذاكرة.
http://www.hpl.hp.com/techreports/Compaq-dec/WRL-95-7.pdf - حواجز الذاكرة
- مقالة صغيرة ورائعة تلخّص المشاكل
https://en.wikipedia.org/wiki/Memory_barrier - أساسيات سلاسل المحادثات
- مقدمة عن البرمجة المتعدّدة سلاسل المحادثات بلغة C++ وJava من إعداد "هانز بوهم" مناقشة سباقات البيانات وطرق المزامنة الأساسية.
http://www.hboehm.info/c++mm/threadsintro.html - تزامن Java عمليًا
- تم نشر هذا الكتاب في عام 2006، ويتناول مجموعة واسعة من المواضيع بتفصيل كبير. يُنصح به بشدة لأي شخص يكتب رمزًا برمجيًا متعدد السلاسل بلغة Java.
http://www.javaconcurrencyinpractice.com - الأسئلة الشائعة حول JSR-133 (نموذج ذاكرة Java)
- مقدّمة بسيطة عن نموذج ذاكرة Java، تتضمّن شرحًا حول المزامنة والمتغيّرات المتغيرة وإنشاء الحقول النهائية.
(قديم قليلاً، لا سيما عندما يناقش لغات أخرى.)
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html - صلاحية عمليات تحويل البرامج في نموذج ذاكرة Java
- شرح تقني إلى حد ما للمشاكل المتبقية في
نموذج ذاكرة Java لا تنطبق هذه المشاكل على المحتوى الخالي من سباق البيانات
والبرامج.
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.112.1790&rep=rep1&type=pdf - نظرة عامة على الحزمة java.util.concurrent
- مستندات حزمة
java.util.concurrent
بالقرب من أسفل الصفحة، يظهر قسم بعنوان "خصائص توافق الذاكرة" الذي يشرح الضمانات التي تقدّمها الفئات المختلفة.java.util.concurrent
ملخّص الحزمة - نظرية وممارسة Java: أساليب الإنشاء الآمنة في Java
- تتناول هذه المقالة بالتفصيل مخاطر تجنّب المراجع أثناء إنشاء العناصر، كما تقدّم إرشادات حول دوال الإنشاء الآمنة لسلاسل المحادثات.
http://www.ibm.com/developerworks/java/library/j-jtp0618.html - نظرية وممارسة Java: إدارة التقلبات
- مقالة رائعة تصف ما يمكنك تحقيقه وما لا يمكنك تحقيقه باستخدام الحقول التي تتغيّر قيمتها بشكل متكرر في Java
http://www.ibm.com/developerworks/java/library/j-jtp06197.html - بيان " Double-Checked Locking is Broken"
- شرح "بيل بوغ" بالتفصيل الطرق المختلفة التي يتم من خلالها كسر القفل باستخدام ميزة "التحقّق مرّتين" بدون استخدام "
volatile
" أو "atomic
". يتضمن لغتي C/C++ وJava.
http://www.cs.umd.edu/~pugh/java/memoryModel/wellCheckedLocking.html - [ARM] اختبارات الحاجز الحمضي وكتاب الطبخ
- مناقشة حول مشاكل بروتوكول ARM SMP، لإلقاء الضوء على مقتطفات قصيرة من رموز ARM. إذا وجدت أن الأمثلة في هذه الصفحة غير محدّدة للغاية، أو إذا كنت تريد قراءة الوصف الرسمي لتعليمات DMB، اقرأ هذا. كما يصف أيضًا التعليمات المستخدمة لحواجز الذاكرة في التعليمات البرمجية القابلة للتنفيذ (ربما يكون ذلك مفيدًا في حالة إنشاء التعليمات البرمجية بسرعة). تجدر الإشارة إلى أن هذا يسبق استخدام ARMv8، الذي
يتوافق مع تعليمات إضافية لترتيب الذاكرة وتم نقلها إلى قاعدة أقوى نوعًا ما
نموذج الذاكرة. (للحصول على التفاصيل، يُرجى الاطّلاع على دليل ARMv8 المرجعي لبنية ARMv8-A)
http://infocenter.arm.com/help/topic/com.arm.doc.genc007826/Barrier_Litmus_Tests_and_Cookbook_A08.pdf - حواجز ذاكرة نواة Linux
- مستندات حول حواجز الذاكرة في النواة في Linux تتضمن بعض الأمثلة المفيدة ورسومات ASCII.
http://www.kernel.org/doc/Documentation/memory-barriers.txt - ISO/IEC JTC1 SC22 WG21 (معايير C++) 14882 (لغة برمجة C++)، القسم 1.10 والفقرة 29 ("مكتبة العمليات البسيطة")
- مسودة المعيار لميزات التشغيل الذري C++ هذا الإصدار هو
من معيار C++14، والذي يتضمن تغييرات طفيفة في هذا المجال
من C++11.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4527.pdf
(مقدمة: http://www.hpl.hp.com/techreports/2008/HPL-2008-56.pdf) - ISO/IEC JTC1 SC22 WG14 (معايير C) 9899 (لغة البرمجة C) الفصل 7.16 ("Atomics <stdatomic.h>")
- مسودة المعيار لميزات التشغيل الذري ISO/IEC 9899-201x C
لمعرفة التفاصيل، يُرجى أيضًا مراجعة تقارير العيوب اللاحقة.
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf - تعيينات C/C++11 للمعالجات (جامعة كامبريدج)
- مجموعة ترجمات
لـ Jaroslav Sevcik وPeter Sewell من تعليمات C++ المُجمَّعة إلى مجموعات تعليمات المعالجات الشائعة المختلفة
http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html - خوارزمية ديكر
- "أول حلّ صحيح معروف لمشكلة الاستبعاد المتبادل في البرمجة المتزامنة". تتضمّن مقالة ويكيبيديا الخوارزمية الكاملة، مع مناقشة حول كيفية تعديلها للعمل مع المجمعات المُحسّنة الحديثة ومعدات SMP.
https://en.wikipedia.org/wiki/Dekker's_algorithm - تعليقات على ARM مقابل ألفا ومعالجة التبعيات
- عنوان بريد إلكتروني على القائمة البريدية لنواة الذراع من Catalin Marinas يتضمن ملخصًا جميلاً لتبعيات العنوان والتحكم.
http://linux.derkeiler.com/Mailing-Lists/Kernel/2009-05/msg11811.html - ما يجب أن يعرفه كل مبرمج عن الذاكرة
- مقالة طويلة جدًا ومفصّلة عن أنواع مختلفة من الذاكرة، لا سيما ذاكرات التخزين المؤقت لوحدة المعالجة المركزية، من تأليف "أولريش دريبر".
http://www.akkadia.org/drepper/cpumemory.pdf - التفكير في نموذج الذاكرة المتسقة على نحو ضعيف مع ARM
- كتب "تشونغ" هذه المقالة تحاول شركة Ishtiaq من شركة ARM, Ltd. وصف نموذج الذاكرة المستخدَم في ARM SMP بأسلوب صارم مع إمكانية الوصول إليها. يأتي تعريف "إمكانية الملاحظة" المستخدم هنا من هذه الورقة. مرة أخرى، يسبق هذا الإصدار ARMv8.
http://portal.acm.org/ft_gateway.cfm?id=1353528&type=pdf&coll=&dl=&CFID=96099715&CFTOKEN=57505711 - كتاب الطبخ JSR-133 للكتّاب المجمّعين
- كتب دوغ لي هذا الكتاب كمرجع مصاحب لمستندات JSR-133 (نموذج ذاكرة Java). تشمل المجموعة الأولية من إرشادات التنفيذ
لنموذج ذاكرة Java الذي استخدمه العديد من كاتبي التجميع،
لا يزال يُستشهد به على نطاق واسع ومن المرجح أن يقدم نظرة.
لسوء الحظ، الأنواع الأربعة للسياج التي تمت مناقشتها هنا ليست جيدة
مع البُنى التي تتوافق مع Android، وتخطيطات C++11 المذكورة أعلاه
مصدرًا أفضل للوصفات الدقيقة، حتى لـ Java.
http://g.oswego.edu/dl/jmm/cookbook.html - x86-TSO: نموذج دقيق وقابل للاستخدام للمبرمجين لمعالجات x86 المتعددة
- وصف دقيق لنموذج الذاكرة مقاس x86 الأوصاف الدقيقة لـ
فإن نموذج ذاكرة ARM أكثر تعقيدًا بكثير للأسف.
http://www.cl.cam.ac.uk/~pes20/weakmemory/cacm.pdf