كيف تنام الصواب والخطأ

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

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

void Sleep(DWORD dwMilliseconds); 

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

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

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

وهو مخصص لمحاكاة البرمجيات لبعض العوامل الخارجية ، والتي يتم تعريفها بشيء حقيقي ، وعملية إيقاف مؤقت.

المثال الصحيح رقم 1


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

المثال الصحيح رقم 2


نحن نكتب وحدة تحكم لغسول لآلة الخبز. يتم ضبط خوارزمية التشغيل بواسطة أحد البرامج وتبدو على النحو التالي:

  1. انتقل إلى الوضع 1.
  2. العمل فيه لمدة 20 دقيقة
  3. انتقل إلى الوضع 2.
  4. العمل فيه لمدة 10 دقائق
  5. أغلق.

كل شيء هنا واضح أيضًا: نحن نعمل مع الوقت ، يتم ضبطه بواسطة العملية التكنولوجية. استخدام النوم مقبول.

الآن دعونا نلقي نظرة على أمثلة من سوء استخدام النوم.

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

مثال سيء رقم 1


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

 if ((!isMultiInst) && (!TheFirstOne)) { HWND hNotepad_plus = ::FindWindow(Notepad_plus_Window::getClassName(), NULL); for (int i = 0 ;!hNotepad_plus && i < 5 ; ++i) { Sleep(100); hNotepad_plus = ::FindWindow(Notepad_plus_Window::getClassName(), NULL); } if (hNotepad_plus) { ... } ... } 


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

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

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

مثال سيئ رقم 2


 HANDLE hThread = ::CreateThread(NULL, 0, threadTextTroller, &trollerParams, 0, NULL); int sleepTime = 1000 / x * y; ::Sleep(sleepTime); 

لم أفهم حقًا ما هي المدة التي ينتظرها هذا الرمز في Notepad ++ ومدة انتظاره ، لكنني غالبًا ما رأيت المضاد العام "بدء البث والانتظار". يتوقع الناس أشياء مختلفة: بداية تيار آخر ، واستلام بعض البيانات منه ، ونهاية عمله. شيئان سيئان هنا على الفور:

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

مثال سيئ رقم 3


نرى هنا خيط خلفية نائماً ينتظر بعض الأحداث.

 class CReadChangesServer { ... void Run() { while (m_nOutstandingRequests || !m_bTerminate) { ::SleepEx(INFINITE, true); } } ... void RequestTermination() { m_bTerminate = true; ... } ... bool m_bTerminate; }; 

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

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

مثال سيء رقم 4


في هذا المثال ، سنلقي نظرة على وظيفة backupDocument ، والتي تعمل بمثابة "حفظ تلقائي" ، وهي مفيدة في حالة تعطل محرر غير متوقع. افتراضيًا ، تنام لمدة 7 ثوانٍ ، ثم تعطي الأمر لحفظ التغييرات (إذا كانت كذلك).

 DWORD WINAPI Notepad_plus::backupDocument(void * /*param*/) { ... while (isSnapshotMode) { ... ::Sleep(DWORD(timer)); ... ::PostMessage(Notepad_plus_Window::gNppHWND, NPPM_INTERNAL_SAVEBACKUP, 0, 0); } return TRUE; } 

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

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

مثال سيء رقم 5


يمكن لـ Notepad ++ "كتابة نص" - أي محاكاة إدخال النص البشري عن طريق الإيقاف المؤقت بين إدخال الحرف. يبدو أنه مكتوب كـ "بيضة عيد الفصح" ، ولكن يمكنك الخروج بنوع من تطبيق العمل لهذه الميزة ( خداع Upwork ، نعم ).

 int pauseTimeArray[nbPauseTime] = {200,400,600}; const int maxRange = 200; ... int ranNum = getRandomNumber(maxRange); ::Sleep(ranNum + pauseTimeArray[ranNum%nbPauseTime]); ::SendMessage(pCurrentView->getHSelf(), SCI_DELETEBACK, 0, 0); 

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

الاستنتاجات


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

Source: https://habr.com/ru/post/ar427843/


All Articles