لا يتم إنشاء المواضيع في openmp. خوارزميات جدولة أكثر تعقيدًا

13.04.2019

تسمح لك البنية التحتية لـ OpenMP بالتنفيذ الفعال لتقنيات البرمجة المتوازية في C وC++ وFortran. يدعم الإصدار 4.2 من مجموعة GNU Compiler Collection (GCC) مواصفات OpenMP 2.5، ويدعم الإصدار 4.4 من GNU أحدث مواصفات OpenMP 3. كما تدعم المجمعات الأخرى، بما في ذلك Microsoft® Visual Studio، OpenMP. ستعلمك هذه المقالة كيفية استخدام برامج التحويل البرمجي OpenMP؛ ويحتوي أيضًا على معلومات حول بعض واجهات برمجة تطبيقات OpenMP ويغطي بعض تقنيات الحوسبة المتوازية باستخدام OpenMP. تستخدم كافة الأمثلة في هذه المقالة برنامج التحويل البرمجي لدول مجلس التعاون الخليجي 4.2.

بداية العمل

الميزة الكبيرة لبرنامج OpenMP هي عدم الحاجة إلى إجراءات إضافية، باستثناء التثبيت القياسي للمترجم الخليجي. يجب أن يتم تجميع تطبيقات OpenMP باستخدام الخيار -fopenmp.

إنشاء أول تطبيق OpenMP

لنبدأ بكتابة طلب بسيط مرحبا بالعالم!تحتوي على براغما إضافية. يظهر رمز هذا التطبيق في القائمة 1.

القائمة 1. برنامج "Hello World" مكتوب باستخدام OpenMP
#يشمل int main() ( #pragma omp موازي ( std::cout<< "Hello World!\n"; } }

بعد تجميع هذا الكود وتشغيله باستخدام g++، سترى الرسالة التالية على شاشتك: مرحبا بالعالم!. الآن دعونا نعيد ترجمة الكود باستخدام خيار -fopenmp. يتم عرض نتيجة البرنامج في القائمة 2.

القائمة 2. تجميع التعليمات البرمجية وتشغيلها باستخدام خيار -fopenmp
tintin$ g++ test1.cpp -fopenmp tintin$ ./a.out أهلاً بالعالم! مرحبا بالعالم! مرحبا بالعالم! مرحبا بالعالم! مرحبا بالعالم! مرحبا بالعالم! مرحبا بالعالم! مرحبا بالعالم!

ماذا حدث؟ عند استخدام خيار المترجم -fopenmp، يتم تشغيل التوجيه المتوازي #pragma omp. أثناء التجميع، تقوم الأجزاء الداخلية لدول مجلس التعاون الخليجي بإنشاء أكبر عدد ممكن من الخيوط المتوازية التي يمكن تنفيذها في ظل ظروف تحميل النظام المثلى (اعتمادًا على تكوينات الأجهزة ونظام التشغيل)، حيث يقوم كل مؤشر ترابط تم إنشاؤه بتنفيذ الكود الموجود في الكتلة بعد البراجما. ويسمى هذا السلوك الموازاة الضمنية، ويتكون قلب OpenMP من مجموعة من البراغمات القوية التي توفر عليك كتابة الكثير من أجزاء التعليمات البرمجية النموذجية (من أجل المتعة، يمكنك مقارنة الكود المقدم مع تنفيذ نفس المهمة باستخدام سلاسل POSIX). أنا أستخدم جهاز كمبيوتر مزودًا بمعالج Intel® Core i7 مع 4 مراكز فعلية لكل منها نواتان منطقيتان، وهو ما يفسر النتائج في القائمة 2 (8 خيوط = 8 مراكز منطقية).

ميزات OpenMP المتوازية

يمكن التحكم في عدد المواضيع بسهولة باستخدام pragma باستخدام الوسيطة num_threads. يوجد أدناه الكود من القائمة 1 مع عدد المواضيع المحددة (5 سلاسل):

القائمة 3. التحكم في عدد المواضيع باستخدام num_threads
#يشمل int main() ( #pragma omp موازي num_threads(5) ( std::cout<< "Hello World!\n"; } }

بدلاً من الوسيطة num_threads، يمكنك استخدام طريقة بديلة لتحديد عدد سلاسل عمليات تنفيذ التعليمات البرمجية. نأتي هنا إلى أول واجهة برمجة تطبيقات OpenMP تسمى omp_set_num_threads. تم تعريف هذه الوظيفة في ملف رأس omp.h. لا تحتاج إلى استخدام أي مكتبات إضافية لتشغيل التعليمات البرمجية في القائمة 4؛ فقط استخدم الخيار -fopenmp.

القائمة 4. التحكم في الخيوط الدقيقة باستخدام omp_set_num_threads
#يشمل #يشمل int main() ( omp_set_num_threads(5); #pragma omp موازي ( std::cout<< "Hello World!\n"; } }

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

القائمة 5. متغيرات البيئة لتكوين OpenMP
tintin$ تصدير OMP_NUM_THREADS=6 tintin$ ./a.out أهلاً بالعالم! مرحبا بالعالم! مرحبا بالعالم! مرحبا بالعالم! مرحبا بالعالم! مرحبا بالعالم!

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

دراسة الحالة

يستخدم OpenMP تقنيات الموازاة الضمنية ويمكنه استخدام البراغما والوظائف الصريحة ومتغيرات البيئة لتمرير التعليمات إلى المترجم. دعونا نلقي نظرة على مثال يوضح بوضوح فوائد استخدام OpenMP. ألقِ نظرة على الكود الموضح في القائمة 6.

