سؤال

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

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

في العمل، كنت أحاول تصحيح بعض التعليمات البرمجية متعددة الخيوط، وعثرت على ما يلي:

EnterCriticalSection(&m_Crit4);
m_bSomeVariable = true;
LeaveCriticalSection(&m_Crit4);

الآن، m_bSomeVariable هو Win32 BOOL (غير متطاير)، والذي يتم تعريفه على حد علمي على أنه int، وعلى x86، تعد قراءة هذه القيم وكتابتها عبارة عن تعليمة واحدة، وبما أن مفاتيح السياق تحدث على حدود التعليمات، فليست هناك حاجة للمزامنة هذه العملية مع قسم حرج.

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

  1. تنفذ وحدة المعالجة المركزية تنفيذًا خارج الترتيب أو أن الخيط الثاني يعمل على نواة مختلفة ولا تتم كتابة القيمة المحدثة في ذاكرة الوصول العشوائي حتى يراها النواة الأخرى؛و
  2. لم تتم محاذاة int بمقدار 4 بايت.

أعتقد أنه يمكن حل الرقم 1 باستخدام الكلمة الأساسية "المتقلبة".في VS2005 والإصدارات الأحدث، يحيط مترجم C++ بالوصول إلى هذا المتغير باستخدام حواجز الذاكرة، مما يضمن كتابة/قراءة المتغير دائمًا بالكامل على ذاكرة النظام الرئيسية قبل استخدامه.

رقم 2: لا أستطيع التحقق، ولا أعرف لماذا ستحدث محاذاة البايت فرقًا.لا أعرف مجموعة تعليمات x86، لكني أعرفها mov هل تحتاج إلى الحصول على عنوان محاذٍ مكون من 4 بايت؟إذا لم يكن الأمر كذلك، فهل تحتاج إلى استخدام مجموعة من التعليمات؟وهذا من شأنه أن يعرض المشكلة.

لذا...

السؤال رقم 1: هل استخدام الكلمة الأساسية "المتقلبة" (استخدام ضمني حواجز الذاكرة والتلميح إلى المترجم بعدم تحسين هذا الرمز) يعفي المبرمج من الحاجة إلى مزامنة 4 بايت/8 بايت على متغير x86/x64 بين عمليات القراءة/الكتابة؟

السؤال 2: هل هناك شرط صريح بأن يكون المتغير محاذاة 4 بايت/8 بايت؟

لقد أجريت المزيد من البحث في الكود الخاص بنا والمتغيرات المحددة في الفصل:

class CExample
{

private:

    CRITICAL_SECTION m_Crit1; // Protects variable a
    CRITICAL_SECTION m_Crit2; // Protects variable b
    CRITICAL_SECTION m_Crit3; // Protects variable c
    CRITICAL_SECTION m_Crit4; // Protects variable d

    // ...

};

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

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

السؤال 3: هل تحليلي للأقسام الهامة صحيح، وهل يجب إعادة كتابة هذا الكود لاستخدام كائنات المزامنة؟لقد ألقيت نظرة على كائنات المزامنة الأخرى (الإشارات وأقفال الدوران)، هل هي أكثر ملاءمة هنا؟

السؤال 4: أين هي الأقسام الهامة/mutexes/الإشارات/spinlocks الأكثر ملاءمة؟أي ما هي مشكلة المزامنة التي يجب تطبيقها عليها.هل هناك عقوبة أداء كبيرة لاختيار واحد على الآخر؟

وأثناء قيامنا بذلك، قرأت أنه لا ينبغي استخدام Spinlocks في بيئة متعددة مؤشرات الترابط أحادية النواة، فقط بيئة متعددة مؤشرات الترابط متعددة النواة.لذا، السؤال 5: هل هذا خطأ، وإذا لم يكن كذلك، لماذا هو صحيح؟

ويرجع الفضل في ذلك مسبقا لأية ردود :)

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

المحلول

س1:باستخدام الكلمة الأساسية "المتقلبة".

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

