Errores comunes de los desarrolladores de Python en las entrevistas





Hola a todos, hoy me gustaría hablarles sobre algunas de las dificultades y conceptos erróneos que enfrentan muchos solicitantes de empleo. Nuestra empresa está creciendo activamente y con frecuencia realizo o participo en entrevistas. Como resultado, identifiqué varios problemas que pusieron a muchos candidatos en una posición difícil. Veámoslos juntos. Cubriré preguntas específicas de Python, pero en general este artículo funcionará para cualquier entrevista de trabajo. Para los desarrolladores experimentados, no se revelará ninguna verdad aquí, pero para aquellos que recién están comenzando su viaje, será más fácil decidir sobre los temas para los próximos días.



La diferencia entre procesos e hilos en Linux



Pues ya sabes, una pregunta tan típica y, en general, sencilla, puramente de comprensión, sin ahondar en detalles y sutilezas. Por supuesto, la mayoría de los solicitantes le dirán que los hilos son más livianos, el contexto cambia más rápido entre ellos y, en general, viven dentro del proceso. Y todo esto es correcto y maravilloso cuando no estamos hablando de Linux. En el kernel de Linux, los subprocesos se implementan de la misma manera que los procesos normales. Un hilo es simplemente un proceso que comparte algunos recursos con otros procesos.



Hay dos llamadas al sistema que se pueden utilizar para crear procesos en Linux:



  • clone()



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



    . ( ), clone()



    .


Me gustaría señalar lo siguiente: cuando haces un fork()



proceso, no obtienes inmediatamente una copia de la memoria del proceso padre. Sus procesos se ejecutarán con una única instancia en memoria. Por lo tanto, si en total debería haber tenido un desbordamiento de memoria, entonces todo seguirá funcionando. El kernel marcará los descriptores de la página de memoria del proceso padre como de solo lectura, y cuando se intente escribir en ellos (por el proceso hijo o padre), se generará y manejará una excepción, lo que hará que se cree una copia completa. Este mecanismo se llama Copia en escritura.



Creo que Linux es un gran libro sobre dispositivos Linux. Programación del sistema "por Robert Love.



Problemas de bucle de eventos



Los servicios y trabajadores asincrónicos en Python o Go son omnipresentes en nuestra empresa. Por lo tanto, consideramos importante tener una comprensión común de la asincronía y cómo funciona el bucle de eventos. Muchos candidatos ya son bastante buenos para responder preguntas sobre las ventajas del enfoque asincrónico y representan correctamente el bucle de eventos como una especie de bucle infinito que le permite comprender si un determinado evento proviene del sistema operativo (por ejemplo, escribir datos en un socket). Pero falta el pegamento: ¿cómo obtiene el programa esta información del sistema operativo?



Por supuesto, lo más simple de recordar esSelect



... Con su ayuda, se crea una lista de descriptores de archivos que planea monitorear. El código del cliente tendrá que verificar todos los identificadores pasados ​​en busca de eventos (y su número está limitado a 1024), lo que lo hace lento e inconveniente.



La respuesta sobre es Select



más que suficiente, pero si recuerda sobre Poll



o Epoll



y habla sobre los problemas que resuelven, esto será una gran ventaja para su respuesta. Para no causar preocupaciones innecesarias: no se nos pide código C y especificaciones detalladas, solo estamos hablando de una comprensión básica de lo que está sucediendo. Lea acerca de las diferencias Select



, Poll



y Epoll



lata en este artículo .



También te aconsejo que mires el tema de la asincronía en Python por David Beasley .



El GIL protege, pero no a ti



Otro error común es que el GIL fue diseñado para proteger a los desarrolladores de problemas de acceso simultáneo a datos. Pero este no es el caso. El GIL, por supuesto, evitará que paralelice su programa con subprocesos (pero no procesos). En términos más simples, el GIL es un bloqueo que debe tomarse antes de cualquier llamada a Python (no es tan importante. Se ejecuta código Python o llamadas a la API de Python C). Por lo tanto, GIL protegerá las estructuras internas de estados inconsistentes, pero usted, como en cualquier otro lenguaje, tendrá que usar primitivas de sincronización.



