باستخدام C/Pthreads:هل يجب أن تكون المتغيرات المشتركة متقلبة؟

StackOverflow https://stackoverflow.com/questions/78172

  •  09-06-2019
  •  | 
  •  

سؤال

في لغة البرمجة C وPthreads كمكتبة ترابط؛هل يجب الإعلان عن المتغيرات/الهياكل المشتركة بين سلاسل الرسائل على أنها متقلبة؟على افتراض أنها قد تكون محمية بقفل أم لا (ربما حواجز).

هل لدى معيار pthread POSIX أي رأي حول هذا الأمر، هل يعتمد هذا على المترجم أم لا؟

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

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

المحلول

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

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

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

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

للحصول على مثال على ضعف المتقلبة، راجع مثال خوارزمية ديكر الخاص بي على http://jakob.engbloms.se/archives/65, ، وهو ما يثبت جيدًا أن المتقلبة لا تعمل على المزامنة.

نصائح أخرى

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

https://software.intel.com/en-us/blogs/2007/11/30/volatile-almost-useless-for-multi-threaded-programming/

الجواب هو بالتأكيد، وبشكل لا لبس فيه، لا.لا تحتاج إلى استخدام كلمة "متقلبة" بالإضافة إلى بدايات المزامنة المناسبة.كل ما يجب القيام به يتم بواسطة هؤلاء البدائيين.

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

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

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

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

هناك فكرة واسعة النطاق مفادها أن الكلمة الأساسية volatile مفيدة للبرمجة متعددة الخيوط.

هانز بوم يشير الى أن هناك ثلاثة استخدامات محمولة فقط للمتقلبة:

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

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

  • الذرية
  • اتساق الذاكرة, ، أي.ترتيب عمليات الخيط كما يراها خيط آخر.

دعونا نتعامل مع (١) أولاً.المتطايرة لا تضمن القراءة أو الكتابة الذرية.على سبيل المثال، لن تكون القراءة أو الكتابة المتقلبة لبنية 129 بت ذرية في معظم الأجهزة الحديثة.تعد القراءة أو الكتابة المتقلبة لـ 32 بت أمرًا ذريًا في معظم الأجهزة الحديثة، ولكن المتقلبة لا علاقة لها به.من المحتمل أن تكون ذرية بدون المواد المتطايرة.الذرية هي في نزوة المترجم.لا يوجد شيء في معايير C أو C++ ينص على أنها يجب أن تكون ذرية.

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

 volatile int Ready;       

    int Message[100];      

    void foo( int i ) {      

        Message[i/10] = 42;      

        Ready = 1;      

    }

إنها تحاول القيام بشيء معقول جدًا في البرمجة متعددة الخيوط:اكتب رسالة ثم أرسلها إلى موضوع آخر.سينتظر الخيط الآخر حتى يصبح جاهزًا غير صفري ثم يقرأ الرسالة.حاول ترجمة ذلك باستخدام "gcc -O2 -S" باستخدام gcc 4.0 أو icc.كلاهما سيجعل المتجر جاهزًا أولاً، بحيث يمكن أن يتداخل مع حساب i/10.إعادة الترتيب ليست خطأ مترجم.إنه مُحسِّن عدواني يقوم بعمله.

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

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

  • المواضيع بوسيكس
  • المواضيع ويندوز (TM).
  • OpenMP
  • تي بي بي

مرتكز على مقال بقلم آرتش روبيسون (إنتل)

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

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

لا.

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

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

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

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

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

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

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

Volatile عبارة عن تلميح مترجم يعمل على تعطيل بعض التحسينات.قد يكون تجميع مخرجات المترجم آمنًا بدونه ولكن يجب عليك استخدامه دائمًا للقيم المشتركة.

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

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

كما أشار بول ماكيني في مقال مشهور وثيقة نواة لينكس:

من المفترض أن يقوم المترجم بما تريده مع مراجع الذاكرة غير محمية بواسطة read_once () و write_once ().بدونها ، يكون المترجم ضمن حقوقه في القيام بجميع أنواع التحولات "الإبداعية" ، والتي يتم تغطيتها في قسم حاجز المترجم.

يتم تعريف READ_ONCE () وWRITE_ONCE () على أنهما قوالب متقلبة على المتغيرات المشار إليها.هكذا:

int y;
int x = READ_ONCE(y);

يعادل:

int y;
int x = *(volatile int *)&y;

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

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


وكما أشار بول ماكيني أيضًا:

لقد رأيت البريق في أعينهم عندما يناقشون تقنيات التحسين التي لا تريد أن يعرفها أطفالك!


ولكن انظر ماذا يحدث ل C11/سي++11.

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

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

من الواضح أن بعض الأشخاص يفترضون أن المترجم يعامل مكالمات المزامنة كحواجز للذاكرة.يفترض "Casey" وجود وحدة معالجة مركزية واحدة بالضبط.

إذا كانت أساسيات المزامنة عبارة عن وظائف خارجية وكانت الرموز المعنية مرئية خارج وحدة الترجمة (الأسماء العامة، المؤشر المُصدَّر، الوظيفة المُصدَّرة التي قد تعدلها) فسيعاملها المترجم - أو أي استدعاء دالة خارجية أخرى - على أنها سياج الذاكرة فيما يتعلق بجميع الأشياء المرئية خارجيًا.

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

لا.

أولاً، volatile ليس ضروري.هناك العديد من العمليات الأخرى التي توفر دلالات مضمونة متعددة الخيوط لا تستخدم volatile.وتشمل هذه العمليات الذرية، وكائنات المزامنة، وما إلى ذلك.

ثانية، volatile لا يكفي.لا يوفر معيار C أي ضمانات حول السلوك متعدد الخيوط للمتغيرات المعلنة volatile.

لذا، نظرًا لكونها ليست ضرورية ولا كافية، فليس هناك فائدة كبيرة من استخدامها.

قد يكون الاستثناء الوحيد هو منصات معينة (مثل Visual Studio) حيث تحتوي على دلالات موثقة متعددة الخيوط.

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

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