عبء الوقت الثقيل. تقرير ياندكس عن الأخطاء الشائعة في العمل مع الوقت

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



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

على سبيل المثال ، رأيت أنه في مشاريع مختلفة بلغات مختلفة ، تنشأ نفس المهام أو المهام المشابهة - العمل مع التاريخ والوقت. بالإضافة إلى هذا العمل نفسه ، يمكن أن يكون عمليات منبثقة في التعليمات البرمجية مع كائنات التاريخ والوقت.



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

ما الذي أريد التحدث عنه في النهاية؟ حول هذه الأنماط المتكررة التي تحدث بغض النظر عن اللغة التي تكتبها والأخطاء التي يسهل ارتكابها وكيفية عدم ارتكابها.

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



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



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



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

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



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

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



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

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

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



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

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

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

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

ما يكفي من الوقت. الجزء الثاني هو أكثر خاطئ.



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

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



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



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



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

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

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

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

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



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

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



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



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

بما أننا انتقلنا إلى React ، فسنقترب منه.



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

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

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



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



من أين يمكن تأجيل رمزك في المتصفح؟ هناك أشياء مثل خنق و debounce. لديهم setTimeout ، setInterval تحت غطاء محرك السيارة ، شيء سبق أن عرضت عنه. لا يزال هناك requestAnimationFrame ، لا يزال هناك requestIdleCallback. وطلبات AJAX أيضًا - يمكن استدعاء عمليات الاسترجاعات لطلب AJAX المؤجلة. لا تنس عنهم أيضًا ، فهم بحاجة أيضًا للتنظيف.



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

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

الجزء الثالث هو عكس الثاني. إنها ، على العكس ، تدور حول التزامن.



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

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



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



للتأكد من اكتمالها ، ماذا لدينا أيضًا في واجهة برمجة التطبيقات لـ Promise؟ هنا هو Promise.all () ، الذي يقوم بتشغيل جميع الطلبات بالتوازي ، وينتظر التنفيذ. هناك Promise.race () ، والذي ينتظر أولهم للنجاح. وبشكل عام ، لا يوجد شيء آخر في API القياسي.



, , - . Async, . . . . , , , , forEach(). forEach() , , forEach() - , , . , map() - , forEach() — .



— bluebird. , , Promise.any(). , , : N , N - , , . , , . .

Promise.race(), , promise , , , . . Promise.any() — reject. . reject , resolve , , . . promise — , .

, map, reduce, each, filter . API , Async JS, . promise . , , , promise. .

promise? , async/await.



. . . , «» . , webdriver. , , - , . . . webdriver.

, await. . , - . await, — , , ! .



— Promise.all(). , await.



: await , then . , .

, . : await, , — , .

, , :


, -, :


? , — Lodash, RxJS . . , . , - . . — , , . .

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


All Articles