Pièges courants des développeurs Python dans les entretiens





Bonjour à tous, aujourd'hui, je voudrais parler de certaines des difficultés et des idées fausses que de nombreux candidats ont. Notre entreprise se développe activement et je mène ou participe souvent à des entretiens. En conséquence, j'ai identifié plusieurs problèmes qui mettent de nombreux candidats dans une position difficile. Regardons-les ensemble. Je couvrirai les questions spécifiques à Python, mais dans l'ensemble, cet article fonctionnera pour n'importe quel entretien d'embauche. Pour les développeurs expérimentés, aucune vérité ne sera révélée ici, mais pour ceux qui ne font que commencer leur voyage, il sera plus facile de décider des sujets pour les prochains jours.



La différence entre les processus et les threads sous Linux



Eh bien, vous savez, une question si typique et, en général, simple, purement pour comprendre, sans creuser dans les détails et les subtilités. Bien sûr, la plupart des candidats vous diront que les threads sont plus légers, que le contexte bascule plus rapidement entre eux et qu'en général ils vivent à l'intérieur du processus. Et tout cela est correct et merveilleux quand on ne parle pas de Linux. Dans le noyau Linux, les threads sont implémentés de la même manière que les processus normaux. Un thread est simplement un processus qui partage certaines ressources avec d'autres processus.



Deux appels système peuvent être utilisés pour créer des processus sous Linux:



  • clone()



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



    . ( ), clone()



    .


Je voudrais souligner ce qui suit: lorsque vous créez un fork()



processus, vous n'obtenez pas immédiatement une copie de la mémoire du processus parent. Vos processus s'exécuteront avec une seule instance en mémoire. Par conséquent, si au total vous auriez dû avoir un débordement de mémoire, tout continuera à fonctionner. Le noyau marquera les descripteurs de page mémoire du processus parent comme étant en lecture seule, et lorsqu'une tentative est faite pour leur écrire (par le processus enfant ou parent), une exception sera levée et gérée, ce qui entraînera la création d'une copie complète. Ce mécanisme est appelé Copy-on-Write.



Je pense que Linux est un excellent livre sur les appareils Linux. Programmation système "par Robert Love.



Problèmes de boucle d'événement



Les services et les travailleurs asynchrones en Python ou Go sont omniprésents dans notre entreprise. Par conséquent, nous considérons qu'il est important d'avoir une compréhension commune de l'asynchronie et du fonctionnement de la boucle d'événements. De nombreux candidats sont déjà assez bons pour répondre aux questions sur les avantages de l'approche asynchrone et représentent correctement la boucle d'événement comme une sorte de boucle infinie qui vous permet de comprendre si un certain événement est venu du système d'exploitation (par exemple, écrire des données sur un socket). Mais la colle manque: comment le programme obtient-il ces informations du système d'exploitation?



Bien sûr, la chose la plus simple à retenir estSelect



... Avec son aide, une liste de descripteurs de fichiers que vous prévoyez de surveiller est formée. Le code client devra vérifier tous les descripteurs passés pour les événements (et leur nombre est limité à 1024), ce qui le rend lent et peu pratique.



La réponse à propos de est Select



plus que suffisante, mais si vous vous souvenez de Poll



ou Epoll



et parlez des problèmes qu'ils résolvent, alors ce sera un gros plus pour votre réponse. Afin de ne pas causer de soucis inutiles: on ne nous demande pas le code C et la spécification détaillée, nous ne parlons que d'une compréhension de base de ce qui se passe. En savoir plus sur les différences Select



, Poll



et Epoll



peut dans cet article .



Je vous conseille également de vous pencher sur le sujet de l'asynchronie en Python par David Beasley .



Le GIL protège, mais pas vous



