مرحبا يا هبر! أوجه انتباهكم إلى ترجمة المقال
"أعلى 20 C ++ أخطاء تعدد العمليات وكيفية تجنبها" بقلم Deb Haldar.
مشهد من فيلم "The Loop of Time" (2012)تعد ميزة "تعدد مؤشرات الترابط" واحدة من أصعب المجالات في البرمجة ، خاصة في C ++. على مر سنوات من التطور ، ارتكبت الكثير من الأخطاء. لحسن الحظ ، تم التعرف على معظمهم من خلال رمز المراجعة والاختبار. ومع ذلك ، فقد تراجع البعض بطريقة ما عن المنتج ، وكان علينا تعديل أنظمة التشغيل ، وهي مكلفة دائمًا.
في هذه المقالة ، حاولت تصنيف جميع الأخطاء التي أعرفها باستخدام الحلول الممكنة. إذا كنت على دراية بأي عيوب أخرى ، أو كانت لديك اقتراحات لحل الأخطاء الموضحة ، فيرجى ترك تعليقاتك تحت المقال.
الخطأ رقم 1: لا تستخدم join () لانتظار مؤشرات ترابط الخلفية قبل الخروج من التطبيق
إذا نسيت إرفاق الدفق (
join () ) أو
فصله (
detach () ) (جعله غير قابل للانضمام) قبل انتهاء البرنامج ، فسيؤدي ذلك إلى تعطله. (ستحتوي الترجمة على الكلمات join في سياق
join () والانفصال في سياق
detach () ، على الرغم من أن هذا ليس صحيحًا تمامًا. في الحقيقة ،
join () هي النقطة التي ينتظر عندها مؤشر ترابط تنفيذ واحد إكمال آخر ، ولا يحدث أي ربط أو دمج مؤشرات الترابط [تعليق المترجم]).
في المثال أدناه ، لقد نسينا تنفيذ
join () لمؤشر الترابط t1 في السلسلة الرئيسية:
#include "stdafx.h"
#include <iostream>
#include <thread>
using namespace std ;
void LaunchRocket ( )
{
cout << "Launching Rocket" << endl ;
}
int main ( )
{
thread t1 ( LaunchRocket ) ;
//t1.join(); // join-
return 0 ;
}
لماذا تعطل البرنامج؟! لأنه في نهاية الدالة
main () ، خرج المتغير t1 خارج النطاق وتم استدعاء destructor مؤشر الترابط. يتحقق destructor مما إذا كان مؤشر الترابط t1
قابلًا للانضمام . يمكن
ربط سلسلة
الرسائل إذا لم يتم فصلها. في هذه الحالة ، يتم استدعاء
std :: terminate في destructor الخاص به. إليك ما يفعله برنامج التحويل البرمجي MSVC ++ ، على سبيل المثال.
~thread ( ) _NOEXCEPT
{ // clean up
if ( joinable ( ) )
XSTD terminate ( ) ;
}
هناك طريقتان لإصلاح المشكلة ، حسب المهمة:
1. دعوة
الانضمام () من موضوع T1 في الموضوع الرئيسي:
int main ( )
{
thread t1 ( LaunchRocket ) ;
t1. join ( ) ; // join t1,
return 0 ;
}
2. افصل التيار t1 عن التيار الرئيسي ، واسمح له بمواصلة العمل كتيار "شيطاني":
int main ( )
{
thread t1 ( LaunchRocket ) ;
t1. detach ( ) ; // t1
return 0 ;
}
الخطأ رقم 2: محاولة إرفاق خيط تم فصله مسبقًا
إذا كان لديك في مرحلة ما من عمل البرنامج تيار
فصل ، فلا يمكنك إرفاقه مرة أخرى بالدفق الرئيسي. هذا خطأ واضح جدا. المشكلة هي أنه يمكنك إلغاء تثبيت الدفق ، ثم كتابة بضع مئات من سطور التعليمات البرمجية ومحاولة إعادة تثبيتها. بعد كل شيء ، من يتذكر أنه كتب 300 سطر إلى الوراء ، أليس كذلك؟
المشكلة هي أن هذا لن يتسبب في حدوث خطأ في التحويل البرمجي ، بدلاً من ذلك سيتعطل البرنامج عند بدء التشغيل. على سبيل المثال:
#include "stdafx.h"
#include <iostream>
#include <thread>
using namespace std ;
void LaunchRocket ( )
{
cout << "Launching Rocket" << endl ;
}
int main ( )
{
thread t1 ( LaunchRocket ) ;
t1. detach ( ) ;
//..... 100 -
t1. join ( ) ; // CRASH !!!
return 0 ;
}
يكمن الحل دائمًا في التحقق من مؤشر الترابط من أجل
joinable () قبل محاولة إرفاقه في مؤشر الترابط الاستدعاء.
int main ( )
{
thread t1 ( LaunchRocket ) ;
t1. detach ( ) ;
//..... 100 -
if ( t1. joinable ( ) )
{
t1. join ( ) ;
}
return 0 ;
}
خطأ # 3: سوء فهم هذا std :: thread :: join () بحظر مؤشر الترابط استدعاء التنفيذ
في التطبيقات الحقيقية ، تحتاج غالبًا إلى فصل عمليات "التشغيل الطويل" الخاصة بمعالجة I / O للشبكة أو انتظار قيام المستخدم بالنقر فوق زر ، إلخ. استدعاء
للانضمام () لسير العمل هذا (على سبيل المثال ، مؤشر ترابط تقديم واجهة المستخدم) قد يتسبب في توقف واجهة المستخدم. هناك طرق تنفيذ أكثر ملاءمة.
على سبيل المثال ، في تطبيقات واجهة المستخدم الرسومية ، قد يقوم مؤشر ترابط العامل ، عند الانتهاء ، بإرسال رسالة إلى مؤشر ترابط واجهة المستخدم. يحتوي دفق واجهة المستخدم على حلقة معالجة الأحداث الخاصة به مثل: تحريك الماوس وضغط المفاتيح وما إلى ذلك. يمكن أن تتلقى هذه الحلقة أيضًا رسائل من مؤشرات ترابط العامل والرد عليها دون الحاجة إلى استدعاء أسلوب حظر
() الصلة .
لهذا السبب بالذات ، أصبحت جميع تفاعلات المستخدم تقريبًا في النظام الأساسي
WinRT من Microsoft غير متزامنة ، ولا تتوفر بدائل متزامنة. تم اتخاذ هذه القرارات لضمان استخدام المطورين لواجهة برمجة التطبيقات التي توفر أفضل تجربة للمستخدم النهائي. يمكنك الرجوع إلى دليل "
Modern C ++ و Windows Store Apps " للحصول على مزيد من المعلومات حول هذا الموضوع.
الخطأ رقم 4: بافتراض أن وسائط دالة الدفق يتم تمريرها بالرجوع بشكل افتراضي
يتم تمرير الوسائط إلى دالة الدفق حسب القيمة افتراضيًا. إذا كنت بحاجة إلى إجراء تغييرات على الوسائط التي تم تمريرها ، فيجب عليك تمريرها بالرجوع إليها باستخدام دالة
std :: ref () .
تحت المفسد ، أمثلة من
مقالة أخرى لـ
C ++ 11 Multithreading Tutorial عبر Q&A - أساسيات إدارة الخيط (Deb Haldar) ، موضحة المعلمة تمرير [تقريبا. مترجم].
مزيد من التفاصيل:عند تنفيذ الكود:
#include "stdafx.h"
#include <string>
#include <thread>
#include <iostream>
#include <functional>
using namespace std ;
void ChangeCurrentMissileTarget ( string & targetCity )
{
targetCity = "Metropolis" ;
cout << " Changing The Target City To " << targetCity << endl ;
}
int main ( )
{
string targetCity = "Star City" ;
thread t1 ( ChangeCurrentMissileTarget, targetCity ) ;
t1. join ( ) ;
cout << "Current Target City is " << targetCity << endl ;
return 0 ;
}
سيتم عرضه في المحطة:
Changing The Target City To Metropolis
Current Target City is Star City
كما ترون ، لم تتغير قيمة المتغير targetCity الذي تم استلامه بواسطة الدالة التي تم استدعاؤها في الدفق حسب المرجع.
أعد كتابة التعليمات البرمجية باستخدام std :: ref () لتمرير الوسيطة:
#include "stdafx.h"
#include <string>
#include <thread>
#include <iostream>
#include <functional>
using namespace std ;
void ChangeCurrentMissileTarget ( string & targetCity )
{
targetCity = "Metropolis" ;
cout << " Changing The Target City To " << targetCity << endl ;
}
int main ( )
{
string targetCity = "Star City" ;
thread t1 ( ChangeCurrentMissileTarget, std :: ref ( targetCity ) ) ;
t1. join ( ) ;
cout << "Current Target City is " << targetCity << endl ;
return 0 ;
}
سوف يخرج:
Changing The Target City To Metropolis
Current Target City is Metropolis
ستؤثر التغييرات التي تم إجراؤها في مؤشر الترابط الجديد على قيمة المتغير targetCity الذي تم إعلانه وتهيئته في الوظيفة الرئيسية .
الخطأ رقم 5: لا تحمي البيانات والموارد المشتركة من خلال قسم مهم (على سبيل المثال ، كائن مزامنة)
في بيئة متعددة الخيوط ، يتنافس أكثر من مؤشر ترابط واحد عادة على الموارد والبيانات المشتركة. غالبًا ما يؤدي ذلك إلى حالة غير مؤكدة بالنسبة للموارد والبيانات ، باستثناء عندما يكون الوصول إليها محميًا بواسطة بعض الآليات التي تسمح فقط لخيط تنفيذ واحد بتنفيذ العمليات عليها في أي وقت.
في المثال أدناه ، يعد
std :: cout موردًا مشتركًا يعمل 6 مؤشرات ترابط معه (t1-t5 + main).
#include "stdafx.h"
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
using namespace std ;
std :: mutex mu ;
void CallHome ( string message )
{
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
}
int main ( )
{
thread t1 ( CallHome, "Hello from Jupiter" ) ;
thread t2 ( CallHome, "Hello from Pluto" ) ;
thread t3 ( CallHome, "Hello from Moon" ) ;
CallHome ( "Hello from Main/Earth" ) ;
thread t4 ( CallHome, "Hello from Uranus" ) ;
thread t5 ( CallHome, "Hello from Neptune" ) ;
t1. join ( ) ;
t2. join ( ) ;
t3. join ( ) ;
t4. join ( ) ;
t5. join ( ) ;
return 0 ;
}
إذا قمنا بتنفيذ هذا البرنامج ، فسوف نحصل على الاستنتاج:
Thread 0x1000fb5c0 says Hello from Main/Earth
Thread Thread Thread 0x700005bd20000x700005b4f000 says says Thread Thread Hello from Pluto0x700005c55000Hello from Jupiter says 0x700005d5b000Hello from Moon
0x700005cd8000 says says Hello from Uranus
Hello from Neptune
وذلك لأن خمسة مؤشرات ترابط في وقت واحد الوصول إلى دفق الإخراج في ترتيب عشوائي. لجعل الاستنتاج أكثر تحديدًا ، يجب حماية الوصول إلى المورد المشترك باستخدام
std :: mutex . فقط قم بتغيير وظيفة
CallHome () بحيث تلتقط كائن المزامنة (mutex) قبل استخدام
std :: cout وتحررها بعد ذلك.
void CallHome ( string message )
{
mu. lock ( ) ;
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
mu. unlock ( ) ;
}
الخطأ رقم 6: ننسى إطلاق القفل بعد الخروج من القسم الحرج
في الفقرة السابقة ، رأيت كيف تحمي قسمًا حرجًا باستخدام كائن مزامنة. ومع ذلك ، فإن استدعاء أساليب
lock () و
unlock () مباشرة على mutex ليس الخيار المفضل لأنك قد تنسى إعطاء القفل المعلق. ماذا سيحدث بعد ذلك؟ سيتم حظر كافة مؤشرات الترابط الأخرى التي تنتظر إصدار المورد بشكل غير محدود وقد يتعطل البرنامج.
في مثالنا التخليقي ، إذا نسيت إلغاء قفل كائن المزامنة في استدعاء دالة
CallHome () ، فسيتم إخراج الرسالة الأولى من الدفق t1 إلى الدفق القياسي وسيتم تعطل البرنامج. ويرجع ذلك إلى حقيقة أن مؤشر الترابط t1 تلقى قفل مزامنة ، وتنتظر مؤشرات الترابط المتبقية إصدار هذا القفل.
void CallHome ( string message )
{
mu. lock ( ) ;
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
//mu.unlock();
}
ما يلي هو إخراج هذا الرمز - تعطل البرنامج ، وعرض الرسالة الوحيدة في الجهاز ، ولم ينته:
Thread 0x700005986000 says Hello from Pluto
تحدث مثل هذه الأخطاء غالبًا ، وهذا هو السبب في أنه من غير المرغوب فيه استخدام أساليب
lock () / unlock () مباشرةً من كائن المزامنة. بدلاً من ذلك ، استخدم فئة القالب
std :: lock_guard ، والتي تستخدم لغة
RAII للتحكم في عمر القفل. عندما يتم إنشاء كائن
lock_guard ، فإنه يحاول السيطرة على كائن المزامنة. عندما يترك البرنامج نطاق كائن
lock_guard ، يتم استدعاء المدمر الذي يحرر كائن المزامنة.
نعيد كتابة وظيفة
CallHome () باستخدام كائن
std :: lock_guard :
void CallHome ( string message )
{
std :: lock_guard < std :: mutex > lock ( mu ) ; //
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
} // lock_guard
الخطأ رقم 7: اجعل حجم القسم الحرج أكبر من اللازم
عندما يتم تنفيذ مؤشر ترابط واحد داخل قسم حرج ، يتم حظر جميع الآخرين الذين يحاولون الدخول إليه. يجب أن نحتفظ بأكبر عدد ممكن من الإرشادات في القسم المهم. لتوضيح ذلك ، يتم تقديم مثال على الكود السيئ مع قسم حرج كبير:
void CallHome ( string message )
{
std :: lock_guard < std :: mutex > lock ( mu ) ; // , std::cout
ReadFifyThousandRecords ( ) ;
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
} // lock_guard mu
أسلوب
ReadFifyThousandRecords () لا يعدل البيانات. لا يوجد سبب لتنفيذه تحت القفل. إذا تم تنفيذ هذه الطريقة لمدة 10 ثوانٍ ، وقراءة 50 ألف صف من قاعدة البيانات ، فسيتم حظر جميع سلاسل الرسائل الأخرى لهذه الفترة بأكملها دون داع. هذا يمكن أن يؤثر تأثيرا خطيرا على أداء البرنامج.
سيكون الحل الصحيح هو الإبقاء على القسم الحرج يعمل فقط مع
الأمراض المنقولة جنسياً :: cout .
void CallHome ( string message )
{
ReadFifyThousandRecords ( ) ; // ..
std :: lock_guard < std :: mutex > lock ( mu ) ; // , std::cout
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
} // lock_guard mu
الخطأ رقم 8: أخذ أقفال متعددة بترتيب مختلف
يعد هذا أحد أكثر أسباب
الجمود شيوعًا ، وهو الموقف الذي يتم فيه حظر مؤشرات الترابط بلا حدود بسبب انتظار الوصول إلى الموارد المحظورة بواسطة مؤشرات الترابط الأخرى. النظر في مثال:
تيار 1 | تيار 2 |
---|
قفل أ | قفل ب |
// ... بعض العمليات | // ... بعض العمليات |
قفل ب | قفل أ |
// ... بعض العمليات الأخرى | // ... بعض العمليات الأخرى |
فتح ب | فتح أ |
فتح أ | فتح ب |
قد ينشأ موقف يحاول فيه مؤشر الترابط 1 التقاط القفل B ويتم حظره لأن مؤشر الترابط 2 قام بالفعل بالتقاطه. في الوقت نفسه ، يحاول مؤشر الترابط الثاني التقاط القفل A ، لكن لا يمكنه القيام بذلك ، لأنه تم التقاطه بواسطة الخيط الأول. لا يمكن تحرير مؤشر ترابط 1 قفل A حتى يتم تأمين B ، إلخ. بمعنى آخر ، يتجمد البرنامج.
مثال التعليمات البرمجية هذا سيساعدك على إعادة إنشاء حالة
توقف تام :
#include "stdafx.h"
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
using namespace std ;
std :: mutex muA ;
std :: mutex muB ;
void CallHome_Th1 ( string message )
{
muA. lock ( ) ;
// -
std :: this_thread :: sleep_for ( std :: chrono :: milliseconds ( 100 ) ) ;
muB. lock ( ) ;
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
muB. unlock ( ) ;
muA. unlock ( ) ;
}
void CallHome_Th2 ( string message )
{
muB. lock ( ) ;
// -
std :: this_thread :: sleep_for ( std :: chrono :: milliseconds ( 100 ) ) ;
muA. lock ( ) ;
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
muA. unlock ( ) ;
muB. unlock ( ) ;
}
int main ( )
{
thread t1 ( CallHome_Th1, "Hello from Jupiter" ) ;
thread t2 ( CallHome_Th2, "Hello from Pluto" ) ;
t1. join ( ) ;
t2. join ( ) ;
return 0 ;
}
إذا قمت بتشغيل هذا الرمز ، فسوف يتعطل. إذا ذهبت أعمق في مصحح الأخطاء في إطار مؤشر الترابط ، سترى أن مؤشر الترابط الأول (يسمى من
CallHome_Th1 () ) يحاول الحصول على تأمين على mutex B ، بينما يحاول مؤشر الترابط 2 (يسمى من
CallHome_Th2 () ) حظر mutex A. لا شيء من مؤشرات الترابط لا يمكن أن تنجح ، الأمر الذي يؤدي إلى طريق مسدود!