القائمة 6. المعالجة المتسلسلة في حلقة for
int main() ( int a, b; // ... رمز التهيئة للصفائف a و b; int c; for (int i = 0; i< 1000000; ++i) c[i] = a[i] * b[i] + a * b; // ... выполняем некоторые действия с массивом c }

من الواضح أن حلقة for يمكن موازنتها ومعالجتها بواسطة العديد من نوى المعالج في وقت واحد، نظرًا لأن حساب قيمة أي عنصر c[k] لا يعتمد بأي شكل من الأشكال على العناصر المتبقية في المصفوفة c. توضح القائمة 7 كيف يمكنك القيام بذلك باستخدام OpenMP.

القائمة 7. المعالجة المتوازية في حلقة for باستخدام التوازي لـ pragma
int main() ( int a, b; // ... رمز لتهيئة المصفوفات a و b; int c; #pragma omp موازي لـ (int i = 0; i< 1000000; ++i) c[i] = a[i] * b[i] + a * b; // ... выполняем некоторые действия с массивом c }

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

القائمة 8. مثال باستخدام وظيفة omp_get_wtime API
#يشمل #يشمل #يشمل #يشمل int main(int argc, char *argv) ( int i, nthreads; Clock_t Clock_timer; double wall_timer; double c; for (nthreads = 1; nthreads<=8; ++nthreads) { clock_timer = clock(); wall_timer = omp_get_wtime(); #pragma omp parallel for private(i) num_threads(nthreads) for (i = 0; i < 1000000; i++) c[i] = sqrt(i * 4 + i * 2 + i); std::cout << "threads: " << nthreads << " time on clock(): " << (double) (clock() - clock_timer) / CLOCKS_PER_SEC << " time on wall: " << omp_get_wtime() - wall_timer << "\n"; } }

في القائمة 8، نقوم بقياس وقت تنفيذ حلقة for الداخلية مع زيادة عدد الخيوط. تقوم الدالة omp_get_wtime API بإرجاع الوقت الفعلي المنقضي (بالثواني) منذ بداية نقطة مرجعية معينة. لذا فإن omp_get_wtime() - يقوم wall_timer بإرجاع وقت التنفيذ الفعلي للحلقة. يتم استخدام استدعاء النظام على مدار الساعة () لتقدير الوقت الذي تستغرقه وحدة المعالجة المركزية لتنفيذ البرنامج بأكمله، أي أننا نضيف كل هذه الفواصل الزمنية مع مراعاة الخيوط قبل الحصول على النتيجة النهائية. على جهاز الكمبيوتر الخاص بي Intel Core i7، حصلت على النتائج الموضحة في القائمة 9.

القائمة 9. إحصائيات الحلقة الداخلية
المواضيع: مرة واحدة على مدار الساعة (): 0.015229 مرة على الحائط: 0.0152249 المواضيع: مرتين على مدار الساعة (): 0.014221 مرة على الحائط: 0.00618792 المواضيع: 3 مرات على مدار الساعة (): 0.014541 مرة على الحائط: 0.00444412 المواضيع: 4 مرات على Clock (): 0.014666 الوقت على الحائط: 0.00440478 الخيوط: 5 مرات على مدار الساعة (): 0.01594 الوقت على الحائط: 0.00359988 الخيوط: 6 مرات على مدار الساعة (): 0.015069 الوقت على الحائط: 0.00303698 الخيوط: 7 مرات على مدار الساعة (): 0.016365 الوقت على الحائط: 0.00258303 الخيوط: 8 الوقت على مدار الساعة (): 0.01678 الوقت على الحائط: 0.00237703

على الرغم من أن وقت وحدة المعالجة المركزية (الوقت على مدار الساعة) تبين أنه هو نفسه تقريبًا في جميع الحالات (كما ينبغي أن يكون، دون مراعاة الوقت الإضافي الذي يقضيه إنشاء سلاسل الرسائل وتبديل السياق)، فإن الوقت الفعلي الذي نهتم به (الوقت على الحائط) انخفض باستمرار مع زيادة عدد الخيوط التي من المفترض أن يتم تنفيذها بالتوازي بواسطة نوى المعالج الفردية. لذلك، دعونا نسجل ملاحظة أخيرة حول بناء جملة pragma: #pragmaتوازي لـ Private(i) يعني أن متغير الحلقة i يتم التعامل معه كذاكرة خيط محلية؛ يحتوي كل مؤشر ترابط على نسخته الخاصة من هذا المتغير. لم تتم تهيئة المتغير المحلي لمؤشر الترابط.

أقسام التعليمات البرمجية الهامة في OpenMP

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

#pragma omp Critical (اسم القسم الاختياري) ( // لا يمكن لخيطين تنفيذ هذه المجموعة من التعليمات البرمجية في نفس الوقت)

لا يمكن تنفيذ التعليمات البرمجية التي تتبع التوجيه النقدي pragma omp إلا في مؤشر ترابط واحد في وقت معين. بالإضافة إلى ذلك، اسم القسم الاختياري هو معرف عام، ولا يمكن معالجة المقاطع الهامة التي لها نفس المعرف بواسطة خيطين في نفس الوقت. دعونا نلقي نظرة على الكود في القائمة 10.

القائمة 10. أقسام مهمة متعددة بنفس الأسماء
#pragma omp Critical (section1) ( myhashtable.insert("key1", "value1"); ) // ... يحتوي هذا على بعض التعليمات البرمجية الأخرى #pragma omp Critical (section1) ( myhashtable.insert("key2", "value2" ");

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

الأقفال وكائنات المزامنة في OpenMP

ومن المثير للاهتمام أن OpenMP يحتوي على إصداراته الخاصة من كائنات المزامنة (بعد كل شيء، OpenMP ليس مجرد براغما). لذا، ألقِ نظرة على النوع omp_lock_t، المحدد في ملف الرأس omp.h. يتم تقييم عمليات كائن المزامنة (mutex) بنمط pthread العادية على أنها صحيحة حتى لو كانت أسماء وظائف واجهة برمجة التطبيقات (API) هي نفسها. فيما يلي خمس وظائف لواجهة برمجة التطبيقات (API) تحتاج إلى معرفتها:

  • omp_init_lock: يجب استخدام وظيفة API هذه أولاً عند الوصول إلى النوع omp_lock_t وهي مخصصة للتهيئة. تجدر الإشارة إلى أنه بعد التهيئة مباشرة، سيكون القفل في الحالة الأولية (غير المحددة).
  • omp_destroy_lock: يدمر القفل. عند استدعاء وظيفة API هذه، يجب أن يكون القفل في حالته الأولية؛ هذا يعني أنه لا يمكنك استدعاء omp_set_lock ثم تدمير القفل.
  • omp_set_lock: يحدد omp_lock_t، أي ينشط كائن المزامنة (mutex). إذا لم يتمكن الخيط من الحصول على قفل، فإنه يستمر في الانتظار حتى تصبح الفرصة متاحة.
  • omp_test_lock: محاولات الحصول على القفل إذا كان متاحًا؛ إرجاع 1 عند النجاح و 0 عند الفشل. هذه الوظيفة غير مانعأي أنه لا يجبر الخيط على الانتظار حتى يتم الحصول على القفل.
  • omp_unset_lock: يعيد القفل إلى حالته الأصلية.

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

القائمة 11. تعزيز قائمة الانتظار ذات الخيوط الواحدة باستخدام OpenMP
#يشمل #include فئة "myqueue.h" omp_q: myqueue العامة (عام: typedef myqueue قاعدة؛ omp_q() ( omp_init_lock(&lock); ) ~omp_q() ( omp_destroy_lock(&lock); ) bool Push(const int& value) ( ​​omp_set_lock(&lock); نتيجة منطقية = this->base::push(value); omp_unset_lock(&lock) نتيجة الإرجاع؛ ); ) إرجاع النتيجة ) // وبالمثل بالنسبة للبوب الخاص: omp_lock_t lock; );

أقفال متداخلة

الأنواع الأخرى من الأقفال في OpenMP هي أقفال omp_nest_lock_t المتنوعة. إنها تشبه omp_lock_t ولكنها تتمتع بميزة إضافية تتمثل في إمكانية الحصول على القفل عدة مرات من خلال الخيط الذي يحمله. في كل مرة يتم فيها الحصول على قفل متداخل بواسطة الخيط الذي يحمله باستخدام omp_set_nest_lock، يتم زيادة عداد القفل الداخلي. يتم تحرير القفل عن طريق تعليق الخيط عندما يؤدي استدعاء واحد أو أكثر لـ omp_unset_nest_lock إلى تقليل عداد القفل الداخلي إلى 0. يتم استخدام وظائف API التالية للعمل مع omp_nest_lock_t:

  • omp_init_nest_lock(omp_nest_lock_t*): يضبط عداد التداخل الداخلي على 0 .
  • omp_destroy_nest_lock(omp_nest_lock_t*): يدمر القفل. يؤدي استدعاء وظيفة API هذه لقفل بقيمة عداد غير الصفر إلى نتائج غير متوقعة.
  • omp_set_nest_lock(omp_nest_lock_t*): يشبه omp_set_lock فيما عدا أن سلسلة المحادثات يمكنها الاتصال به عدة مرات.
  • omp_test_nest_lock(omp_nest_lock_t*): هو إصدار غير محظور من وظيفة omp_set_nest_lock API.
  • omp_unset_nest_lock(omp_nest_lock_t*): تحرير القفل عندما يصل عداد القفل الداخلي إلى 0. وفي حالات أخرى، يؤدي كل استدعاء لوظيفة API هذه إلى تقليل قيمة العداد.

السيطرة التفصيلية على إنجاز المهمة

لقد رأينا بالفعل أن كتلة التعليمات البرمجية التي تتبع التوجيه المتوازي pragma omp تتم معالجتها بالتوازي بواسطة جميع سلاسل العمليات. يمكن أيضًا تقسيم التعليمات البرمجية الموجودة داخل هذه الكتل إلى فئات سيتم تنفيذها في سلاسل عمليات محددة. دعونا نلقي نظرة على الكود في القائمة 12.

القائمة 12. استخدام المقاطع المتوازية عمليًا
int main() ( #pragma omp موازي ( cout<< "Это выполняется во всех потоках\n"; #pragma omp sections { #pragma omp section { cout << "Это выполняется параллельно\n"; } #pragma omp section { cout << "Последовательный оператор 1\n"; cout << "Это всегда выполняется после оператора 1\n"; } #pragma omp section { cout << "Это тоже выполняется параллельно\n"; } } } }

تتم معالجة التعليمات البرمجية التي تسبق توجيه أقسام pragma omp ومباشرة بعد التوجيه المتوازي pragma omp بالتوازي بواسطة جميع سلاسل العمليات. باستخدام توجيه pragma omp section، يتم تقسيم الكود الذي يتبعه إلى أقسام فرعية منفصلة. يمكن تنفيذ كل كتلة قسم pragma omp بواسطة مؤشر ترابط منفصل. ومع ذلك، يتم دائمًا تنفيذ العبارات الفردية داخل كتلة القسم بشكل تسلسلي. تعرض القائمة 13 نتائج تشغيل الكود في القائمة 12.

القائمة 13. نتائج تنفيذ الكود من القائمة 12
tintin$ ./a.out هذا يعمل في جميع المواضيع هذا يعمل في جميع المواضيع هذا يعمل في جميع المواضيع هذا يعمل في جميع المواضيع هذا يعمل في جميع المواضيع هذا يعمل في جميع المواضيع هذا يعمل في جميع المواضيع هذا يعمل بالتوازي بيان تسلسلي 1 هذا يتم تنفيذه أيضًا بالتوازي ويتم تنفيذه دائمًا بعد العبارة 1

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

التوجيهات الخاصة الأولى والأخيرة خاصة بالتزامن مع الحلقات المتوازية

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

التوجيه الخاص الأول

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

القائمة 14. استخدام متغير مؤشر ترابط محلي غير متزامن مع مؤشر الترابط الرئيسي
#يشمل #يشمل int main() ( int idx = 100; #pragma omp موازي خاص(idx) ( printf("On thread %d idx = %d\n", omp_get_thread_num(), idx); ) )

إليك ما حصلت عليه نتيجة لذلك (قد تختلف نتائجك).

في الدفق 1 idx = 1 في الدفق 5 idx = 1 في الدفق 6 idx = 1 في الدفق 0 idx = 0 في الدفق 4 idx = 1 في الدفق 7 idx = 1 في الدفق 2 idx = 1 في الدفق 3 idx = 1

تحتوي القائمة 15 على رمز يستخدم التوجيه firstprivate. كما هو متوقع، يُظهر الإخراج أنه تم تعيين المتغير idx على 100 في كافة سلاسل الرسائل.

القائمة 15. استخدام التوجيه firstprivate لتهيئة متغيرات مؤشر الترابط المحلية
#يشمل #يشمل int main() ( int idx = 100; #pragma omp موازية firstprivate(idx) ( printf("On thread %d idx = %d\n", omp_get_thread_num(), idx); ) )

لاحظ أيضًا أنه تم استخدام طريقة omp_get_thread_num() للوصول إلى معرف مؤشر الترابط. يختلف هذا المعرف عن إخراج المعرف بواسطة الأمر العلوي لنظام التشغيل Linux®، وهذا المخطط هو مجرد وسيلة لـ OpenMP لتتبع عدد سلاسل العمليات. إذا كنت تخطط لاستخدام التوجيه firstprivate في كود C++، فلاحظ ميزة أخرى: المتغير المستخدم بواسطة التوجيه firstprivate هو مُنشئ نسخة لتهيئة نفسه من متغير مؤشر ترابط رئيسي، لذلك إذا كان مُنشئ النسخ خاصًا لفصلك، فقد يكون ذلك تؤدي إلى عواقب غير سارة. الآن دعنا ننتقل إلى التوجيه الخاص الأخير، والذي يمثل في كثير من النواحي الوجه الآخر للعملة.

التوجيه الخاص الأخير

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

القائمة 16. حلقة متوازية بدون مزامنة البيانات مع الخيط الرئيسي
#يشمل #يشمل int main() ( int idx = 100; int main_var = 2120; #pragma omp موازي للخاص(idx) لـ (idx = 0; idx< 12; ++idx) { main_var = idx * idx; printf("В потоке %d idx = %d main_var = %d\n", omp_get_thread_num(), idx, main_var); } printf("Возврат в главный поток со значением переменной main_var = %d\n", main_var); }

على جهازي ذي 8 مراكز، يقوم OpenMP بإنشاء ستة خيوط للكتلة المتوازية. يحتوي كل خيط بدوره على تكرارين في الحلقة. تعتمد القيمة النهائية للمتغير main_var على الخيط الذي تم تنفيذه أخيرًا، وبالتالي على قيمة المتغير idx في هذا الخيط. بمعنى آخر، لا تعتمد قيمة المتغير main_var على القيمة الأخيرة للمتغير idx، ولكنها تعتمد على القيمة التي يحتوي عليها المتغير idx في مؤشر الترابط الذي تم تنفيذه آخر مرة. هذا المثال موضح في القائمة 17.

القائمة 17. اعتماد قيمة المتغير main_var على آخر خيط تم تنفيذه
في الموضوع 4 idx = 8 main_var = 64 في الموضوع 2 idx = 4 main_var = 16 في الموضوع 5 idx = 10 main_var = 100 في الموضوع 3 idx = 6 main_var = 36 في الموضوع 0 idx = 0 main_var = 0 في الموضوع 1 idx = 2 main_var = 4 في الموضوع 4 idx = 9 main_var = 81 في الموضوع 2 idx = 5 main_var = 25 في الموضوع 5 idx = 11 main_var = 121 في الموضوع 3 idx = 7 main_var = 49 في الموضوع 0 idx = 1 main_var = 1 في الموضوع 1 idx = 3 main_var = 9 ارجع إلى الموضوع الرئيسي بقيمة المتغير main_var = 9

قم بتشغيل الكود الموجود في القائمة 17 عدة مرات للتأكد من أن قيمة main_var في الخيط الرئيسي تعتمد دائمًا على قيمة idx في آخر خيط تم تشغيله. ماذا لو كنت بحاجة إلى مزامنة قيمة متغير الخيط الرئيسي مع القيمة النهائية لمتغير idx في الحلقة؟ هذا هو المكان الذي يكون فيه التوجيه lastprivate مفيدًا، كما هو موضح في القائمة 18. كما هو الحال مع المثال السابق، قم بتشغيل الكود في القائمة 18 عدة مرات وسترى أن القيمة النهائية للمتغير main_var في الموضوع الرئيسي هي 121 (أي قيمة المتغير idx في التكرار الأخير للحلقة).

القائمة 18. المزامنة باستخدام التوجيه الأخير الخاص
#يشمل #يشمل int main() ( int idx = 100; int main_var = 2120; #pragma omp موازي للخاص (idx) lastprivate(main_var) لـ (idx = 0; idx)< 12; ++idx) { main_var = idx * idx; printf("В потоке %d idx = %d main_var = %d\n", omp_get_thread_num(), idx, main_var); } printf("Возврат в главный поток со значением переменной main_var = %d\n", main_var); }

تعرض القائمة 19 نتائج تشغيل الكود في القائمة 18.

القائمة 19. نتائج تنفيذ التعليمات البرمجية من القائمة 18 (لاحظ أنه يتم دائمًا تعيين main_var دائمًا على 121 في الموضوع الرئيسي)
في الموضوع 3 idx = 6 main_var = 36 في الموضوع 2 idx = 4 main_var = 16 في الموضوع 1 idx = 2 main_var = 4 في الموضوع 4 idx = 8 main_var = 64 في الموضوع 5 idx = 10 main_var = 100 في الموضوع 3 idx = 7 main_var = 49 في الخيط 0 idx = 0 main_var = 0 في الخيط 2 idx = 5 main_var = 25 في الخيط 1 idx = 3 main_var = 9 في الخيط 4 idx = 9 main_var = 81 في الخيط 5 idx = 11 main_var = 121 فولت Thread 0 idx = 1 main_var = 1 ارجع إلى الموضوع الرئيسي بقيمة المتغير main_var = 121

ملاحظة أخيرة: لدعم العامل الأخير الخاص في كائن C++، يجب أن يكون للفئة المقابلة عامل عام = طريقة متاحة.

دمج الفرز في OpenMP

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

القائمة 20. دمج الفرز في OpenMP
#يشمل #يشمل #يشمل استخدام اسم للمحطة؛ المتجه دمج (ناقل ثابت & يسار، ناقل ثابت واليمين) (المتجه نتيجة؛ left_it غير الموقعة = 0، right_it = 0؛ بينما (left_it< left.size() && right_it < right.size()) { if(left < right) { result.push_back(left); left_it++; } else { result.push_back(right); right_it++; } } // Занесение оставшихся данных из обоих векторов в результирующий while(left_it < left.size()) { result.push_back(left); left_it++; } while(right_it < right.size()) { result.push_back(right); right_it++; } return result; } vectorدمج الفرز (ناقل & vec, int threads) ( // شرط الإنهاء: يتم فرز القائمة بالكامل إذا // كانت تحتوي على عنصر واحد فقط. if(vec.size() == 1) ( return vec; ) // تحديد موقع الوسط العنصر في ناقل الأمراض المنقولة جنسيا ::vector ::iterator middle = vec.begin() + (vec.size() / 2); المتجه left(vec.begin(), middle); المتجه حق (وسط، vec.end ())؛ // إجراء فرز دمج على متجهين أصغر if (threads > 1) ( #pragma omp أقسام متوازية ( #pragma omp section ( left = mergesort(left, threads/2);) #pragma omp section ( right = mergesort(right, المواضيع - المواضيع/2) ) ) آخر ( left = mergesort(left, 1); right = mergesort(right, 1); ) return merge(left, right); ) int main() ( Vector ت(1000000); ل (طويل ط = 0؛ ط<1000000; ++i) v[i] = (i * i) % 1000000; v = mergesort(v, 1); for (long i=0; i<1000000; ++i) cout << v[i] << "\n"; }

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

خاتمة

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

البرمجة الموازية مع OpenMP

مقدمة................................................. .......................................................... ............. ........................................... ......

البرمجة الموازية ........................................... ................ .................................................. .................

كتابة البرامج الموازية ........................................... .......... ........................................... ........

البنى المتوازية ................................ ................................ . ... .............................................................. ......... ............

OpenMP................................................................ .. ................................................ .......................................................... .....

مقدمة إلى برنامج OpenMP.............................................. .... .............................................. .......... ..........................

نموذج برنامج OpenMP ........................................... .... .............................................. .......... .....

كيف تتفاعل المواضيع؟ ........................................... .......................................................................... ........................... .....

أساسيات برنامج OpenMP................................................ ... .............................................................. ......... ..............................................

بناء الجملة................................................. .................................................. ...... ....................................

المناطق الموازية ........................................... ... .............................................................. ......... ...............

نموذج التنفيذ ........................................... ... .............................................................. ......... ....................

بنيات OpenMP .............................................. ... .............................................................. ......... ..........................

شروط الوفاء ........................................... .... .............................................. .......... ...................

الشروط خاص، مشترك، افتراضي ........................................... .......................................................... ............. ...

الشرط الأول خاص ............................ ... .............................................................. ......... ...............

بنيات OpenMP لتوزيع العمل ........................................... ........................... ...........................

بالتوازي مع / DO حلقة ........................................... .......................................................... ............. .....

المقاطع الموازية ........................................... ... .............................................................. ......... ..........

تصميم واحد ........................................... ... .............................................................. ......... ................

شروط الاستيفاء (2) ........................................... .......................................................... ............. ...............

إذا كان الشرط ........................................... ... .............................................................. ......... ..............................................

الحالة الأخيرة خاصة ............................ ... .............................................................. ......... .............

شرط التخفيض ........................................... ... .............................................................. ......... ...............

شرط الجدول الزمني ........................................... ... .............................................................. ......... ...................

حالة أمر ........................................... ... .............................................................. ......... ....................

متغيرات بيئة OpenMP ........................................... ..... ................................................ ......

وظائف مكتبة OpenMP ........................................... .................... .............................. ........................... ..

تبعية البيانات .............................................. .......................................................... .............. ...............

أدوات المزامنة في OpenMP................................................. ...... ........................................................... ........

جزء حرج................................................ ... .............................................................. ......... ...................

القسم الذري ........................................... ... .............................................................. ......... ........................

الحواجز................................................. .......................................................... ............. ........................................... .

تثبيت أمر التنفيذ ........................................... ........................... ............................. ........................... .

البناء المتدفق ........................................... ... .............................................................. ......... ..........................

إمكانيات OpenMP المتقدمة................................................ .................... .............................. .................

تصحيح أخطاء كود OpenMP ........................................... ..... ................................................ .......................... ...........................

ضبط أداء كود OpenMP................................................. ...... ................................

النهج الأساسي .............................................. ... .............................................................. ......... .......................

التحليل التلقائي ........................................... ................... .............................................. ............

التعريف بالبرنامج ........................................... .... .............................................. .......... ....

التسلسل الهرمي للذاكرة ........................................... .... .............................................. .......... ........................

مهام................................................. .................................................. ...... ........................................................... ...

مهمة 1................................................ ... .............................................................. ......... ...........................................

المهمة 2 .............................................. ... .............................................................. ......... ...........................................

المهمة 3 ........................................... ... .............................................................. ......... ...........................................

المهمة 4 .............................................. ... .............................................................. ......... ...........................................

المهمة 5 ........................................... ... .............................................................. ......... ...........................................

المهمة 6 .............................................. ... .............................................................. ......... ...........................................

مقدمة

البرمجة الموازية

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

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

كتابة البرامج الموازية

يتكون تطوير البرامج الموازية (PP) من ثلاث مراحل رئيسية:

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

توزيع المهمة بين المعالجات (المعالجات الافتراضية). في بعض الحالات، يمكن ترك هذه المشكلة لتقدير بيئة تشغيل البرنامج.

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

البنى المتوازية

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

أنظمة الذاكرة الموزعة. كل معالج له ذاكرته الخاصة ولا يمكنه الوصول مباشرة إلى ذاكرة معالج آخر.

عند تطوير برامج مثل الأنظمة، يجب على المبرمج أن يحدد بوضوح نظام الاتصال بأكمله (Message Passing). المكتبات: MPI، PVM، Shmem (Cray فقط).

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

مناهج تطوير البرمجيات: الخيوط، توجيهات المترجم (OpenMP)، آلية تمرير الرسائل.

الأنظمة المجمعة. يمكن دمج أجهزة الكمبيوتر ذات التكوينات المختلفة في مجموعات.

  • كانج سو جاتلين
  • بيت إيسينسي

تنفيذ تعدد العمليات دون بذل جهد إضافي

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

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

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

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

تمكين OpenMP في Visual C++

تم تطوير معيار OpenMP في عام 1997 كواجهة برمجة التطبيقات (API) التي تهدف إلى كتابة تطبيقات محمولة ومتعددة الخيوط. في البداية كان يعتمد على لغة فورتران، لكنه شمل لاحقًا لغة C/C++. أحدث إصدار من OpenMP هو 2.0؛ إنه مدعوم بالكامل بواسطة Visual C++ 2005. كما يتم دعم معيار OpenMP أيضًا بواسطة النظام الأساسي Xbox 360.

قبل أن تقوم بالبرمجة، يجب أن تعرف كيفية تمكين ميزات OpenMP الخاصة بالمترجم. للقيام بذلك، استخدم خيار برنامج التحويل البرمجي /openmp المقدم في Visual C++ 2005. (يمكنك تمكين توجيهات OpenMP في صفحات خصائص المشروع عن طريق تحديد خصائص التكوين وC/C++ واللغة وتغيير قيمة خاصية دعم OpenMP.) عند مواجهة الخيار /openmp، يقوم المترجم بتعريف رمز _OPENMP، والذي يمكن أن يكون يُستخدم لتحديد ما إذا كانت مرافق OpenMP ممكّنة أم لا. للقيام بذلك، فقط اكتب #ifndef _OPENMP.

يتواصل OpenMP مع التطبيقات من خلال مكتبة الاستيراد vcomp.lib. تسمى مكتبة وقت التشغيل المقابلة vcomp.dll. تدعم إصدارات تصحيح الأخطاء لمكتبات الاستيراد ووقت التشغيل (vcompd.lib وvcompd.dll، على التوالي) رسائل الخطأ الإضافية التي تم إنشاؤها بواسطة بعض العمليات غير القانونية. يرجى ملاحظة أن Visual C++ لا يدعم الارتباط الثابت مع مكتبة وقت التشغيل OpenMP، على الرغم من أن إصدار Xbox 360 يدعم ذلك.

المعالجة المتوازية في OpenMP

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

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

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

عند النقطة 3، تنتهي المنطقة الموازية المتداخلة. يقوم كل خيط في منطقة متوازية متداخلة بمزامنة حالته مع سلاسل الرسائل الأخرى في تلك المنطقة، لكن المناطق المختلفة لا تتزامن مع بعضها البعض. عند النقطة 4 تنتهي المنطقة الموازية الأولى، وعند النقطة 5 تبدأ منطقة جديدة. يتم الاحتفاظ بالبيانات المحلية لكل مؤشر ترابط في الفواصل الزمنية بين المناطق المتوازية.

هذه هي أساسيات نموذج التنفيذ في OpenMP. أنت الآن جاهز لمعرفة من أين تبدأ في تطوير تطبيق موازي.

بنيات OpenMP

يعد OpenMP سهل الاستخدام ويتضمن نوعين أساسيين فقط من البنيات: توجيهات pragma ووظائف وقت تشغيل OpenMP. عادةً ما تخبر توجيهات براغما المترجم بتنفيذ التنفيذ المتوازي لكتل ​​التعليمات البرمجية. كل هذه التوجيهات تبدأ بـ #pragma omp. مثل أي توجيهات براغماية أخرى، يتم تجاهلها من قبل مترجم لا يدعم تقنية معينة - في هذه الحالة OpenMP.

تُستخدم وظائف OpenMP بشكل أساسي لتغيير واسترجاع معلمات البيئة. بالإضافة إلى ذلك، يتضمن OpenMP وظائف API لدعم بعض أنواع المزامنة. لاستخدام وظائف مكتبة وقت التشغيل (وقت التشغيل) OpenMP هذه، يجب عليك تضمين ملف الرأس omp.h في برنامجك. إذا كنت تستخدم توجيهات OpenMP pragma فقط في تطبيقك، فلن تحتاج إلى تضمين هذا الملف.

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

#pragma المرجع<директива>[القسم [[،] القسم]...]

يدعم OpenMP التوجيهات المتوازية، لـ، المتوازية لـ، القسم، الأقسام، الفردية، الرئيسية، الحرجة، المتدفقة، المرتبة، والذرية، والتي تحدد إما آليات مشاركة العمل أو بنيات المزامنة. في هذه المقالة سنناقش معظم التوجيهات.

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

تنفيذ المعالجة المتوازية

على الرغم من وجود العديد من توجيهات OpenMP، إلا أننا لن نحتاج إليها جميعًا مرة واحدة. التوجيه الأكثر أهمية والأكثر شيوعًا هو الموازي. يقوم بإنشاء منطقة موازية للكتلة المنظمة التي تتبعها، على سبيل المثال:

#pragma omp الموازي [section[ [,] section]...] كتلة منظمة

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

على سبيل المثال، خذ بعين الاعتبار برنامج "Hello World" الكلاسيكي:

#pragma omp متوازي ( printf("Hello World\n"); )

في نظام المعالج المزدوج، تتوقع بالطبع الحصول على ما يلي:

مرحبا بالعالم مرحبا بالعالم

ومع ذلك، يمكن أن تكون النتيجة مثل هذا:

HellHell أو WorWlodrl د

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

دعونا نلقي نظرة على مثال أكثر جدية يحدد متوسط ​​عنصرين من عناصر المصفوفة المتجاورة ويكتب النتائج إلى مصفوفة أخرى. يستخدم هذا المثال بنية OpenMP الجديدة #pragma omp، والتي تتعلق بتوجيهات مشاركة العمل. لا تُستخدم هذه التوجيهات للتنفيذ المتوازي للتعليمات البرمجية، بل للتوزيع المنطقي لمجموعة من مؤشرات الترابط من أجل تنفيذ بنيات منطق التحكم المحددة. ينص التوجيه #pragma omp على أنه عند تنفيذ حلقة for في منطقة متوازية، يجب توزيع تكرارات الحلقة بين سلاسل عمليات المجموعة:

#pragma omp موازي ( #pragma omp for(int i = 1; i< size; ++i) x[i] = (y + y)/2; }

إذا كان هذا الرمز يعمل على جهاز كمبيوتر بأربعة معالجات وكان الحجم بقيمة 100، فيمكن تعيين التكرارات 1-25 للمعالج الأول، و26-50 للثاني، و51-75 للثالث، و76-99 للمعالج الأول. الرابع. يعد هذا أمرًا نموذجيًا لسياسة الجدولة التي تسمى ثابتة. سنناقش سياسات الجدولة لاحقًا.

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

إذا حذفنا #pragma omp للتعليمات من المثال الذي قدمناه للتو، فسيقوم كل خيط بتنفيذ حلقة for كاملة، مما يؤدي إلى الكثير من العمل غير الضروري:

#pragma omp موازي ( for(int i = 1; i< size; ++i) x[i] = (y + y)/2; }

نظرًا لأن الحلقات هي البناء الأكثر شيوعًا حيث يمكن موازنة تنفيذ التعليمات البرمجية، فإن OpenMP يدعم طريقة مختصرة لكتابة مزيج من #pragma ompتوازي و #pragma omp للتوجيهات:

#pragma omp موازي لـ for(int i = 1; i< size; ++i) x[i] = (y + y)/2;

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

من أجل (int i = 1؛ i<= n; ++i) // цикл 1 a[i] = a + b[i]; for(int i = 0; i < n; ++i) // цикл 2 x[i] = x + b[i];

تعد موازنة الحلقة 1 مشكلة لأنه لإجراء التكرار i، تحتاج إلى معرفة نتيجة التكرار i-1، أي أن التكرار i يعتمد على التكرار i-1. تعد حلقة التوازي 2 أيضًا مشكلة، ولكن لسبب مختلف. في هذه الحلقة، يمكنك تقييم قيمة x[i] إلى x، ومع ذلك، بمجرد القيام بذلك، لن تتمكن بعد ذلك من تقييم قيمة x. لوحظ اعتماد التكرار i-1 على التكرار i.

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

بالإضافة إلى ذلك، يضع OpenMP قيودًا على حلقات for التي يمكن تضمينها في #pragma omp للكتلة أو #pragma omp الموازي للكتلة. يجب أن تتبع حلقات For التنسيق التالي:

من أجل ([نوع عدد صحيح] i = حلقة ثابتة؛ i (<,>,=,<=,>=) حلقة ثابتة؛ أنا (+،-)= حلقة ثابتة)

تم تقديم هذه المتطلبات حتى يتمكن OpenMP من تحديد عدد التكرارات عند الدخول إلى الحلقة.

مقارنة دعم الخيوط في OpenMP وWin32

نعتقد أنه سيكون من المفيد مقارنة المثال الذي قدمناه للتو، والذي يتضمن #pragma omp الموازي للتوجيه، مع الكود الذي سيتعين علينا كتابته لحل نفس المشكلة باستخدام Windows API. كما ترون في القائمة 1، يتطلب الأمر الكثير من التعليمات البرمجية لتحقيق نفس النتيجة، وخلف الكواليس، يقوم هذا الخيار بالكثير من العمل. وبالتالي، يحدد منشئ فئة ThreadData ما يجب أن تكون عليه قيم البداية والتوقف في كل مرة يتم فيها استدعاء مؤشر الترابط. يعالج OpenMP كل هذه التفاصيل بنفسه ويزود المبرمج بوسائل إضافية لتكوين المناطق المتوازية والتعليمات البرمجية.

القائمة 1. تعدد مؤشرات الترابط في Win32

Class ThreadData ( public: // يقوم المُنشئ بتهيئة حقول البداية والإيقاف ThreadData(int threadNum); int start; int stop; ); DWORD ThreadFn(void* passInData) ( ThreadData *threadData = (ThreadData *)passedInData; for(int i = threadData->start; i< threadData->قف؛ ++i) x[i] = (y + y) / 2; العودة 0؛ ) void ParallelFor() ( // ابدأ مجموعات من المواضيع for(int i=0; i< nTeams; ++i) ResumeThread(hTeams[i]); // Для каждого потока здесь неявно вызывается // метод ThreadFn // Ожидание завершения работы WaitForMultipleObjects(nTeams, hTeams, TRUE, INFINITE); } int main(int argc, char* argv) { // Создание групп потоков for(int i=0; i < nTeams; ++i) { ThreadData *threadData = new ThreadData(i); hTeams[i] = CreateThread(NULL, 0, ThreadFn, threadData, CREATE_SUSPENDED, NULL); } ParallelFor(); // имитация OpenMP-конструкции parallel for // Очистка for(int i=0; i < nTeams; ++i) CloseHandle(hTeams[i]); }

البيانات العامة والخاصة

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

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

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

القائمة 2. أقسام توجيه OpenMP ومتداخلة للحلقة

المبلغ العائم = 10.0f؛ MatrixClass myMatrix; int j = myMatrix.RowStart(); كثافة العمليات أنا؛ #pragma omp موازي ( #pragma omp لـ firstprivate(j) lastprivate(i) تخفيض(+: sum) for(i = 0; i< count; ++i) { int doubleI = 2 * i; for(; j < doubleI; ++j) { sum += myMatrix.GetElement(i, j); } } }

ثانيا، المتغيرات المحلية لكتل ​​المناطق الموازية خاصة. في التين. 3 المتغير doubleI هو كذلك لأنه تم الإعلان عنه في المنطقة الموازية. أي متغيرات غير ثابتة وغير MatrixClass تم الإعلان عنها في أسلوب myMatrix::GetElement ستكون خاصة.

ثالثًا، أي متغيرات محددة في الأقسام خاص، وأول خاص، وأخير خاص، وتخفيض ستكون خاصة. في القائمة 2، أصبحت المتغيرات i وj وsum خاصة بكل موضوع في المجموعة، مما يعني أن كل موضوع سيكون له نسخته الخاصة من كل من هذه المتغيرات.

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

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

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

قسم التخفيض له دلالات مماثلة، لكنه يقبل متغيرا وعاملا. يتم سرد العوامل التي يدعمها هذا القسم في الجدول. 1، ويجب أن يكون المتغير من النوع العددي (مثل float أو int أو long، ولكن ليس std::vector أو int وما إلى ذلك). تتم تهيئة متغير قسم التخفيض في كل موضوع بالقيمة المحددة في الجدول. في نهاية كتلة التعليمات البرمجية، يتم تطبيق بيان شرط التخفيض على كل نسخة خاصة من المتغير بالإضافة إلى القيمة الأصلية للمتغير.

طاولة 1. مشغلي قسم التخفيض

في القائمة 2، تتم تهيئة المبلغ ضمنيًا في كل مؤشر ترابط إلى 0.0f (لاحظ أن الجدول يعرض قيمة أساسية تبلغ 0، ولكن في هذه الحالة يأخذ النموذج 0.0f لأن المجموع عبارة عن عدد عائم). بعد تنفيذ #pragma omp للكتلة، يتم تنفيذ العملية + على جميع القيم الجزئية وقيمة المجموع الأصلية (والتي في حالتنا هي 10.0f). يتم تعيين النتيجة إلى مجموع المتغير المشترك الأصلي.

المعالجة المتوازية في بنيات أخرى غير الحلقات

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

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

القائمة 3. الفرز السريع باستخدام الأقسام المتوازية

فرز سريع باطلة (int numList، int nLower، int nUpper) (if (nLower< nUpper) { // Разбиение интервала сортировки int nSplit = Partition (numList, nLower, nUpper); #pragma omp parallel sections { #pragma omp section QuickSort (numList, nLower, nSplit - 1); #pragma omp section QuickSort (numList, nSplit + 1, nUpper); } } }

في هذا المثال، يقوم التوجيه #pragma الأول بإنشاء منطقة قسم متوازية. يتم تعريف كل قسم بواسطة توجيه القسم #pragma omp. يتم تعيين مؤشر ترابط واحد من مجموعة سلاسل الرسائل لكل قسم في منطقة متوازية، ويتم تنفيذ جميع الأقسام في وقت واحد. يستدعي كل قسم أسلوب QuickSort بشكل متكرر.

كما هو الحال مع #pragma omp الموازي للبناء، يجب عليك التأكد من أن الأقسام مستقلة عن بعضها البعض حتى يمكن تنفيذها بالتوازي. إذا تم تغيير الموارد المشتركة عبر الأقسام دون مزامنة الوصول إليها، فقد تكون النتيجة غير متوقعة.

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

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

سيؤدي تجميع هذا التطبيق بدون خيار /openmp إلى إنشاء الإصدار التسلسلي الصحيح. إحدى مزايا OpenMP هي أنه متوافق مع المترجمين الذين لا يدعمون OpenMP.

توجيهات براغما للتزامن

عند تشغيل عدة سلاسل رسائل في وقت واحد، غالبًا ما تكون هناك حاجة لمزامنتها. يدعم OpenMP عدة أنواع من المزامنة التي تساعد في العديد من المواقف.

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

يتم أيضًا إجراء مزامنة الحاجز الضمني في نهاية كل كتلة من المقاطع #pragma omp و #pragma omp المفردة و #pragma omp. لتعطيل مزامنة الحاجز الضمني في أي من كتل مشاركة العمل الثلاث هذه، حدد قسم "الآن":

#pragma omp موازي ( #pragma omp for nowait for(int i = 1; i< size; ++i) x[i] = (y + y)/2; }

كما ترون، يقول هذا القسم من توجيه التوازي أنه ليست هناك حاجة لمزامنة سلاسل الرسائل في نهاية الحلقة، على الرغم من أنها ستظل متزامنة في نهاية المنطقة المتوازية.

النوع الثاني هو تزامن الحاجز الصريح. في بعض المواقف، يُنصح بتنفيذها جنبًا إلى جنب مع الحالة الضمنية. للقيام بذلك، قم بتضمين توجيه الحاجز #pragma omp في التعليمات البرمجية الخاصة بك.

يمكن استخدام الأقسام الحرجة كحواجز. في Win32 API، يتم استخدام الدالتين EnterCriticalSection وLeafCriticalSection للدخول إلى قسم حرج والخروج منه. في OpenMP، يتم استخدام التوجيه #pragma omp Critical [name] لهذا الغرض. يحتوي على نفس دلالات القسم الحرج لـ Win32 ويعتمد على EnterCriticalSection. يمكنك استخدام قسم حرج مسمى، ومن ثم يكون الوصول إلى مجموعة من التعليمات البرمجية حصريًا بشكل متبادل فقط للأقسام الهامة الأخرى التي تحمل نفس الاسم (وهذا ينطبق على العملية بأكملها). إذا لم يتم تحديد اسم، فسيتم مطابقة التوجيه مع بعض الأسماء التي اختارها النظام. الوصول إلى كافة الأقسام الهامة غير المسماة يستبعد بعضها البعض.

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

في بعض الأحيان لا تكون إمكانيات التوجيه الفردي كافية. في بعض الحالات، تريد تنفيذ كتلة من التعليمات البرمجية بواسطة مؤشر الترابط الرئيسي - على سبيل المثال، إذا كان هذا الخيط مسؤولاً عن معالجة واجهة المستخدم الرسومية وتحتاج إليه لأداء بعض المهام. ثم يتم تطبيق التوجيه #pragma omp master. على عكس التوجيه الفردي، لا يوجد حاجز ضمني عند الدخول إلى الكتلة الرئيسية أو الخروج منها.

لإكمال جميع عمليات الذاكرة المعلقة قبل بدء العملية التالية، استخدم توجيه #pragma omp Flush، وهو ما يعادل وظيفة المترجم الداخلي _ReadWriteBarrier.

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

#pragma omp موازي ( if(omp_get_thread_num() > 3) ( #pragma omp Single // الكود غير متاح لجميع سلاسل الرسائل x++; ) )

إجراءات وقت التشغيل OpenMP

بالإضافة إلى التوجيهات الموضحة بالفعل، يدعم OpenMP عددًا من الإجراءات المفيدة. تنقسم هذه إلى ثلاث فئات واسعة: وقت التشغيل، والقفل/المزامنة، ووظائف المؤقت (لم يتم تناول هذه الأخيرة في هذه المقالة). كل هذه الوظائف لها أسماء تبدأ بـ omp_ ويتم تعريفها في ملف الرأس omp.h.

تسمح لك إجراءات الفئة الأولى بالاستعلام وتعيين معلمات مختلفة لبيئة تشغيل OpenMP. لا يمكن استدعاء الوظائف التي تبدأ أسماؤها بـ omp_set_ إلا خارج المناطق المتوازية. يمكن استخدام جميع الوظائف الأخرى داخل المناطق المتوازية وخارجها.

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

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

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

لتعيين وقراءة الخصائص التي تحدد إمكانية إنشاء سلاسل الرسائل وتداخل المناطق المتوازية ديناميكيًا، استخدم الوظائف omp_set_dynamic وomp_get_dynamic وomp_set_nested وomp_get_nested. بالإضافة إلى ذلك، يمكن لكل مؤشر ترابط طلب معلومات حول بيئته. لمعرفة رقم الموضوع في مجموعة المواضيع، اتصل بـ omp_get_thread_num. تذكر أنه لا يُرجع معرف مؤشر ترابط Windows، بل يُرجع رقمًا في النطاق من 0 إلى omp_get_num_threads - 1.

تتيح الدالة omp_in_parallel للخيط معرفة ما إذا كان يقوم حاليًا بتنفيذ منطقة متوازية، وتقوم omp_get_num_procs بإرجاع عدد المعالجات في الجهاز.

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

القائمة 4. استخدام إجراءات وقت تشغيل OpenMP

#يشمل #يشمل int main() ( omp_set_dynamic(1); omp_set_num_threads(10); #pragma omp موازية // منطقة متوازية 1 ( #pragma omp Single printf("عدد المواضيع في المنطقة الديناميكية = %d\n"، omp_get_num_threads()); ) printf("\n"); omp_set_dynamic(0); omp_set_num_threads(10); , omp_get_num_threads()); printf("\n"); omp_set_num_threads(10); #pragma omp موازية // منطقة متوازية 3 ( #pragma omp Single printf("عدد المواضيع في المنطقة المعطلة هو = %d\n", omp_get_num_threads() ) ) printf("\n"); omp_set_nested(1); d\n"، omp_get_num_threads() ) )

بتجميع هذا الكود في Visual Studio 2005 وتشغيله على جهاز كمبيوتر عادي ثنائي المعالج، حصلنا على النتيجة التالية:

عدد سلاسل الرسائل في المنطقة الديناميكية = 2 عدد سلاسل الرسائل في المنطقة غير الديناميكية = 10 عدد سلاسل الرسائل في منطقة التداخل المعطلة = 1 عدد سلاسل الرسائل في منطقة التداخل المعطلة = 1 عدد سلاسل الرسائل في المنطقة المتداخلة = 2 عدد سلاسل الرسائل في المنطقة المتداخلة هو = 2

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

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

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

Omp_set_dynamic(0); omp_set_nested(1); omp_set_num_threads(10); #pragma omp موازي ( #pragma omp موازي ( #pragma omp Single printf("عدد سلاسل الرسائل في المنطقة المتداخلة هو = %d\n"، omp_get_num_threads()); ) )

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

عدد المواضيع في المنطقة المتداخلة = 10 عدد المواضيع في المنطقة المتداخلة = 10 عدد المواضيع في المنطقة المتداخلة = 10 عدد المواضيع في المنطقة المتداخلة = 10 عدد المواضيع في المنطقة المتداخلة = 10 عدد المواضيع في المنطقة المتداخلة = 10 عدد المواضيع في المنطقة المتداخلة = 10 عدد المواضيع في المنطقة المتداخلة = 10 عدد المواضيع في المنطقة المتداخلة = 10 عدد المواضيع في المنطقة المتداخلة = 10

طرق المزامنة/القفل

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

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

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

طاولة 2. وظائف للعمل مع الأقفال في OpenMP وWin32

حظر OpenMP بسيط قفل OpenMP المتداخل وظيفة Win32
omp_lock_t omp_nest_lock_t جزء حرج
omp_init_lock omp_init_nest_lock تهيئة القسم الحرج
omp_destroy_lock omp_destroy_nest_lock حذف القسم الحرج
omp_set_lock omp_set_nest_lock أدخل القسم الحرج
omp_unset_lock omp_unset_nest_lock اترك القسم الحرج
omp_test_lock omp_test_nest_lock حاولEnterCriticalSection

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

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

المعالجة المتوازية لهياكل البيانات

تعرض القائمة 5 رمز حلقتين تعملان بالتوازي، وفي بدايتهما لا يعرف وقت التشغيل عدد التكرارات. يتكرر المثال الأول عبر عناصر حاوية std::vector STL، ويتكرر المثال الثاني عبر عناصر القائمة المرتبطة القياسية.

القائمة 5. إجراء عدد غير معروف من التكرارات

#pragma ompتوازي ( // المعالجة المتوازية لمتجه STL std::vector ::مكرر مكرر; for(iter = xVect.begin(); iter != xVect.end(); ++iter) ( #pragma omp Single nowait (process1(*iter); ) ) // معالجة متوازية لقائمة مرتبطة قياسية for(LList) *listWalk = listHead; listWalk != NULL; listWalk = listWalk->next) ( #pragma omp Single nowait (process2(listWalk); ) )

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

تجدر الإشارة إلى أنه في مثال ناقل STL، قبل الدخول إلى الحلقة، يمكننا تحديد عدد تكراراتها بقيمة std::vector.size، والتي تسمح لنا بإحضار الحلقة إلى النموذج المتعارف عليه لـ OpenMP :

#pragma omp موازي لـ for(int i = 0; i< xVect.size(); ++i) process(xVect[i]);

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

خوارزميات جدولة أكثر تعقيدًا

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

لتحديد إحدى آليات الجدولة هذه، استخدم قسم الجدولة في #pragma omp أو #pragma omp الموازي للتوجيه. يبدو شكل هذا القسم كما يلي:

الجدول الزمني (خوارزمية الجدولة [، عدد التكرارات])

وفيما يلي أمثلة على هذه التوجيهات:

#pragma omp موازية للجدول الزمني (ديناميكي، 15) for(int i = 0; i< 100; ++i) ... #pragma omp parallel #pragma omp for schedule(guided)

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

في الجدولة المُدارة، يتم تحديد عدد التكرارات التي يقوم بها كل مؤشر ترابط بواسطة الصيغة التالية:

Number_of_iterations_executed بواسطة مؤشر ترابط = الحد الأقصى (number_of_unallocated_iterations/omp_get_num_threads()، عدد التكرارات)

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

من خلال تحديد التوجيه #pragma omp للجدول الزمني (ديناميكي، 15)، يمكن تنفيذ حلقة for مكونة من 100 تكرار بواسطة أربعة سلاسل رسائل كما يلي:

الموضوع 0 مصرح له بتنفيذ التكرارات 1-15 الموضوع 1 مصرح له بتنفيذ التكرارات 16-30 الموضوع 2 مصرح له بتنفيذ التكرارات 31-45 الموضوع 3 مصرح له بتنفيذ التكرارات 46-60 الموضوع 2 يكمل التكرارات الموضوع 2 مصرح له بالتنفيذ من التكرارات 61-75 يكمل الموضوع 3 تنفيذ التكرارات يصبح الموضوع 3 مخولاً بتنفيذ التكرارات 76-90 يكمل الموضوع 0 تنفيذ التكرارات يصبح الموضوع 0 مخولاً بتنفيذ التكرارات 91-100

ولكن هذا ما يمكن أن تكون عليه نتيجة تنفيذ نفس الحلقة بأربعة سلاسل إذا تم تحديد التوجيه #pragma omp للجدول الزمني (المرشد، 15):

الموضوع 0 مصرح له بتنفيذ التكرارات 1-25 الموضوع 1 مصرح له بتنفيذ التكرارات 26-44 الموضوع 2 مصرح له بتنفيذ التكرارات 45-59 الموضوع 3 مصرح له بتنفيذ التكرارات 60-64 الموضوع 2 يكمل التكرارات الموضوع 2 مصرح له بالتنفيذ من التكرارات 65-79 يكمل الموضوع 3 تنفيذ التكرارات يصبح الموضوع 3 مخولاً بتنفيذ التكرارات 80-94 يكمل الموضوع 2 تنفيذ التكرارات يصبح الموضوع 2 مخولاً بتنفيذ التكرارات 95-100

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

النهج الأخير - جدولة وقت التشغيل - ليس حتى خوارزمية جدولة، ولكنه طريقة لاختيار إحدى الخوارزميات الثلاث الموصوفة ديناميكيًا. إذا تم تحديد معلمة وقت التشغيل في قسم الجدول، فإن وقت تشغيل OpenMP يستخدم خوارزمية الجدولة المحددة لحلقة for المحددة باستخدام متغير OMP_SCHEDULE. له التنسيق "النوع [، عدد التكرارات]"، على سبيل المثال:

اضبط OMP_SCHEDULE=ديناميكي،8

توفر جدولة وقت التشغيل بعض المرونة في اختيار نوع الجدولة، حيث تكون الجدولة الثابتة هي الافتراضية.

متى تستخدم برنامج OpenMP؟

إن معرفة متى تستخدم تقنية OpenMP لا تقل أهمية عن معرفة كيفية استخدامها. نأمل أن تساعدك نصيحتنا.

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

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

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

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

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

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

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

تعتبر توجيهات OpenMP pragma سهلة الاستخدام، ولكنها لا توفر معلومات تفصيلية عن الخطأ. إذا كنت تكتب تطبيقًا بالغ الأهمية ويحتاج إلى اكتشاف الأخطاء واستعادة التشغيل العادي بأمان، فمن المحتمل تجنب OpenMP (على الأقل في الوقت الحالي). على سبيل المثال، إذا لم يتمكن OpenMP من إنشاء سلاسل رسائل للمناطق المتوازية أو لقسم مهم، يصبح سلوك البرنامج غير محدد. في Visual C++ 2005، يستمر وقت تشغيل OpenMP في محاولة إكمال المهمة المطلوبة لفترة من الوقت ثم يتوقف. في الإصدارات المستقبلية من OpenMP، من بين أمور أخرى، سنقوم بتنفيذ آلية قياسية للإخطار بالأخطاء.

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

المخاطر التي يمكن أن تقع فيها عند استخدام OpenMP

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

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

لتتمكن من فتح كتلة منظمة، التعبير

#pragma المرجع<директива>[الفصل]

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

// #pragma omp موازي سيئ ( // خطأ في الترجمة) // جيد #pragma omp موازي ( // الكود)

قد يكون تصحيح أخطاء تطبيقات OpenMP في Visual Studio 2005 أمرًا صعبًا. على وجه الخصوص، ترتبط بعض المضايقات بالدخول و/أو الخروج من منطقة متوازية بالضغط على المفتاح F10/F11. وذلك لأن المترجم يقوم بإنشاء تعليمات برمجية إضافية لاستدعاء مجموعات وقت التشغيل ومؤشر الترابط. لا يعرف مصحح الأخطاء هذا الأمر، لذا فإن ما تراه قد يبدو غريبًا بالنسبة لك. نوصي بتعيين نقطة توقف في منطقة متوازية والضغط على F5 للوصول إليها. للخروج من منطقة متوازية، قم بتعيين نقطة توقف خارج تلك المنطقة واضغط على F5.

عندما تكون داخل منطقة متوازية، ستعرض نافذة سلاسل الرسائل الخاصة بمصحح الأخطاء معلومات حول سلاسل الرسائل التي تعمل في مجموعة سلاسل الرسائل. لن تتوافق معرفات هذه المواضيع مع سلاسل عمليات OpenMP، ولكن مع سلاسل عمليات Windows الأساسية.

حاليًا، لا يمكنك استخدام التحسين الموجه لملف التعريف (PGO) مع OpenMP. لحسن الحظ، تعتمد تقنية OpenMP على توجيهات براغما، لذا يمكنك تجميع تطبيقك باستخدام خيار /openmp ومع PGO ومعرفة النهج الأكثر كفاءة.

برنامج OpenMP و.NET

قليل من الناس يربطون بين الحوسبة عالية الأداء و.NET، لكن Visual C++ 2005 يعمل على تحسين هذا الوضع. تجدر الإشارة بشكل خاص إلى أننا جعلنا OpenMP يعمل مع كود C++ المُدار. لتحقيق ذلك، جعلنا /openmp متوافقًا مع /clr و/clr:OldSyntax. أي أنه يمكنك استخدام OpenMP لتنفيذ أساليب أنواع .NET بالتوازي والتي تخضع لجمع البيانات المهملة. يرجى ملاحظة أن /openmp غير متوافق حاليًا مع /clr:safe أو /clr:pure، ولكننا نخطط لإصلاح ذلك.

يجب أن نذكر أحد القيود المهمة المرتبطة باستخدام OpenMP في التعليمات البرمجية المُدارة. يجب استخدام التطبيق الذي يستخدم OpenMP في مجال تطبيق واحد فقط. عند تحميل AppDomain آخر في عملية تم بالفعل تحميل وقت تشغيل OpenMP فيها، قد يتعطل التطبيق.

OpenMP عبارة عن تقنية موازية للتطبيقات بسيطة ولكنها قوية. يسمح لك بتنفيذ التنفيذ المتوازي لكل من الحلقات والكتل الوظيفية للتعليمات البرمجية. يمكن دمجه بسهولة في التطبيقات الموجودة ويمكن تمكينه/تعطيله باستخدام خيار مترجم واحد. يتيح لك OpenMP الاستفادة الكاملة من قوة الحوسبة للمعالجات متعددة النواة. ننصحك بشدة بالتعرف على مواصفات OpenMP. حظا سعيدا في تطوير برامج متعددة الخيوط!