سؤال

لدي سؤال حول نموذج الكود التالي (m_value ليس متقلبًا ، ويتم تشغيل كل مؤشر ترابط على معالج منفصل)

void Foo() // executed by thread #1, BEFORE Bar() is executed
{
   Interlocked.Exchange(ref m_value, 1);
}

bool Bar() // executed by thread #2, AFTER Foo() is executed
{
   return m_value == 1;
}

لا استخدام interlocked.exchange في FOO () يضمن أنه عند تنفيذ BAR () ، سأرى القيمة "1"؟ (حتى لو كانت القيمة موجودة بالفعل في سجل التسجيل أو ذاكرة التخزين المؤقت؟) أو هل أحتاج إلى وضع حاجز ذاكرة قبل قراءة قيمة m_value?

أيضًا (لا علاقة للسؤال الأصلي) ، هل من القانوني إعلان عضو متقلبة وتمريره بالرجوع إليه interlockedxx أساليب؟ (يحذر المترجم من تمرير المواد المتطايرة بالرجوع إليه ، فهل يجب أن أتجاهل التحذير في مثل هذه الحالة؟)

يرجى الملاحظة, ، أنا لا أبحث عن "طرق أفضل للقيام بالأشياء" ، لذا يرجى عدم نشر الإجابات التي تشير إلى طرق بديلة تمامًا للقيام بالأشياء ("استخدم قفل بدلاً من ذلك" وما إلى ذلك) ، يخرج هذا السؤال بدافع الاهتمام ..

هل كانت مفيدة؟

المحلول

يطابق النمط المعتاد لاستخدام حاجز الذاكرة ما ستضعه في تنفيذ قسم حرج ، ولكنه ينقسم إلى أزواج للمنتج والمستهلك. على سبيل المثال ، يكون تنفيذ القسم الحاسم في النموذج عادةً:

while (!pShared->lock.testAndSet_Acquire()) ;
// (this loop should include all the normal critical section stuff like
// spin, waste, 
// pause() instructions, and last-resort-give-up-and-blocking on a resource 
// until the lock is made available.)

// Access to shared memory.

pShared->foo = 1 
v = pShared-> goo

pShared->lock.clear_Release()

اكتساب حاجز الذاكرة أعلاه يتأكد من أن أي أحمال (pshared-> goo) قد تكون قد بدأت قبل إلقاء تعديل القفل الناجح ، لإعادة تشغيل إذا كانت ضرورية.

يضمن حاجز ذاكرة الإصدار أن يتم إكمال الحمل من GOO إلى المتغير (المحلي) V قبل أن يتم مسح كلمة القفل التي تحمي الذاكرة المشتركة.

لديك نمط مماثل في مشهد العلم الذري للمنتج النموذجي ومستهلك (من الصعب معرفة عينة ما إذا كان هذا هو ما تفعله ولكن يجب أن توضح الفكرة).

لنفترض أن المنتج الخاص بك يستخدم متغيرًا ذريًا للإشارة إلى أن بعض الحالة الأخرى جاهزة للاستخدام. ستحتاج إلى شيء مثل هذا:

pShared->goo = 14

pShared->atomic.setBit_Release()

بدون وجود حاجز "كتابة" هنا في المنتج ، ليس لديك أي ضمان بأن الجهاز لن يصل إلى المتجر الذري قبل أن يكون متجر Goot (حتى لو كان لديك آلية تضمن أن المترجم يأمر الأشياء بالطريقة التي تريدها).

في المستهلك

if ( pShared->atomic.compareAndSwap_Acquire(1,1) )
{
   v = pShared->goo 
}

بدون حاجز "قراءة" هنا ، لن تعرف أن الأجهزة لم تختفي وجلب غو لك قبل اكتمال الوصول الذري. الذرية (أي: الذاكرة المعالجة بالوظائف المتشابكة التي تقوم بأشياء مثل قفل CMPXCHG) ، هي فقط "ذرية" فيما يتعلق بنفسها ، وليس الذاكرة الأخرى.

الآن ، فإن الشيء المتبقي الذي يجب ذكره هو أن بنيات الحاجز غير قابلة للمواصلة للغاية. من المحتمل أن يوفر المترجم الخاص بك اختلافات _acquire و _release لمعظم طرق التلاعب الذرية ، وهذه هي أنواع الطرق التي تستخدمها. اعتمادًا على النظام الأساسي الذي تستخدمه (أي: ia32) ، قد تكون هذه بالضبط ما ستحصل عليه بدون اللواحق _acquire () أو _Release (). المنصات التي تكون فيها هذه الأمور هي IA64 (ميتة بشكل فعال باستثناء HP حيث لا يزال يرخيل قليلاً) ، و PowerPC. كان IA64. معدلات تعليمات. ACQ و .REL على معظم تعليمات الحمل والمتجر (بما في ذلك الإرشادات الذرية مثل CMPXCHG). لدى PowerPC تعليمات منفصلة لهذا (ISYNC و LWSYNC يمنحك حواجز القراءة والكتابة على التوالي).

الآن. بعد قول كل هذا. هل لديك حقًا سبب وجيه للذهاب إلى هذا المسار؟ يمكن أن يكون القيام بكل هذا بشكل صحيح أمرًا صعبًا للغاية. كن مستعدًا للكثير من الشك في الذات وانعدام الأمن في مراجعات التعليمات البرمجية وتأكد من أن لديك الكثير من اختبارات التزامن العالية مع جميع أنواع مشهد التوقيت العشوائي. استخدم قسمًا مهمًا ما لم يكن لديك سبب وجيه للغاية لتجنب ذلك ، ولا تكتب هذا القسم الحرج بنفسك.

نصائح أخرى