(الصورة قابلة للنقر)
ماذا يمكنك أن تفعل حيال ذلك؟ أفضل حل هو إعادة هيكلة الكود بحيث تحدث أقفال القفل بنفس الترتيب في كل مرة.
اعتمادًا على الموقف ، يمكنك استخدام استراتيجيات أخرى:
1. استخدم
فئة المجمع
std :: scoped_lock لالتقاط أقفال متعددة بشكل مشترك:
std :: scoped_lock lock { muA, muB } ;
2. استخدم فئة
std :: timed_mutex ، والتي يمكنك من خلالها تحديد مهلة ، وبعد ذلك سيتم إصدار القفل في حالة عدم توفر المورد.
std :: timed_mutex m ;
void DoSome ( ) {
std :: chrono :: milliseconds timeout ( 100 ) ;
while ( true ) {
if ( m. try_lock_for ( timeout ) ) {
std :: cout << std :: this_thread :: get_id ( ) << ": acquire mutex successfully" << std :: endl ;
m. unlock ( ) ;
} else {
std :: cout << std :: this_thread :: get_id ( ) << ": can't acquire mutex, do something else" << std :: endl ;
}
}
}
خطأ # 9: محاولة الاستيلاء على الأمراض المنقولة جنسيا :: قفل mutex مرتين
محاولة قفل القفل مرتين سيؤدي إلى سلوك غير محدد. في معظم تطبيقات تصحيح الأخطاء ، سيتعطل هذا. على سبيل المثال ، في التعليمة البرمجية أدناه ، يقوم
LaunchRocket () بتأمين كائن المزامنة (mutex) ثم استدعاء
StartThruster () . ما يثير الفضول هو أنك لن تواجه هذه المشكلة في التعليمات البرمجية أعلاه أثناء التشغيل العادي للبرنامج ، فالمشكلة تحدث فقط عند طرح استثناء ، ويكون مصحوبًا بسلوك غير محدد أو عند إنهاء البرنامج بشكل غير طبيعي.
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <mutex>
std :: mutex mu ;
static int counter = 0 ;
void StartThruster ( )
{
try
{
// -
}
catch ( ... )
{
std :: lock_guard < std :: mutex > lock ( mu ) ;
std :: cout << "Launching rocket" << std :: endl ;
}
}
void LaunchRocket ( )
{
std :: lock_guard < std :: mutex > lock ( mu ) ;
counter ++ ;
StartThruster ( ) ;
}
int main ( )
{
std :: thread t1 ( LaunchRocket ) ;
t1. join ( ) ;
return 0 ;
}
لحل هذه المشكلة ، يجب عليك تصحيح التعليمات البرمجية بطريقة لمنع إعادة استرداد الأقفال التي تم تلقيها مسبقًا. يمكنك استخدام
std :: recursive_mutex كحل عكاز ، لكن مثل هذا الحل يشير دائمًا إلى بنية سيئة للبرنامج.
الخطأ رقم 10: استخدم أدوات المزامنة عندما تكفي أنواع الذرات المنقولة جنسياً ::
عندما تحتاج إلى تغيير أنواع البيانات البسيطة ، على سبيل المثال ، قيمة منطقية أو عداد صحيح ، فإن استخدام
std: atomic عادةً ما يوفر أداء أفضل من استخدام mutexes.
على سبيل المثال ، بدلاً من استخدام البنية التالية:
int counter ;
...
mu. lock ( ) ;
counter ++ ;
mu. unlock ( ) ;
من الأفضل أن تعلن عن متغير كـ
std :: atomic :
std :: atomic < int > counter ;
...
counter ++ ;
للحصول على مقارنة تفصيلية بين mutex و atomic ، راجع
المقارنة: البرمجة Lockless مع الذرات في C ++ 11 مقابل. مزامنة وأقفال RW »الخطأ رقم 11: قم بإنشاء وتدمير عدد كبير من مؤشرات الترابط مباشرةً ، بدلاً من استخدام مجموعة من مؤشرات الترابط المجانية
يعد إنشاء مؤشرات الترابط وتدميرها عملية مكلفة من حيث وقت المعالج. تخيل محاولة لإنشاء دفق أثناء قيام النظام بإجراء عمليات حسابية مكثفة ، على سبيل المثال ، تقديم الرسومات أو فيزياء ألعاب الحوسبة. تتمثل الطريقة المستخدمة غالبًا في مثل هذه المهام في إنشاء مجموعة من مؤشرات الترابط المخصصة مسبقًا والتي يمكنها التعامل مع المهام الروتينية ، مثل الكتابة على القرص أو إرسال البيانات عبر الشبكة طوال دورة حياة العملية.
ميزة أخرى من تجمع مؤشرات الترابط مقارنةً بإنتاج مؤشرات الترابط وتدميرها بنفسك هي أنك لا داعي للقلق بشأن
زيادة عدد سلاسل
الرسائل (وهي حالة يتجاوز فيها عدد مؤشرات الترابط عدد النوى المتاحة ويقضي جزء كبير من وقت المعالج في تبديل السياقات [تقريبًا. مترجم]). قد يؤثر هذا على أداء النظام.
بالإضافة إلى ذلك ، فإن استخدام المسبح ينقذنا من آلام إدارة دورة حياة الخيوط ، والتي تترجم في النهاية إلى كود أكثر إحكاما مع وجود أخطاء أقل.
المكتبات الأكثر شيوعًا التي تقوم بتطبيق تجمع مؤشرات الترابط هي
كتل بناء خيط Intel (TBB) ومكتبة Microsoft Parallel Patterns Library (PPL) .
الخطأ رقم 12: لا تعالج الاستثناءات التي تحدث في مؤشرات ترابط الخلفية
لا يمكن معالجة الاستثناءات التي تم طرحها في مؤشر ترابط واحد في مؤشر ترابط آخر. دعنا نتخيل أن لدينا وظيفة تطرح استثناءً. إذا قمنا بتنفيذ هذه الوظيفة في مؤشر ترابط منفصل متفرع من مؤشر الترابط الرئيسي للتنفيذ ، ونتوقع أن نحصل على أي استثناء يتم طرحه من مؤشر الترابط الإضافي ، فلن يعمل هذا. النظر في مثال:
#include "stdafx.h"
#include<iostream>
#include<thread>
#include<exception>
#include<stdexcept>
static std :: exception_ptr teptr = nullptr ;
void LaunchRocket ( )
{
throw std :: runtime_error ( "Catch me in MAIN" ) ;
}
int main ( )
{
try
{
std :: thread t1 ( LaunchRocket ) ;
t1. join ( ) ;
}
catch ( const std :: exception & ex )
{
std :: cout << "Thread exited with exception: " << ex. what ( ) << " \n " ;
}
return 0 ;
}
عند تنفيذ هذا البرنامج ، سيتم تعطله ، ومع ذلك ، لن يتم تنفيذ كتلة catch في الدالة main () ولن تتعامل مع الاستثناء الذي تم طرحه في مؤشر الترابط t1.
الحل لهذه المشكلة هو استخدام الميزات من C ++ 11: يتم استخدام
std :: استثناء_ptr لمعالجة الاستثناء طرح في مؤشر ترابط الخلفية. فيما يلي الخطوات التي تحتاج إلى اتخاذها:
- إنشاء مثيل عمومي للفئة std :: استثناء_النظام التهيئة إلى nullptr
- داخل دالة يتم تشغيلها في مؤشر ترابط منفصل ، تعامل مع جميع الاستثناءات وقم بتعيين القيمة std :: current_exception () للمتغير العالمي std :: استثناء_ptr المعلن في الخطوة السابقة
- تحقق من قيمة المتغير العام داخل الخيط الرئيسي
- إذا تم تعيين القيمة ، فاستخدم الدالة std :: rethrow_exception ((استثناء) p) للاتصال بشكل متكرر بالاستثناء الذي تم اكتشافه مسبقًا ، وتمريره بالرجوع كمرجع
لا يتم استدعاء الاستثناء حسب المرجع في الخيط الذي تم إنشاؤه فيه ، لذلك تعد هذه الميزة رائعة للتعامل مع الاستثناءات في مؤشرات الترابط المختلفة.
في الكود أدناه ، يمكنك التعامل بأمان مع الاستثناء في سلسلة الخلفية.
#include "stdafx.h"
#include<iostream>
#include<thread>
#include<exception>
#include<stdexcept>
static std :: exception_ptr globalExceptionPtr = nullptr ;
void LaunchRocket ( )
{
try
{
std :: this_thread :: sleep_for ( std :: chrono :: milliseconds ( 100 ) ) ;
throw std :: runtime_error ( "Catch me in MAIN" ) ;
}
catch ( ... )
{
//
globalExceptionPtr = std :: current_exception ( ) ;
}
}
int main ( )
{
std :: thread t1 ( LaunchRocket ) ;
t1. join ( ) ;
if ( globalExceptionPtr )
{
try
{
std :: rethrow_exception ( globalExceptionPtr ) ;
}
catch ( const std :: exception & ex )
{
std :: cout << "Thread exited with exception: " << ex. what ( ) << " \n " ;
}
}
return 0 ;
}
الخطأ رقم 13: استخدم مؤشرات الترابط لمحاكاة العملية غير المتزامنة ، بدلاً من استخدام std :: async
إذا كنت بحاجة إلى تنفيذ التعليمات البرمجية بشكل غير متزامن ، أي بدون حظر مؤشر الترابط الرئيسي للتنفيذ ، سيكون الخيار الأفضل هو استخدام
std :: async () . هذا يكافئ إنشاء دفق وتمرير التعليمات البرمجية اللازمة للتنفيذ في هذا الدفق من خلال مؤشر إلى دالة أو معلمة في شكل دالة lambda. ومع ذلك ، في الحالة الأخيرة ، تحتاج إلى مراقبة إنشاء هذا المرفق ومرفقه / فصله ، وكذلك معالجة جميع الاستثناءات التي قد تحدث في هذا الموضوع. إذا كنت تستخدم
std :: async () ، فإنك تخفف نفسك من هذه المشاكل وتقلل أيضًا بشكل كبير من فرصك في الوصول إلى
طريق مسدود .
ميزة أخرى هامة لاستخدام
std :: async هي القدرة على الحصول على نتيجة عملية غير متزامنة مرة أخرى إلى مؤشر الترابط الاستدعاء باستخدام الكائن
std :: future . تخيل أن لدينا
دالة ConjureMagic () تقوم بإرجاع int. يمكننا بدء عملية غير متزامنة ، والتي ستحدد القيمة في المستقبل إلى الكائن
المستقبلي ، عند اكتمال المهمة ، ويمكننا استخراج نتيجة التنفيذ من هذا الكائن في تدفق التنفيذ الذي تم استدعاء العملية منه.
// future
std :: future asyncResult2 = std :: async ( & ConjureMagic ) ;
//... - future
// future
int v = asyncResult2. get ( ) ;
الحصول على النتيجة مرة أخرى من مؤشر الترابط قيد التشغيل إلى المتصل أكثر تعقيدًا. طريقتان ممكنتان:
- تمرير إشارة إلى متغير الإخراج إلى الدفق الذي سيؤدي إلى حفظ النتيجة.
- قم بتخزين النتيجة في متغير الحقل الخاص بكائن سير العمل ، والذي يمكن قراءته بمجرد اكتمال سلسلة عمليات التنفيذ.
وجد
Kurt Guntheroth أنه فيما يتعلق بالأداء ، فإن النفقات العامة لإنشاء دفق تبلغ 14 ضعف استخدام الدفق غير
المتزامن .
خلاصة القول: استخدم
std :: async () افتراضيًا حتى تجد وسيطات قوية لصالح استخدام
std :: thread مباشرة.
الخطأ رقم 14: لا تستخدم std :: launch :: async إذا كانت المزامنة مطلوبة
دالة
std :: async () ليست الاسم الصحيح تمامًا ، لأنه قد لا يعمل بشكل افتراضي بشكل غير متزامن!
هناك سياسات وقت تشغيل
std :: async :
- الأمراض المنقولة جنسيا :: launch :: async : تبدأ الوظيفة التي تم تمريرها في التنفيذ مباشرة في سلسلة رسائل منفصلة
- std :: launch :: مؤجل : لا يتم تشغيل الوظيفة التي تم تمريرها على الفور ، يتم تأخير تشغيلها قبل إجراء مكالمات get () أو wait () على كائن std :: future ، والتي سيتم إرجاعها من استدعاء std :: async . في مكان استدعاء هذه الأساليب ، سيتم تنفيذ الوظيفة بشكل متزامن.
عندما نسمي
std :: async () بالمعلمات الافتراضية ، يبدأ بمزيج من هذه المعلمتين ، مما يؤدي في الواقع إلى سلوك غير متوقع. هناك عدد من الصعوبات الأخرى المرتبطة باستخدام
std: async () مع سياسة بدء التشغيل الافتراضية:
- عدم القدرة على التنبؤ بالوصول الصحيح إلى متغيرات التدفق المحلي
- قد لا تبدأ مهمة غير متزامنة على الإطلاق بسبب حقيقة أن المكالمات إلى get () وطرق الانتظار () قد لا يتم استدعاءها أثناء تنفيذ البرنامج
- عند استخدامها في الحلقات التي يتوقع فيها شرط الخروج أن يكون الكائن std :: future جاهزًا ، قد لا تنتهي هذه الحلقات أبدًا ، لأن std :: future التي يتم إرجاعها بواسطة استدعاء std :: async قد تبدأ في الحالة المؤجلة.
لتجنب كل هذه الصعوبات ، قم
دائمًا باستدعاء
std :: async باستخدام سياسة
إطلاق std :: launch :: async .
لا تفعل هذا:
// myFunction std::async
auto myFuture = std :: async ( myFunction ) ;
بدلاً من ذلك ، افعل هذا:
// myFunction
auto myFuture = std :: async ( std :: launch :: async , myFunction ) ;
تعتبر هذه النقطة بمزيد من التفصيل في كتاب سكوت مايرز "الفعال والحديث C ++".
الخطأ رقم 15: استدعاء طريقة get () لكائن std :: future في كتلة تعليمات برمجية يكون وقت تنفيذها بالغ الأهمية
يعالج الرمز أدناه النتيجة التي تم الحصول عليها من الكائن
std :: future لعملية غير متزامنة. ومع ذلك ، سيتم تأمين
حلقة الوقت حتى تكتمل العملية غير المتزامنة (في هذه الحالة ، لمدة 10 ثوانٍ). إذا كنت تريد استخدام هذه الحلقة لعرض المعلومات على الشاشة ، فقد يؤدي ذلك إلى تأخيرات غير سارة في تقديم واجهة المستخدم.
#include "stdafx.h"
#include <future>
#include <iostream>
int main ( )
{
std :: future < int > myFuture = std :: async ( std :: launch :: async , [ ] ( )
{
std :: this_thread :: sleep_for ( std :: chrono :: seconds ( 10 ) ) ;
return 8 ;
} ) ;
//
while ( true )
{
//
std :: cout << "Rendering Data" << std :: endl ;
int val = myFuture. get ( ) ; // 10
// - Val
}
return 0 ;
}
ملاحظة : هناك مشكلة أخرى في التعليمة البرمجية أعلاه وهي محاولة الوصول إلى الكائن
std :: future للمرة الثانية ، على الرغم من أنه تم استرداد حالة الكائن
std :: future عند التكرار الأول للحلقة ولا يمكن استعادته.
سيكون الحل الصحيح هو التحقق من صحة الكائن
std :: future قبل استدعاء الأسلوب
get () . وبالتالي ، فإننا لا نمنع إتمام المهمة غير المتزامنة ولا نحاول استجواب الكائن
std :: future المستخرج بالفعل.
يتيح لك مقتطف الشفرة هذا تحقيق ذلك:
#include "stdafx.h"
#include <future>
#include <iostream>
int main ( )
{
std :: future < int > myFuture = std :: async ( std :: launch :: async , [ ] ( )
{
std :: this_thread :: sleep_for ( std :: chrono :: seconds ( 10 ) ) ;
return 8 ;
} ) ;
//
while ( true )
{
//
std :: cout << "Rendering Data" << std :: endl ;
if ( myFuture. valid ( ) )
{
int val = myFuture. get ( ) ; // 10
// - Val
}
}
return 0 ;
}
№16: , , , std::future::get()
تخيل أن لدينا جزء التعليمات البرمجية التالي ، ما رأيك سيكون نتيجة استدعاء std :: future :: get () ؟ إذا كنت تفترض أن البرنامج سوف يتعطل - أنت على حق تماما! يتم طرح الاستثناء في العملية غير المتزامنة فقط عندما يتم استدعاء الأسلوب get () على الكائن std :: future . وإذا لم يتم استدعاء طريقة get () ، فسيتم تجاهل الاستثناء وإلقاءه عندما يخرج الكائن std :: future عن نطاقه. إذا كانت العملية غير المتزامنة قد تؤدي إلى استثناء ، فيجب عليك دائمًا إنهاء المكالمة إلى std :: future :: get () في كتلة try / catch . مثال على كيفية ظهور هذا:#include "stdafx.h"
#include <future>
#include <iostream>
int main ( )
{
std :: future < int > myFuture = std :: async ( std :: launch :: async , [ ] ( )
{
throw std :: runtime_error ( "Catch me in MAIN" ) ;
return 8 ;
} ) ;
if ( myFuture. valid ( ) )
{
int result = myFuture. get ( ) ;
}
return 0 ;
}
#include "stdafx.h"
#include <future>
#include <iostream>
int main ( )
{
std :: future < int > myFuture = std :: async ( std :: launch :: async , [ ] ( )
{
throw std :: runtime_error ( "Catch me in MAIN" ) ;
return 8 ;
} ) ;
if ( myFuture. valid ( ) )
{
try
{
int result = myFuture. get ( ) ;
}
catch ( const std :: runtime_error & e )
{
std :: cout << "Async task threw exception: " << e. what ( ) << std :: endl ;
}
}
return 0 ;
}
№17: std::async,
على الرغم من أن std :: async () كافية في معظم الحالات ، إلا أن هناك حالات قد تحتاج فيها إلى تحكم دقيق في تنفيذ التعليمات البرمجية في دفق. على سبيل المثال ، إذا كنت ترغب في ربط مؤشر ترابط محدد بنواة معالج معينة في نظام متعدد المعالجات (على سبيل المثال ، Xbox).يحدد جزء الشفرة المحدد ربط الخيط بنواة المعالج الخامسة في النظام. هذا ممكن بفضل الأسلوب native_handle () للكائن std :: thread ، وتمريره إلى دالة دفق Win32 API . هناك العديد من الميزات الأخرى المتوفرة من خلال دفق Win32 API غير متوفرة في std :: thread أو std :: async () . عند العمل من خلال#include "stdafx.h"
#include <windows.h>
#include <iostream>
#include <thread>
using namespace std ;
void LaunchRocket ( )
{
cout << "Launching Rocket" << endl ;
}
int main ( )
{
thread t1 ( LaunchRocket ) ;
DWORD result = :: SetThreadIdealProcessor ( t1. native_handle ( ) , 5 ) ;
t1. join ( ) ;
return 0 ;
}
std :: async () ، وظائف النظام الأساسي هذه غير متوفرة ، مما يجعل هذه الطريقة غير مناسبة للمهام الأكثر تعقيدًا.بديل هو إنشاء std :: packaged_task ونقله إلى مؤشر ترابط التنفيذ المطلوب بعد تعيين خصائص مؤشر الترابط.الخطأ رقم 18: إنشاء سلاسل عمليات "تشغيل" أكثر بكثير من النوى المتوفرة
من وجهة النظر المعمارية ، يمكن تصنيف التدفقات إلى مجموعتين: "الركض" و "الانتظار".تستخدم سلاسل العمليات قيد التشغيل 100٪ من وقت معالج النواة التي تعمل عليها. عند تخصيص أكثر من مؤشر ترابط قيد التشغيل إلى قلب واحد ، تنخفض كفاءة استخدام وحدة المعالجة المركزية. لن نحصل على مكسب في الأداء إذا قمنا بتنفيذ أكثر من مؤشر ترابط قيد التشغيل على معالج واحد - في الواقع ، ينخفض الأداء بسبب تبديل السياق الإضافي.تستخدم مؤشرات انتظار الانتظار فقط بضع دورات على مدار الساعة يتم تنفيذها عليها أثناء انتظار أحداث النظام أو إدخال / إخراج الشبكة ، إلخ. في هذه الحالة ، يظل معظم وقت المعالج المتاح للنواة غير مستخدم. يستطيع مؤشر ترابط انتظار واحد معالجة البيانات ، بينما ينتظر الآخرون الأحداث ليتم تشغيلها - وهذا هو السبب في أنه من المفيد توزيع عدة مؤشرات انتظار انتظار على لب واحد. يمكن أن توفر جدولة عدة مؤشرات ترابط معلقة لكل نواة أداء برنامج أكبر بكثير.لذلك ، كيف نفهم عدد مؤشرات الترابط قيد التشغيل التي يدعمها النظام؟ استخدم الأسلوب std :: thread :: hardware_concurrency () . عادةً ما تُرجع هذه الوظيفة عدد نوى المعالج ، ولكنها تأخذ في الاعتبار النوى التي تتصرف كأنها نواة منطقية أو أكثر بسببhypertreading .يجب عليك استخدام القيمة التي تم الحصول عليها من النظام الأساسي الهدف لتخطيط الحد الأقصى لعدد مؤشرات الترابط قيد التشغيل في وقت واحد من البرنامج. يمكنك أيضًا تعيين نواة واحدة لجميع سلاسل الرسائل المعلقة ، واستخدام العدد المتبقي من النوى لتشغيل سلاسل العمليات. على سبيل المثال ، في نظام رباعي النواة ، استخدم نواة واحدة لجميع مؤشرات الترابط المعلقة ، وللثلاثة النوى المتبقية ، ثلاثة سلاسل تشغيل قيد التشغيل. اعتمادًا على كفاءة برنامج جدولة سلاسل الرسائل ، قد تقوم بعض سلاسل عملياتك القابلة للتنفيذ بتبديل السياق (بسبب فشل الوصول إلى الصفحة ، وما إلى ذلك) ، مما يترك النواة غير نشطة لبعض الوقت. إذا لاحظت هذا الموقف أثناء التوصيف ، فيجب عليك إنشاء عدد أكبر قليلاً من سلاسل العمليات المنفذة من عدد النوى ، وتكوين هذه القيمة لنظامك.الخطأ رقم 19: استخدام الكلمة الأساسية المتقلبة للمزامنة
الكلمة الأساسية المتقلبة ، قبل تحديد نوع المتغير ، لا تجعل العمليات على هذا المتغير الذري أو الخيط آمنة. ما تريد ربما هو الأمراض المنقولة جنسيا :: الذرية .انظر المناقشة على stackoverflow لمزيد من التفاصيل.الخطأ رقم 20: استخدام تأمين القفل الحر ما لم يكن ضروريًا تمامًا
هناك شيء معقد يحبه كل مهندس. يبدو إنشاء برامج تعمل بدون أقفال مغريًا للغاية مقارنة بآليات المزامنة التقليدية ، مثل المزامنة (mutex) ، ومتغيرات الحالة ، وعدم التزامن ، وما إلى ذلك. أن استخدام البرمجة غير المؤمّنة كخيار أولي هو نوع من التحسين السابق لأوانه والذي يمكن أن يمضي جانبًا في أكثر اللحظات غير المناسبة (فكر في حدوث فشل في نظام التشغيل عندما لا يكون لديك تفريغ كومة كامل!).في مسيرتي المهنية في C ++ ، كان هناك موقف واحد فقط يتطلب تنفيذ الكود بدون أقفال ، لأننا عملنا في نظام بموارد محدودة ، حيث لا ينبغي أن تأخذ كل معاملة في مكوننا أكثر من 10 ميكروثانية.قبل التفكير في تطبيق نهج التطوير دون حظر ، يرجى الإجابة على ثلاثة أسئلة:- هل حاولت تصميم بنية النظام الخاص بك بحيث لا يحتاج إلى آلية التزامن؟ بشكل عام ، أفضل التزامن هو عدم التزامن.
- إذا كنت بحاجة إلى التزامن ، فهل قمت بتعريف رمزك لفهم خصائص الأداء؟ إذا كان الأمر كذلك ، هل حاولت تحسين الاختناقات؟
- يمكنك مقياس أفقيا بدلا من مقياس عموديا؟
باختصار ، لتطوير التطبيقات العادي ، يرجى مراعاة البرمجة غير المؤمنة فقط عندما تستنفد جميع البدائل الأخرى. هناك طريقة أخرى لإلقاء نظرة على ذلك وهي أنه إذا كنت لا تزال ترتكب بعض الأخطاء الـ 19 المذكورة أعلاه ، فيجب أن تظل بعيدًا عن البرمجة دون حظر.[من. مترجم: شكرا جزيلا ل vovo4K لمساعدتي في إعداد هذه المقالة.]