بالضبط.إذا لم تكن تقوم بإنشاء تعليمات برمجية محمولة، فإن Visual Studio ينفذها بهذه الطريقة تمامًا.إذا كنت تريد أن تكون محمولاً، فإن خياراتك "محدودة" حاليًا.حتى C++0x لا توجد طريقة محمولة لكيفية تحديد العمليات الذرية مع ترتيب القراءة/الكتابة المضمون وتحتاج إلى تنفيذ حلول لكل منصة.ومع ذلك، فقد قام Boost بالفعل بالمهمة القذرة نيابةً عنك، ويمكنك استخدامها بداياتها الذرية.

س2:هل يجب أن يكون المتغير محاذاة 4 بايت/8 بايت؟

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

س3:هل يجب إعادة كتابة هذا الرمز لاستخدام كائنات المزامنة؟

القسم الحرج هو كائن المزامنة خفيف الوزن.ما لم تكن بحاجة إلى المزامنة بين العمليات، استخدم الأقسام المهمة.

س 4:أين هي الأقسام الهامة/mutexes/الإشارات/spinlocks الأكثر ملاءمة؟

الأقسام الحرجة هل تقدر حتى لا تدور ينتظر لك.

س5:لا ينبغي استخدام Spinlocks في قلب واحد

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

نصائح أخرى

1) لا يوجد شيء متقلب يقول فقط إعادة تحميل القيمة من الذاكرة في كل مرة لا يزال من الممكن أن يتم تحديثها نصفها.

يحرر:2) يوفر Windows بعض الوظائف الذرية.ابحث عن وظائف "متشابكة"..

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

8.1.1 يضمن العمليات الذرية المضمونة Intel486 (ومعالجات الأحدث منذ ذلك الحين) أن يتم دائمًا تنفيذ عمليات الذاكرة الأساسية التالية من الناحية الذرية:
• قراءة أو كتابة بايت
• قراءة أو كتابة كلمة محاذية لحدود 16 بت
• قراءة أو كتابة كلمة مزدوجة محاذاة على حدود 32 بت
يضمن معالج Pentium (والمعالجات الأحدث منذ ذلك الحين) أن يتم دائمًا تنفيذ عمليات الذاكرة الإضافية التالية بشكل ذري:
• قراءة أو كتابة كلمة رباعية محاذية لحدود 64 بت
• وصول 16 بت إلى مواقع الذاكرة غير المخزنة مؤقتًا والتي تتلاءم مع ناقل بيانات 32 بت
تضمن معالجات عائلة P6 (والمعالجات الأحدث منذ ذلك الحين) أن يتم دائمًا تنفيذ عملية الذاكرة الإضافية التالية بشكل ذري:
• وصول غير محدد 16 و 32 و 64 بت إلى الذاكرة المخزنة مؤقتًا والتي تتناسب مع خط ذاكرة التخزين المؤقت
الوصول إلى الذاكرة القابلة للتخزين المؤقت التي يتم تقسيمها عبر عروض الحافلات ، وخطوط ذاكرة التخزين المؤقت ، وحدود الصفحات غير مضمونة لتكون ذرية من قبل Intel Core 2 Duo ، Intel Atom ، Intel Core Duo ، Pentium M ، Pentium 4 ، Intel Xeon ، P6 ، Pentium ، Pentium و intel486 معالجات.توفر معالجات أسرة Intel Core 2 ، و Intel Atom ، و Intel Core Duo ، و Pentium M ، و Pentium 4 ، و Intel Xeon ، و P6 إشارات تحكم في الحافلة التي تسمح بأنظمة فرعية خارجية للذاكرة لجعل الانقسام ذريًا ؛لكن ستؤثر عمليات الوصول إلى البيانات غير المحاذاة بشكل خطير على أداء المعالج و يجب تجنبها.تعليمات x87 أو تعليمات SSE التي تصل إلى بيانات أكبر من كلمة رباعية يمكن تنفيذها باستخدام عمليات وصول متعددة للذاكرة.إذا كان مثل هذا التعليمات يخزن إلى الذاكرة ، قد تكتمل بعض عمليات الوصول (الكتابة إلى الذاكرة) بينما تكتمل عملية أخرى يتسبب في حدوث خطأ في العملية لأسباب معمارية (على سبيل المثالبسبب إدخال جدول صفحات التي تحمل علامة "غير موجود").في هذه الحالة ، تأثيرات عمليات الوصول المكتملة قد يكون مرئيا للبرنامج على الرغم من أن التعليمات العامة تسببت في حدوث خطأ.إذا TLB تم تأخير الإبطال (انظر القسم 4.10.3.4) ، قد تحدث أخطاء الصفحة هذه حتى لو كانت جميع عمليات الوصول إلى نفس الصفحة.

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