También dicen que el GIL solo es necesario para que el GC funcione correctamente. Para ella, él, por supuesto, es necesario, pero este no es el final.



Desde el punto de vista de la ejecución, incluso la función más simple se dividirá en varios pasos:



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

      
      





Desde el punto de vista del procesador, cada una de estas operaciones no es atómica. Python ejecutará muchas instrucciones del procesador para cada línea de código de bytes. En este caso, no debe permitir que otros subprocesos cambien el estado de la pila o realicen cualquier otra modificación de la memoria, esto dará lugar a una falla de segmentación o un comportamiento incorrecto. Por lo tanto, el intérprete solicita un bloqueo global en cada instrucción de código de bytes. Sin embargo, el contexto se puede cambiar entre instrucciones individuales, y aquí el GIL no nos salva de ninguna manera. Puede leer más sobre el código de bytes y cómo trabajar con él en la documentación .



Sobre el tema de la seguridad GIL, vea un ejemplo 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

      
      





En mi máquina, el error se bloquea de forma estable. Si de repente no funciona para usted, ejecútelo varias veces o agregue subprocesos. Con una pequeña cantidad de subprocesos, obtendrá un problema flotante (el error aparece y no aparece). Es decir, además de los datos incorrectos, tales situaciones tienen un problema en la forma de su naturaleza flotante. Esto también nos lleva al siguiente problema: primitivas de sincronización.



Y nuevamente, no puedo dejar de referirme a David Beasley .



Primitivas de sincronización



En general, las primitivas de sincronización no son la mejor pregunta para Python, pero muestran una comprensión general del problema y cuán profundamente cavó en esta dirección. El tema del multihilo, al menos con nosotros, se pregunta como un extra, y solo será un plus (si responde). Pero está bien si aún no lo ha encontrado. Podemos decir que esta pregunta no está ligada a un idioma específico.



Muchos pitonistas novatos, como escribí anteriormente, esperan el poder milagroso del GIL, por lo que no investigan el tema de las primitivas de sincronización. Pero en vano, puede resultar útil al realizar operaciones y tareas en segundo plano. El tema de las primitivas de sincronización es amplio y está bien entendido, en particular, recomiendo leerlo en el libro "Programación de aplicaciones de Python Core" de Wesley J. Chun.



Y como ya hemos visto un ejemplo en el que GIL no nos ayudó a trabajar con hilos, consideraremos el ejemplo más simple de cómo protegernos de tal problema.



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

      
      





Reintentar por toda la cabeza



Nunca puede confiar en el hecho de que la infraestructura siempre funcionará de manera estable. En las entrevistas, a menudo pedimos diseñar un microservicio simple que interactúe con otros (por ejemplo, a través de HTTP). El tema de la estabilidad del servicio a veces confunde a los candidatos. Me gustaría señalar algunos problemas que los candidatos pasan por alto cuando proponen reintentar a través de HTTP.



El primer problema: es posible que el servicio simplemente no funcione durante mucho tiempo. Las solicitudes repetidas en tiempo real no tendrán sentido.



Aproximadamente Retry puede acabar con un servicio que ha comenzado a ralentizarse bajo carga. Lo mínimo que necesita es un aumento en la carga, que puede crecer significativamente debido a las solicitudes repetidas. Siempre es interesante para nosotros discutir los métodos para guardar el estado e implementar el envío después de que el servicio comienza a funcionar normalmente.



Alternativamente, puede intentar cambiar el protocolo de HTTP a algo con entrega garantizada (AMQP, etc.).



La malla de servicios también puede hacerse cargo de la tarea de reintento. Puedes leer más en este artículo .



En general, como dije, no hay sorpresas aquí, pero este artículo puede ayudarlo a descubrir qué temas abordar. No solo para entrevistas, sino también para una comprensión más profunda de la esencia de los procesos en curso.



All Articles