حواجز الذاكرة لا تساعدك بشكل خاص. يحددون ترتيبًا بين عمليات الذاكرة ، وفي هذه الحالة يكون لكل مؤشر ترابط فقط عملية ذاكرة واحدة بحيث لا يهم. أحد السيناريو النموذجي يكتب غير منطقي للحقول في هيكل ، حاجز الذاكرة ، ثم نشر عنوان الهيكل على مؤشرات الترابط الأخرى. يضمن الحاجز أن ينظر إلى جميع وحدات المعالجة المركزية التي يكتب إلى أعضاء الهياكل قبل أن يحصلوا على عنوانها.

ما تحتاجه حقًا هو العمليات الذرية ، أي. وظائف interlockedxxx ، أو المتغيرات المتطايرة في C#. إذا كانت القراءة في الشريط ذريًا ، فيمكنك أن تضمن عدم قيام المترجم أو وحدة المعالجة المركزية ، بأي تحسينات تمنعها من قراءة القيمة قبل الكتابة في FOO ، أو بعد الكتابة في FOO اعتمادًا على تنفيذها أولاً. نظرًا لأنك تقول أنك "تعرف" كتابة كتاب Foo قبل قراءة Bar ، فإن Bar سيعود دائمًا.

بدون قراءة الشريط الذرية ، يمكن أن تقرأ قيمة محدثة جزئيًا (أي القمامة) ، أو قيمة مخزنة مؤقتًا (إما من المترجم أو من وحدة المعالجة المركزية) ، وكلاهما قد يمنع الشريط من العودة إلى حد ما.

إن معظم القراءات المحاذاة لضمان وحدة المعالجة المركزية الحديثة هي ذرية ، لذا فإن الحيلة الحقيقية هي أنه عليك أن تخبر المترجم أن القراءة ذرية.

لست متأكدًا تمامًا ولكني أعتقد أن interlocer.exchange سوف تستخدم وظيفة interlockedExchange من Windows API الذي يوفر حاجز الذاكرة الكامل على أي حال.

تقوم هذه الوظيفة بإنشاء حاجز ذاكرة كامل (أو سياج) لضمان اكتمال عمليات الذاكرة بالترتيب.

تضمن عمليات التبادل المتشابكة حاجز الذاكرة.

تستخدم وظائف التزامن التالية الحواجز المناسبة لضمان ترتيب الذاكرة:

  • الوظائف التي تدخل أو تترك أقسام حرجة

  • الوظائف التي تشير إلى كائنات التزامن

  • وظائف الانتظار

  • وظائف متشابكة

(مصدر : حلقة الوصل)

لكنك محظوظ مع متغيرات التسجيل. إذا كان m_value في سجل في الشريط ، فلن ترى التغيير إلى m_value. بسبب هذا ، يجب أن تعلن المتغيرات المشتركة "متقلبة".

لو m_value لا يتم وضع علامة عليه volatile, ، ثم لا يوجد سبب للاعتقاد بأن القيمة تقرأ في Bar مسيجة. يمكن أن تعيد تحسينات المترجمة أو التخزين المؤقت أو عوامل أخرى ترتيب القراءات والكتابة. يعد التبادل المتشابك مفيدًا فقط عند استخدامه في نظام بيئي من مراجع الذاكرة المسورة بشكل صحيح. هذا هو الهدف الأساسي في وضع علامة على الحقل volatile. نموذج الذاكرة .NET ليس مستقيمًا للأمام كما قد يتوقع البعض.

يجب أن تضمن Interlocked.exchange () أن القيمة يتم مسحها إلى جميع وحدات المعالجة المركزية بشكل صحيح - إنها توفر حاجز الذاكرة الخاص بها.

أنا مندهش من أن المترجم يتجمع حول تمرير متقلبة إلى interlocared.exchange () - حقيقة أنك تستخدم entroloced.exchange () يجب أن تفرض متغيرًا متطايرًا تقريبًا.

المشكلة لك ربما انظر أنه إذا قام المترجم ببعض التحسين الثقيل للشريط () ويدرك أن لا شيء يغير قيمة m_value ، فيمكنه تحسين الشيك. هذا ما ستفعله الكلمة الرئيسية المتطايرة - من شأنه أن يلمح إلى المترجم أن هذا المتغير قد يتم تغييره خارج وجهة نظر المحسن.

إذا لم تخبر المترجم أو وقت التشغيل ذلك m_value لا ينبغي قراءتها قبل الشريط m_value سابق ل Bar() وببساطة استخدام القيمة المخزنة مؤقتًا. إذا كنت ترغب في التأكد من أنها ترى إصدار "أحدث" من m_value, ، إما دفع في أ Thread.MemoryBarrier() او استعمل Thread.VolatileRead(ref m_value). هذا الأخير أقل تكلفة من حاجز الذاكرة الكامل.

من الناحية المثالية ، يمكنك أن تدفع في حاجز قراءة ، ولكن يبدو أن CLR لا يدعم ذلك مباشرة.

تحرير: هناك طريقة أخرى للتفكير في الأمر هي أن هناك بالفعل نوعان من حواجز الذاكرة: حواجز الذاكرة المترجم التي تخبر المترجم كيفية التسلسل يقرأ ويكتب وحواجز ذاكرة وحدة المعالجة المركزية التي تخبر وحدة المعالجة المركزية كيفية تسلسل القراءة والكتابة. ال Interlocked وظائف استخدام حواجز ذاكرة وحدة المعالجة المركزية. حتى لو تعاملهم المترجم كحواجز ذاكرة مترجم ، فإنه لا يزال لا يهم ، كما في هذه الحالة المحددة ، Bar() كان يمكن تجميعها بشكل منفصل ولم تكن معروفة للاستخدامات الأخرى m_value وهذا يتطلب حاجز الذاكرة المترجم.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top