3) سيحاول القسم الحرج تدوير القفل لقفله عدة مرات ثم يقوم بقفل كائن المزامنة (mutex).يمكن لـ Spin Locking أن يمتص طاقة وحدة المعالجة المركزية دون القيام بأي شيء ويمكن أن يستغرق كائن المزامنة بعض الوقت للقيام بأشياءه.تعد الأقسام الحرجة خيارًا جيدًا إذا لم تتمكن من استخدام الوظائف المتشابكة.

4) هناك عقوبات أداء لاختيار واحد على الآخر.إنه طلب كبير جدًا للتعرف على فوائد كل شيء هنا.تحتوي مساعدة MSDN على الكثير من المعلومات الجيدة حول كل منها.أقترح قراءتها.

5) يمكنك استخدام قفل الدوران في بيئة مترابطة واحدة، وهذا ليس ضروريًا عادةً على الرغم من أن إدارة الخيوط تعني أنه لا يمكن أن يكون لديك معالجان يصلان إلى نفس البيانات في وقت واحد.هذا غير ممكن.

1:متقلب في ذاته غير مجدية عمليا لتعدد مؤشرات الترابط.فهو يضمن تنفيذ القراءة/الكتابة، بدلاً من تخزين القيمة في السجل، ويضمن عدم إعادة ترتيب القراءة/الكتابة فيما يتعلق بالآخرين volatile يقرأ / يكتب.ولكن لا يزال من الممكن إعادة ترتيبها فيما يتعلق بالعناصر غير المتطايرة، والتي تمثل في الأساس 99.9% من التعليمات البرمجية الخاصة بك.لقد أعادت مايكروسوفت تعريفها volatile لتغليف جميع عمليات الوصول أيضًا بحواجز الذاكرة، ولكن ليس من المضمون أن يكون هذا هو الحال بشكل عام.سوف ينكسر بصمت على أي مترجم يحدد volatile كما يفعل المعيار.(سيتم تجميع الكود وتشغيله، ولن يكون آمنًا بعد الآن)

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

2:نعم، يجب محاذاة الكائن حتى تكون القراءة/الكتابة ذرية.

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

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

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

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

لا تستخدم متقلبة.لا علاقة له تقريبًا بسلامة الخيط.يرى هنا للأسفل.

لا يحتاج التعيين إلى BOOL إلى أي عناصر أولية للمزامنة.وسوف تعمل بشكل جيد دون أي جهد خاص من جانبك.

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

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

المتقلبة لا تعني حواجز الذاكرة.

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

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

void test() 
{
    volatile int a;
    volatile int b;
    int c;

    c = 1;
    a = 5;
    b = 3;
}

باستخدام الكود أعلاه (على افتراض ذلك c لم يتم تحسينه بعيدًا) التحديث إلى c يمكن أن يحدث قبل أو بعد التحديثات a و b, ، وتوفير 3 النتائج المحتملة.ال a و b نضمن إجراء التحديثات بالترتيب. c يمكن تحسينه بسهولة بواسطة أي مترجم.مع ما يكفي من المعلومات، يمكن للمترجم حتى تحسين بعيدا a و b (إذا كان من الممكن إثبات عدم وجود مؤشرات ترابط أخرى تقرأ المتغيرات وأنها غير مرتبطة بمصفوفة أجهزة (لذلك في هذه الحالة، يمكن إزالتها في الواقع).لاحظ أن المعيار لا يتطلب سلوكًا محددًا، بل يتطلب حالة يمكن إدراكها مع as-if قاعدة.

الأسئلة 3:تعمل الأقسام CRITICAL_SECTIONs وMutexes بنفس الطريقة تقريبًا.كائن Win32 mutex هو كائن kernel، لذا يمكن مشاركته بين العمليات، وانتظاره باستخدام WaitForMultipleObjects، وهو ما لا يمكنك فعله باستخدام CRITICAL_SECTION.ومن ناحية أخرى، يعتبر القسم CRITICAL_SECTION أخف وزنًا وبالتالي أسرع.لكن منطق الكود يجب ألا يتأثر بالذي تستخدمه.

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

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