Une autre idée fausse courante est que le GIL a été conçu pour protéger les développeurs des problèmes d'accès aux données simultanés. Mais ce n'est pas le cas. Le GIL vous empêchera bien sûr de paralléliser votre programme avec des threads (mais pas des processus). Pour faire simple, le GIL est un verrou qui doit être pris avant tout appel à Python (pas si important. Le code Python est exécuté ou les appels à l'API C Python). Par conséquent, le GIL protégera les structures internes des états incohérents, mais vous devrez, comme dans tout autre langage, utiliser des primitives de synchronisation.



Ils disent également que le GIL n'est nécessaire que pour que le GC fonctionne correctement. Pour elle, il est bien sûr nécessaire, mais ce n'est pas la fin.



Du point de vue de l'exécution, même la fonction la plus simple sera décomposée en plusieurs étapes:



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

      
      





Du point de vue du processeur, chacune de ces opérations n'est pas atomique. Python exécutera de nombreuses instructions de processeur pour chaque ligne de bytecode. Dans le même temps, vous ne devez pas autoriser d'autres threads à changer l'état de la pile ou à effectuer toute autre modification de la mémoire, cela entraînera une erreur de segmentation ou un comportement incorrect. Par conséquent, l'interpréteur demande un verrou global sur chaque instruction de bytecode. Cependant, le contexte peut être changé entre les instructions individuelles, et ici le GIL ne nous sauve en rien. Vous pouvez en savoir plus sur le bytecode et son utilisation dans la documentation .



Sur le thème de la sécurité GIL, voir un exemple simple:



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

      
      





Sur ma machine, l'erreur se bloque de manière stable. Si soudainement cela ne fonctionne pas pour vous, exécutez-le plusieurs fois ou ajoutez des threads. Avec un petit nombre de threads, vous obtiendrez un problème flottant (l'erreur apparaît et n'apparaît pas). Autrement dit, en plus des données incorrectes, de telles situations présentent également un problème sous la forme de leur nature flottante. Cela nous amène également au problème suivant: les primitives de synchronisation.



Et encore une fois, je ne peux que me référer à David Beasley .



Primitives de synchronisation



En général, les primitives de synchronisation ne sont pas la meilleure question pour Python, mais elles montrent une compréhension générale du problème et à quel point vous avez creusé dans cette direction. Le sujet du multithreading, du moins chez nous, est demandé en bonus, et ne sera qu'un plus (si vous répondez). Mais ce n'est pas grave si vous ne l'avez pas encore rencontré. On peut dire que cette question n'est pas liée à une langue spécifique.



De nombreux pythonistes novices, comme je l'ai écrit ci-dessus, espèrent le pouvoir miraculeux du GIL, donc ils ne se penchent pas sur le sujet des primitives de synchronisation. Mais en vain, cela peut s'avérer utile lors de l'exécution d'opérations et de tâches en arrière-plan. Le sujet des primitives de synchronisation est vaste et bien compris, en particulier, je recommande de le lire dans le livre "Core Python Applications Programming" de Wesley J. Chun.



Et puisque nous avons déjà examiné un exemple où le GIL ne nous a pas aidés à travailler avec des threads, nous allons considérer l'exemple le plus simple de la façon de nous protéger d'un tel problème.



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

      
      





Réessayez partout sur la tête



Vous ne pouvez jamais compter sur le fait que l'infrastructure fonctionnera toujours de manière stable. Dans les entretiens, nous demandons souvent de concevoir un microservice simple qui interagit avec les autres (par exemple, via HTTP). La question de la stabilité du service déroute parfois les candidats. Je voudrais souligner quelques problèmes que les candidats négligent lorsqu'ils proposent de réessayer via HTTP.



Le premier problème: le service peut tout simplement ne pas fonctionner pendant une longue période. Les demandes répétées en temps réel n'auront aucun sens.



En gros, Réessayer peut terminer un service qui a commencé à ralentir sous la charge. Le moins dont il a besoin est une augmentation de la charge, qui peut augmenter considérablement en raison de demandes répétées. Il est toujours intéressant pour nous de discuter des méthodes de sauvegarde de l'état et d'implémentation de la répartition après que le service commence à fonctionner normalement.



Vous pouvez également essayer de changer le protocole de HTTP en quelque chose avec une livraison garantie (AMQP, etc.).



Le service mesh peut également prendre en charge la tâche de nouvelle tentative. Vous pouvez en savoir plus dans cet article .



Dans l'ensemble, comme je l'ai dit, il n'y a pas de surprises ici, mais cet article peut vous aider à déterminer les sujets à aborder. Non seulement pour les entretiens, mais aussi pour une compréhension plus approfondie de l'essence des processus en cours.



All Articles