تم تحسين الإصدارات Android 3.0 والإصدارات الأحدث للتوافق مع الأنظمة الأساسية الهياكل الهندسية متعددة المعالجات. يتناول هذا المستند المشكلات التي قد تظهر عند كتابة تعليمات برمجية متعددة السلاسل للأنظمة المتماثلة متعددة المعالجة في اللغات C وC++ وJava (والتي يشار إليها في ما بعد باسم "Java" والإيجاز). فهي مخصّصة كدليل تمهيدي لمطوّري تطبيقات Android، وليس كدليل مناقشة حول هذا الموضوع.
مقدّمة
يشير الاختصار SMP إلى "المعالج المتعدد المتماثل". يصف التصميم في وحدة معالجة مركزية بها نواتان أو أكثر يتشاركان في الوصول إلى الذاكرة الرئيسية. حتى قبل بضع سنوات، كانت جميع أجهزة Android UP (المعروفة اختصارًا بـ Uni-Processor).
معظم أجهزة Android، إن لم يكن كلها، تحتوي دائمًا على وحدات معالجة مركزية (CPU) متعددة، ولكن في الماضي، كان تطبيق واحد منها فقط يُستخدم لتشغيل التطبيقات بينما يدير البعض الآخر أجزاءً مختلفة من الأجهزة الأجهزة (مثل الراديو). قد يكون لكل وحدات المعالجة المركزية بُنى مختلفة، فإن البرامج التي تعمل عليها لم تتمكن من استخدام الذاكرة الرئيسية للتواصل مع آخر.
تركز معظم أجهزة 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
التأكد من فتح الأقفال بشكل صحيح في حدث استثناء. - المتغيّرات المتغيرة/الذرية
- توفّر 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
غير المهيأ. أو قد يقرر برنامج التحويل البرمجي أن العلامة غير قابلة أثناء التكرار الحلقي Thread 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++، يمكن أن يكون توصيل مؤشر أو
الإشارة إلى كائن غير قابل للتغيير بدون مزامنة مناسبة،
مثل أي سباق بيانات، خطأ.
وفي هذه الحالة، ومن المحتمل بشكل معقول أن تؤدي إلى أعطال متقطّعة،
على سبيل المثال، قد ترى سلسلة الاستلام جدول طريقة غير مهيأ
المؤشر بسبب إعادة ترتيب المتجر.
إذا لم تكن فئة مكتبة موجودة أو فئة غير قابلة للتغيير
المناسبة، وهي عبارة Java synchronized
أو C++
يجب استخدام lock_guard
/ unique_lock
للحماية
إلى أي حقل يمكن الوصول إليه باستخدام أكثر من سلسلة محادثات واحدة. في حال عدم استجابة كائنات المزامنة
بما يناسب حالتك، يجب أن تذكر الحقول المشتركة
volatile
أو atomic
، ولكن عليك توخي الحذر الشديد
فهم التفاعلات بين السلاسل. لن تتضمن هذه البيانات
أن تتجنب أخطاء البرمجة الشائعة المتزامنة، لكنها ستساعدك
تجنب الأخطاء الغامضة المرتبطة بتحسين برامج التحويل البرمجي وبروتوكول SMP
وحوادث السير.
يجب تجنب "النشر" مرجعًا إلى كائن، أي إتاحته للآخرين في الدالة الإنشائية. هذا أقل أهمية في لغة C++ أو إذا التزمت "لا سباقات بيانات" النصيحة في 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
تشبه متاجر C++ memory_order_release
. لغة 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://ar.wikipedia.org/wiki/Memory_barrier - أساسيات Threads
- مقدمة عن البرمجة المتعدّدة سلاسل المحادثات بلغة 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 للمعالجات (جامعة كامبريدج)
- مجموعة ترجمات "ياروسلاف سيفسيك" و"بيتر سيويل"
من ذرات 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