
Olá a todos, hoje gostaria de falar sobre algumas das dificuldades e equívocos que muitos candidatos a emprego enfrentam. Nossa empresa está crescendo ativamente e frequentemente conduzo ou participo de entrevistas. Como resultado, identifiquei vários problemas que colocam muitos candidatos em uma posição difícil. Vamos olhar para eles juntos. Abordarei questões específicas do Python, mas no geral este artigo funcionará para qualquer entrevista de emprego. Para desenvolvedores experientes, nenhuma verdade será revelada aqui, mas para aqueles que estão apenas começando sua jornada, será mais fácil decidir sobre os tópicos nos próximos dias.
A diferença entre processos e threads no Linux
Bem, você sabe, uma pergunta tão típica e, em geral, simples, puramente para entender, sem cavar em detalhes e sutilezas. Claro, a maioria dos candidatos dirá que os tópicos são mais leves, o contexto muda entre eles mais rapidamente e, em geral, eles vivem dentro do processo. E tudo isso é correto e maravilhoso quando não estamos falando de Linux. No kernel do Linux, os threads são implementados da mesma maneira que os processos normais. Um thread é simplesmente um processo que compartilha alguns recursos com outros processos.
Existem duas chamadas de sistema que podem ser usadas para criar processos no Linux:
clone()
. . , . ( , , ).fork()
. ( ),clone()
.
Gostaria de salientar o seguinte: quando você faz um
fork()
processo, você não obtém imediatamente uma cópia da memória do processo pai. Seus processos serão executados com uma única instância na memória. Portanto, se no total houver um estouro de memória, tudo continuará funcionando. O kernel marcará os descritores de página de memória do processo pai como somente leitura e, quando for feita uma tentativa de gravar neles (pelo processo filho ou pai), uma exceção será levantada e tratada, o que fará com que uma cópia completa seja criada. Este mecanismo é denominado Copy-on-Write.
Acho que Linux é um ótimo livro sobre dispositivos Linux. Programação do sistema "por Robert Love.
Problemas de loop de evento
Os serviços e trabalhadores assíncronos em Python ou Go são onipresentes em nossa empresa. Portanto, consideramos importante ter um entendimento comum sobre assincronia e como funciona o loop de eventos. Muitos candidatos já são muito bons em responder a perguntas sobre as vantagens da abordagem assíncrona e representam corretamente o Loop de Eventos como uma espécie de loop infinito que permite entender se um determinado evento veio do sistema operacional (por exemplo, gravar dados em um soquete). Mas falta a cola: como o programa obtém essas informações do sistema operacional?
Claro, a coisa mais simples de lembrar é
Select
... Com sua ajuda, é formada uma lista de descritores de arquivo que você planeja monitorar. O código do cliente terá que verificar todos os identificadores passados para eventos (e seu número é limitado a 1024), o que o torna lento e inconveniente.
A resposta sobre é
Select
mais do que suficiente, mas se você se lembrar de
Poll
ou
Epoll
e falar sobre os problemas que eles resolvem, isso será uma grande vantagem para sua resposta. Para não causar preocupações desnecessárias: não somos solicitados a fornecer o código C e a especificação detalhada, estamos falando apenas sobre uma compreensão básica do que está acontecendo. Leia sobre as diferenças
Select
,
Poll
e
Epoll
pode em este artigo .
Também aconselho você a examinar o tópico de assincronia em Python, de David Beasley .
O GIL protege, mas não você
Outro equívoco comum é que o GIL foi projetado para proteger os desenvolvedores de problemas simultâneos de acesso a dados. Mas este não é o caso. O GIL irá, é claro, evitar que você paralelize seu programa com threads (mas não com processos). Em termos simples, o GIL é um bloqueio que deve ser executado antes de qualquer chamada para Python (não tão importante. O código Python é executado ou chamadas Python C API). Portanto, o GIL protegerá as estruturas internas de estados inconsistentes, mas você, como em qualquer outra linguagem, terá que usar primitivas de sincronização.
Eles também dizem que o GIL só é necessário para que o GC funcione corretamente. Para ela, ele, claro, é necessário, mas isso não é tudo.
Do ponto de vista da execução, mesmo a função mais simples será dividida em várias etapas:
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
Do ponto de vista do processador, cada uma dessas operações não é atômica. Python executará muitas instruções do processador para cada linha de bytecode. Neste caso, você não deve permitir que outros threads alterem o estado da pilha ou façam qualquer outra modificação na memória, isso levará a uma falha de segmentação ou comportamento incorreto. Portanto, o interpretador solicita um bloqueio global em cada instrução de bytecode. No entanto, o contexto pode ser alterado entre as instruções individuais, e aqui o GIL não nos salva de forma alguma. Você pode ler mais sobre bytecode e como trabalhar com ele na documentação .
Sobre o tema segurança GIL, veja um exemplo simples:
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
Na minha máquina, o erro falha de forma estável. Se de repente ele não funcionar para você, execute-o várias vezes ou adicione tópicos. Com um pequeno número de threads, você terá um problema de flutuação (o erro aparece e não aparece). Ou seja, além de dados incorretos, tais situações apresentam um problema na forma de flutuação. Isso também nos leva ao próximo problema: primitivas de sincronização.
E, novamente, não posso deixar de me referir a David Beasley .
Primitivas de sincronização
Em geral, as primitivas de sincronização não são a melhor pergunta para Python, mas mostram uma compreensão geral do problema e quão profundamente você se aprofundou nessa direção. O tópico de multithreading, pelo menos conosco, é pedido como um bônus, e será apenas um plus (se você responder). Mas está tudo bem se você ainda não o encontrou. Podemos dizer que essa questão não está ligada a uma linguagem específica.
Muitos pythonists novatos, como escrevi acima, esperam pelo poder miraculoso do GIL, então eles não olham para o tópico de primitivos de sincronização. Mas em vão, pode ser útil ao executar operações e tarefas em segundo plano. O tópico de primitivas de sincronização é amplo e bem compreendido, em particular, recomendo a leitura sobre isso no livro "Core Python Applications Programming" de Wesley J. Chun.
E como já vimos um exemplo em que o GIL não nos ajudou a trabalhar com threads, vamos considerar o exemplo mais simples de como nos proteger 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
Tente novamente na cabeça
Você nunca pode confiar no fato de que a infraestrutura sempre funcionará de forma estável. Em entrevistas, geralmente pedimos para projetar um microsserviço simples que interaja com outras pessoas (por exemplo, por HTTP). A questão da estabilidade do serviço às vezes confunde os candidatos. Eu gostaria de apontar alguns problemas que os candidatos esquecem ao propor uma nova tentativa por HTTP.
O primeiro problema: o serviço pode simplesmente não funcionar por muito tempo. Solicitações repetidas em tempo real serão inúteis.
A tentativa malfeita pode encerrar um serviço que começou a ficar lento sob carga. O mínimo que ele precisa é de um aumento na carga, que pode aumentar significativamente devido a solicitações repetidas. Estamos sempre interessados em discutir métodos para salvar o estado e implementar o despacho depois que o serviço começar a funcionar normalmente.
Como alternativa, você pode tentar alterar o protocolo de HTTP para algo com entrega garantida (AMQP, etc.).
A malha de serviço também pode assumir a tarefa de nova tentativa. Você pode ler mais neste artigo .
No geral, como eu disse, não há surpresas aqui, mas este artigo pode ajudá-lo a descobrir quais tópicos abordar. Não apenas para entrevistas, mas também para uma compreensão mais profunda da essência dos processos em andamento.