المزالق الشائعة لمطوري Python في المقابلات





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



الفرق بين العمليات والخيوط في لينكس



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



هناك نوعان من استدعاءات النظام التي يمكن استخدامها لإنشاء عمليات في Linux:



  • clone()



    . . , . ( , , ).
  • fork()



    . ( ), clone()



    .


أود أن أشير إلى ما يلي: عند إجراء fork()



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



أعتقد أن Linux هو كتاب رائع عن أجهزة Linux. برمجة النظام "لروبرت لوف.



مشاكل تكرار الأحداث



الخدمات والعاملين غير المتزامنين في Python أو Go موجودون في كل مكان في شركتنا. لذلك ، نعتبر أنه من المهم أن يكون لدينا فهم مشترك لعدم التزامن وكيفية عمل Event Loop. العديد من المرشحين بارعون بالفعل في الإجابة عن أسئلة حول مزايا النهج غير المتزامن ويمثلون بشكل صحيح حلقة الأحداث كنوع من الحلقة اللانهائية التي تسمح لك بفهم ما إذا كان حدث معين قد أتى من نظام التشغيل (على سبيل المثال ، كتابة البيانات إلى مأخذ توصيل). لكن الغراء مفقود: كيف يحصل البرنامج على هذه المعلومات من نظام التشغيل؟



بالطبع ، أبسط شيء يجب تذكره هوSelect



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



الإجابة عن Select



أكثر من كافية ، ولكن إذا كنت تتذكر Poll



أو Epoll



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



، Poll



و Epoll



يمكن في هذه المادة .



أنصحك أيضًا بإلقاء نظرة على موضوع عدم التزامن في بايثون بواسطة David Beasley .



GIL يحمي ، لكن ليس أنت



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



يقولون أيضًا أن GIL مطلوب فقط لكي يعمل GC بشكل صحيح. بالنسبة لها ، هو بالطبع مطلوب ، لكن هذه ليست نهاية الأمر.



من وجهة نظر التنفيذ ، سيتم تقسيم أبسط وظيفة إلى عدة خطوات:



import dis

def sum_2(a, b):
    return a + b

dis.dis(sum_2)


4           0 LOAD_FAST                0 (a)
             2 LOAD_FAST                1 (b)
             4 BINARY_ADD
             6 RETURN_VALUE

      
      





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



حول موضوع أمان GIL ، راجع مثالًا بسيطًا:



import threading

a = 0
def x():
    global a
    for i in range(100000):
        a += 1

threads = []

for j in range(10):
    thread = threading.Thread(target=x)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

assert a == 1000000

      
      





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



ومرة أخرى ، لا يسعني إلا أن أشير إلى ديفيد بيسلي .



مبادئ التزامن



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



يأمل العديد من صانعي الثعابين المبتدئين ، كما كتبت أعلاه ، في القوة المعجزة لـ GIL ، لذا فهم لا ينظرون في موضوع بدائيات التزامن. ولكن عبثًا ، يمكن أن يكون مفيدًا عند إجراء العمليات والمهام في الخلفية. موضوع أساسيات المزامنة كبير ومفهوم جيدًا ، على وجه الخصوص ، أوصي بالقراءة عنه في كتاب "Core Python Applications Programming" من تأليف Wesley J. Chun.



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



import threading
lock = threading.Lock()

a = 0
def x():
    global a
    lock.acquire()
    try:
        for i in range(100000):
            a += 1
    finally:
        lock.release()

threads = []

for j in range(10):
    thread = threading.Thread(target=x)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

assert a == 1000000

      
      





أعد المحاولة في جميع أنحاء الرأس



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



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



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



بدلاً من ذلك ، يمكنك محاولة تغيير البروتوكول من HTTP إلى شيء ذي تسليم مضمون (AMQP ، وما إلى ذلك).



يمكن أن تتولى شبكة الخدمة أيضًا مهمة إعادة المحاولة. يمكنك قراءة المزيد في هذا المقال .



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



All Articles