No livro “Python. Para as alturas da excelência ”Luciano Ramallo descreve uma história. Em 2000, Luciano fez cursos e, uma vez que Guido van Rossum olhou para a platéia. Quando esse evento apareceu, todos começaram a fazer perguntas. Quando perguntado sobre quais funções o Python emprestou de outras linguagens, Guido respondeu: "Tudo o que é bom no Python é roubado de outras linguagens".
É mesmo. O Python vive há muito tempo no contexto de outras linguagens de programação e absorve conceitos de seu ambiente: o assíncio é emprestado, graças ao surgimento de expressões lambp do Lisp, e o Tornado foi copiado do libevent. Mas se alguém empresta idéias, são as de Erlang. Ele foi criado há 30 anos e todos os conceitos em Python que estão sendo implementados ou estão apenas esboçados estão trabalhando em Erlang há muito tempo: vários núcleos, mensagens como base de comunicação, chamadas de método e introspecção dentro de um sistema de produção ao vivo. Essas idéias, de uma forma ou de outra, encontram sua expressão em sistemas como o
Seastar.io .
Se você não levar em consideração a Data Science, na qual o Python está fora de competição, tudo o mais já está implementado no Erlang: trabalhando com uma rede, lidando com HTTP e soquetes da web, trabalhando com bancos de dados. Portanto, é importante que os desenvolvedores de Python entendam para onde a linguagem se moverá: ao longo de uma estrada que já passou há 30 anos.
Para entender a história do desenvolvimento de outras linguagens e entender onde o progresso está progredindo, convidamos
Maxim Lapshin (
erlyvideo ), autor do projeto Erlyvideo.ru, para o
Moscow Python Conf ++ .
Sob o corte está a versão em texto deste relatório, a saber: em que direção o sistema é forçado a se desenvolver, que continua a migrar do código linear simples para o libevent e além, o que é comum e quais são as diferenças entre o Elixir e o Python. Prestaremos atenção especial em como gerenciar soquetes, threads e dados em diferentes linguagens e plataformas de programação.
O Erlyvideo.ru possui um sistema de vigilância por vídeo no qual o controle de acesso para câmeras é escrito em Python. Essa é uma tarefa clássica para esse idioma. Existem usuários e câmeras, vídeos nos quais eles podem assistir: alguém vê algumas câmeras, enquanto outros veem um site comum.
O Python foi escolhido porque é conveniente escrever um serviço assim: afinal, existem frameworks, ORMs, programadores. O software desenvolvido é empacotado e vendido aos usuários. Erlyvideo.ru é uma empresa que vende software e não apenas fornece serviços.
Quais problemas com Python eu quero resolver.
Por que existem tais problemas com multicore? Rodamos o Flussonic em computadores estádios antes mesmo da Intel. Mas o Python tem dificuldades com isso: por que ainda não está usando todos os 80 núcleos de nossos servidores para funcionar?
Como não sofrer soquetes abertos? Monitorar o número de soquetes abertos é um grande problema. Quando atingir o limite, feche e evite vazamentos também.
As variáveis globais esquecidas têm uma solução? Vazar variáveis globais é um inferno para qualquer linguagem de coleta de lixo como Java ou C #.
Como usar o ferro sem desperdiçar recursos? Como sobreviver sem executar 40 trabalhadores Jung e 64 GB de RAM, se quisermos usar servidores com eficiência e não gastar centenas de milhares de dólares por mês em hardware desnecessário?
Por que o multicore é necessário
Para que todos os núcleos sejam totalmente utilizados, são necessários muito mais trabalhadores do que núcleos. Por exemplo, para 40 núcleos de processador, são necessários 100 trabalhadores: um trabalhador foi ao banco de dados e o outro está ocupado com outra coisa.
Um trabalhador pode consumir 300-400 MB . Ainda estamos escrevendo isso em Python, e não em Ruby on Rails, que pode consumir várias vezes mais e 40 GB de RAM serão desperdiçados com facilidade e facilidade. Não é muito caro, mas por que comprar memória onde você não pode comprar.
O multi-core ajuda a atrapalhar os dados compartilhados e a reduzir o consumo de memória , executando de maneira conveniente e segura muitos processos independentes. É muito mais fácil programar, mas mais caro da memória.
Gerenciamento de soquete
No soquete da web, pesquisamos os dados de tempo de execução das câmeras a partir do back-end. O software Python se conecta à Flussonic e pesquisa os dados de status das câmeras: funcionem ou não, existem novos eventos.
Por outro lado, o cliente se conecta e, através do soquete da Web, enviamos esses dados para o navegador. Queremos transferir dados do cliente em tempo real: a câmera ligou e desligou, o gato comeu, dormiu, rasgou um sofá, apertou o botão e afastou o gato.
Mas, por exemplo, ocorreu algum tipo de problema: o banco de dados não respondeu à solicitação, todo o código caiu, havia dois soquetes abertos. Começamos a recarregar, fizemos alguma coisa, mais uma vez esse problema - havia dois soquetes. O erro do banco de dados foi processado incorretamente e duas conexões abertas travaram. Com o tempo, isso leva a vazamentos de soquetes.
Variáveis globais esquecidas
Criou um ditado global para a lista de navegadores conectados via soquete da web. Uma pessoa faz login no site, abrimos um soquete da web para ele. Em seguida, colocamos o soquete da web com seu identificador em algum tipo de ditado global e acontece que ocorre algum tipo de erro.
Por exemplo, eles gravaram um link de conexão no dict para enviar dados.
Uma exceção funcionou, esqueci de excluir o link e os dados travaram . Então, depois de algum tempo, 64 GB estão começando a ser perdidos e quero dobrar a memória no servidor. Esta não é uma solução, porque os dados vazam de qualquer maneira.
Sempre cometemos erros - somos pessoas e não podemos acompanhar tudo.
A questão é que alguns erros ocorrem, mesmo aqueles que não esperávamos ver.
Excursão histórica
Para chegar ao tópico principal, vamos nos aprofundar na história. Tudo o que estamos falando sobre Python, Go e Erlang agora, outras pessoas seguiram esse caminho cerca de 30 anos atrás. Nós, em Python, percorremos um longo caminho e preenchemos os obstáculos que já foram ultrapassados décadas atrás. O caminho se repete de uma maneira incrível.
Dos
Primeiro, vamos ao DOS, é o mais próximo. Antes dele, havia coisas completamente diferentes e nem todo mundo está vivo que se lembra de computadores antes do DOS.
O programa DOS ocupava o computador (quase) exclusivamente . Enquanto um jogo, por exemplo, está sendo executado, nada mais é executado. Você não acessa a Internet - ainda não está lá e nem chega a lugar algum. Foi triste, mas as lembranças são quentes, porque estão associadas à juventude.
Multitarefa cooperativa
Como foi realmente doloroso com o DOS, novos desafios surgiram, os computadores se tornaram mais poderosos.
Décadas atrás, eles desenvolveram o conceito de multitarefa cooperativa , mesmo antes do Windows 3.11.
Os dados são separados por processos e cada processo é executado separadamente: eles são de alguma forma protegidos um do outro. Um código incorreto em um processo não poderá estragar o código no navegador (os primeiros navegadores já apareceram).
A próxima pergunta é: como o tempo de computação será distribuído entre diferentes processos? Depois, não havia mais do que um núcleo; um sistema com processador duplo era uma raridade. O esquema era o seguinte: enquanto um processo foi, por exemplo, para um disco para dados, o segundo processo recebe controle do sistema operacional. O primeiro será capaz de obter o controle quando o segundo for voluntariamente. Simplifico bastante a situação, mas o
processo de alguma forma permitiu voluntariamente removê-la do processador .
Multitarefa preemptiva
A multitarefa cooperativa levou ao seguinte problema: o processo pode travar porque está mal escrito.
Se o processador demorar muito para processar, ele bloqueia o restante . Nesse caso, o computador travou e nada pôde ser feito com ele, por exemplo, alternando a janela.
Em resposta a esse problema, a multitarefa preventiva foi inventada. O próprio sistema operacional agora dirige estritamente: remove processos da execução, separa completamente seus dados, protege a memória do processo e proporciona a todos uma certa quantidade de tempo computacional.
O sistema operacional aloca os mesmos intervalos de tempo para cada processo .
A questão do prazo ainda está em aberto. Hoje, os desenvolvedores de sistemas operacionais ainda estão apresentando o que é certo, em que ordem, para quem e quanto tempo dar para o gerenciamento. Hoje vemos o desenvolvimento dessas idéias.
Streams
Mas isso não foi suficiente. Os processos precisam trocar dados: através da rede é caro, de alguma forma ainda complicado. Portanto, o
conceito de fluxos foi inventado.
Threads são processos leves que compartilham uma memória comum.
Os fluxos foram criados com a esperança de que tudo seja fácil, simples e divertido. Agora
a programação multithread é considerada antipadrão . Se a lógica de negócios estiver escrita em threads, é provável que esse código seja descartado, porque provavelmente há erros nele. Se parece que não há erros, você simplesmente ainda não os encontrou.
A programação multithread é uma coisa extremamente complexa. Existem poucas pessoas que realmente se dedicaram à capacidade de escrever em tópicos e obtiveram algo realmente funcionando.
Enquanto isso,
computadores com vários núcleos apareceram. Eles trouxeram coisas terríveis com eles. Foi adotada uma abordagem completamente diferente dos dados, surgiram perguntas com a localidade dos dados, agora você precisa entender a partir de qual kernel você acessa quais dados.
Um núcleo precisa colocar os dados aqui, o outro ali, e em nenhum caso confundir essas coisas, porque os clusters realmente apareceram dentro do computador. Dentro de um computador moderno, existe um cluster quando parte da memória é soldada em um núcleo e a outra em outro. O tempo de trânsito entre esses dados pode variar em ordens de magnitude.
Exemplos de Python
Considere um exemplo simples de "Serviço para ajudar o cliente". Ele seleciona o melhor preço para a mercadoria em várias plataformas: dirigimos em nome da mercadoria e procuramos pregões com um preço mínimo.
Este é o código no antigo Django, Python 2. Hoje não é muito popular, poucas pessoas iniciam projetos nele.
@api_view(['GET']) def best_price(request): name = request.GET['name'] price1 = http_fetch_price('market.yandex.ru', name) price2 = http_fetch_price('ebay.com', name) price3 = http_fetch_price('taobao.com', name) return Response(min([price1,price2,price3]))
Uma solicitação chega, vamos para um back-end e depois para outro. Nos locais em que
http_fetch_price
é
http_fetch_price
, os threads são bloqueados. Neste momento, todo o trabalhador embarca em uma viagem ao Yandex.Market, depois ao eBay, até um tempo limite em Taobao e, no final, dá uma resposta.
Todo esse tempo todo o trabalhador está de pé .
É muito difícil pesquisar vários back-ends ao mesmo tempo. Essa é uma situação ruim: a memória é consumida, é necessário o lançamento de um grande número de trabalhadores e o monitoramento de todo o serviço. É necessário analisar a frequência com que essas solicitações são necessárias, você ainda precisa executar trabalhadores ou existem outras extras novamente. Esses são os mesmos problemas de que falei.
É necessário interrogar vários back-ends, por sua vez .
O que vemos no Python?
Um processo por tarefa, no Python ainda não há multicore. A situação é clara: nos idiomas desta classe, é difícil criar um multicore simples e seguro, porque isso
prejudica o desempenho .
Se você usar o dict a partir de threads diferentes, o acesso aos dados poderá ser escrito assim: cole duas instâncias do Python na memória para que eles removem os dados - eles simplesmente os quebram. Por exemplo, para ditar e não quebrar nada, você precisa colocar mutexes na frente dele. Se houver um mutex antes de cada ditado, o sistema diminuirá a velocidade cerca de 1000 vezes - será simplesmente inconveniente. É difícil arrastá-lo para um multicore.
Temos
apenas um segmento de execução e
apenas processos podem ser escalados . De fato, reinventamos o DOS dentro do processo - a linguagem de script de 2010. Dentro do processo, há algo que se assemelha ao DOS: enquanto estamos fazendo algo, todos os outros processos não funcionam. Ninguém gostou do enorme aumento de custos e da resposta lenta.
Os reatores Socket apareceram em Python há algum tempo, embora o próprio conceito tenha nascido há muito tempo. Agora você pode esperar a disponibilidade de vários soquetes de uma só vez.
A princípio, o reator ficou em demanda em servidores como o nginx. Inclusive devido ao uso correto dessa tecnologia, ela se tornou popular. Em seguida, o conceito entrou nas linguagens de script como Python e Ruby.
A idéia do reator é que passamos à programação orientada a eventos.
Programação Orientada a Eventos
Um contexto de execução produz uma solicitação. Enquanto aguarda uma resposta, um contexto diferente está sendo executado. Vale ressaltar que quase passamos pelo mesmo estágio de evolução que a transição do DOS para o Windows 3.11. Somente pessoas fizeram isso 20 anos antes, e em Python e Ruby apareceu 10 anos atrás.
Torcido
Essa é uma estrutura de rede orientada a eventos. Apareceu em 2002 e está escrito em Python. Peguei o exemplo acima e o reescrevi no Twisted.
def render_GET(self, request): price1 = deferred_fetch_price('market.yandex.ru', name) price2 = deferred_fetch_price('ebay.com', name) price3 = deferred_fetch_price('taobao.com', name) dl = defer.DeferredList([price1,price2,price3]) def reply(prices): request.write('%d'.format(min(prices))) request.finish() dl.addCallback(reply) return server.NOT_DONE_YET
Pode haver erros, imprecisões e a manipulação notória de erros não é suficiente. Mas o esquema aproximado é o seguinte: não fazemos uma solicitação, mas pedimos que a solicite algum tempo depois, quando houver tempo. Na linha com
defer.DeferredList
, queremos coletar as respostas de várias consultas.
De fato, o código consiste em duas partes. Na primeira parte, o que aconteceu antes da solicitação e na segunda, o que foi depois.
Toda a história da programação orientada a eventos está saturada com a dor de quebrar o código linear em "antes da solicitação" e "após a solicitação".
Isso dói porque os trechos de código são misturados: as últimas linhas ainda são executadas na solicitação original e a função de
reply
será chamada depois.
Não é fácil ter em mente exatamente porque quebramos o código linear, mas tinha que ser feito. Sem entrar em detalhes, o código que foi reescrito do Django para o Twisted
produzirá uma pseudo-aceleração completamente incrível .
Idéia torcida
Um objeto pode ser ativado quando o soquete estiver pronto.
Pegamos objetos nos quais coletamos os dados necessários do contexto e vinculamos sua ativação ao soquete. A disponibilidade do soquete é agora um dos controles mais importantes para todo o sistema. Objetos serão nossos contextos.
Mas, ao mesmo tempo, a linguagem ainda separa o próprio conceito do contexto de execução no qual vivem as exceções.
O contexto de execução vive separadamente dos objetos e é vagamente conectado a eles . Aqui, o problema surge com o fato de estarmos tentando coletar dados dentro de objetos: não há como ficar sem eles, mas a linguagem não o suporta.
Tudo isso leva a um inferno clássico de retorno de chamada. Por que, por exemplo, eles amam o Node.js - até recentemente, não havia outros métodos, mas ele ainda aparecia no Python. O problema é que há
quebras de código nos pontos de E / S externas que levam ao retorno de chamada.
Há muitas perguntas. É possível "colar" as bordas da lacuna no código? É possível voltar ao código humano normal? O que fazer se um objeto lógico funcionar com dois soquetes e um deles estiver fechado? Como não esquecer de fechar o segundo? É possível usar de alguma forma todos os núcleos?
Async io
Uma boa resposta para essas perguntas é E / S assíncrona. Este é um passo íngreme para a frente, embora não seja fácil. OI assíncrono é uma coisa complicada, sob o capô do qual existem muitas nuances dolorosas.
async def best_price(request): name = request.GET['name'] price1 = async_http_fetch_price('market.yandex.ru', name) price2 = async_http_fetch_price('ebay.com', name) price3 = async_http_fetch_price('taobao.com', name) prices = await asyncio.wait([price1,price2,price3]) return min(prices)
A diferença de código está oculta na sintaxe
async/await
. Pegamos tudo o que era antes, mas não fomos para a rede nesse código. Removemos o
Callback(reply)
, que estava no exemplo anterior, e o escondemos atrás de
await
- o local onde o código será cortado com uma tesoura. Ele será dividido em duas partes: a parte de chamada e a parte de retorno de chamada, que processa os resultados.
Este é um
ótimo açúcar sintático . Existem métodos para colar várias expectativas em uma. Isso é legal, mas há uma nuance:
tudo pode ser quebrado por um soquete "clássico" . No Python, ainda há um grande número de bibliotecas que acessam o soquete de forma síncrona, criam uma
timer library
e estragam tudo para você. Como depurar isso, eu não sei.
Mas o
assíncio não ajuda com vazamentos e multicore . Portanto, não há mudanças fundamentais, embora tenha se tornado melhor.
Ainda temos todos os problemas sobre os quais falamos no início:
- fácil vazar com soquetes;
- fácil deixar links em variáveis globais;
- tratamento de erros muito meticuloso;
- ainda é difícil fazer multi-core.
O que fazer
Se tudo isso vai evoluir, eu não sei, mas mostrarei a implementação em outros idiomas e plataformas.
Contextos de execução isolados. Nos contextos de execução, os resultados são acumulados, os soquetes são mantidos: objetos lógicos nos quais geralmente armazenamos todos os dados sobre retornos de chamada e soquetes. Um conceito: pegue contextos de execução, cole-os em segmentos de execução e isole-os completamente um do outro.
Mudança de paradigma de objetos. Vamos conectar o contexto ao segmento de execução. Existem análogos, isso não é algo novo. Se alguém tentou editar o código-fonte do Apache e escrever módulos para eles, ele sabe que existe um pool de Apache.
Não são permitidos links entre os pools do Apache. Os dados de um pool do Apache - o pool associado às solicitações, estão localizados dentro dele e você não pode obter nada com isso.
Teoricamente, é possível, mas se você fizer isso, alguém repreenderá ou não aceitará o patch ou terá uma depuração longa e dolorosa na produção. Depois disso, ninguém fará isso e permitirá que outros façam essas coisas. É simplesmente impossível se referir a dados entre contextos, é necessário isolamento completo.
Como trocar atividades? O que é necessário não são pequenas mônadas, que são fechadas entre si e não se comunicam. Precisamos que eles se comuniquem. Uma abordagem é a troca de mensagens. Esse é aproximadamente o caminho que o Windows seguiu ao trocar mensagens entre processos. Em um sistema operacional normal, você não pode fornecer um link para a memória de outro processo, mas pode sinalizar pela rede, como no UNIX, ou através de mensagens, como no Windows.
Todos os recursos dentro do processo e do contexto se tornam um encadeamento de execução . Colamos juntos:
- dados de tempo de execução em uma máquina virtual na qual ocorrem exceções;
- o segmento de execução, como o que está sendo executado no processador;
- Um objeto no qual todos os dados são coletados logicamente.
Parabéns - nós inventamos o UNIX dentro de uma linguagem de programação! Esta ideia foi inventada por volta de 1969. Até o momento, ele ainda não está em Python, mas é provável que o Python chegue a isso. E talvez ela não venha - eu não sei.
O que isso dá
Primeiro de tudo,
controle automático sobre recursos . No Moscow Python Conf ++ 2019, eles
disseram que você pode escrever um programa no Go e processar todos os erros. O programa permanecerá como uma luva e funcionará por meses. Isso é verdade, mas não lidamos com todos os erros.
Somos pessoas vivas, sempre temos prazos, o desejo de fazer algo útil e não lidar com o 535º erro de hoje. Código espalhado pelo tratamento de erros nunca causa sentimentos quentes em ninguém.
Portanto, todos escrevemos “caminho feliz” e depois descobrimos isso na produção. Sejamos honestos: somente quando você precisar processar algo, começamos a processar. A programação defensiva é um pouco diferente e não é um desenvolvimento comercial.
Portanto,
quando temos autocontrole para erros - tudo bem . Mas os sistemas operacionais o criaram há 50 anos: se algum processo morrer, tudo o que abrir será fechado automaticamente. Hoje ninguém precisa escrever um código que limpe os arquivos por trás do processo finalizado. Isso não existe há 50 anos em nenhum sistema operacional, mas no Python você ainda precisa seguir isso com cuidado e cuidado com as mãos. Isso é estranho.
Você pode levar a computação pesada para um contexto diferente , mas já pode ir para outro núcleo. Nós compartilhamos os dados, não precisamos mais de mutexes. Você pode enviar os dados em um contexto diferente, dizer: "Você fará isso em algum lugar e, em seguida, deixe-me saber que você terminou e fez alguma coisa".
Uma implementação assíncrona sem as palavras "async / waitit" . Mais uma pequena ajuda da máquina virtual, do tempo de execução. É sobre isso que conversamos com
async/await
: você também pode converter em mensagens, remover
async/await
e obtê-lo no nível da máquina virtual.
Processos Erlang
Erlang foi inventado há 30 anos. Os caras barbudos, que não estavam muito barbudos na época, olharam para o UNIX e transferiram todos os conceitos para a linguagem de programação. Eles decidiram que agora teriam suas próprias coisas para dormir à noite e silenciosamente iriam pescar sem um computador. Ainda não havia laptops, mas os caras barbudos já sabiam que isso deveria ser pensado com antecedência.
Temos Erlang (Elixir) - contextos ativos que se executam . Além disso, meu exemplo sobre Erlang. No Elixir, parece o mesmo, com algumas variações.
best_price(Name) -> Price1 = spawn_price_fetcher('market.yandex.ru', Name), Price2 = spawn_price_fetcher('ebay.com', Name), Price3 = spawn_price_fetcher('taobao.com', Name), lists:min(wait4([Price1,Price2,Price3])).
Lançamos vários buscadores - esses são vários novos contextos separados que estamos esperando. Eles esperaram, coletaram os dados e retornaram o resultado como o preço mínimo. Tudo isso é semelhante ao
async/await
, mas sem as palavras "assíncrono / espera".
Recursos do Elixir
O Elixir está localizado na base de Erlang, e todos os conceitos de linguagem são portados silenciosamente para o Elixir. Quais são as suas características?
Proibição de links entre processadores. Por processo, quero dizer um processo leve dentro de uma máquina virtual - contexto. Simplificado, se portado para Python, links de dados dentro de outro objeto são proibidos em Erlang. Você pode ter um link para o objeto inteiro como uma caixa fechada, mas não pode fazer referência aos dados dentro dele. Você não pode nem sintaticamente obter um ponteiro para dados que estão dentro de outro objeto. Você só pode saber sobre o próprio objeto.
Não há mutexes dentro de processos (objetos). Isso é importante - pessoalmente, nunca quero interpor minha história com a história da depuração de voos multithread para produção. Eu não desejo isso para ninguém.
Os processos podem se mover pelos núcleos, é seguro. Não precisamos mais ignorar, como em Java, vários outros
pointer
e reescrevê-los ao mover dados de um lugar para outro: não temos dados e links internos comuns. Por exemplo, de onde vem o problema da escassez de quadril? Devido ao fato de alguém se referir a esses dados.
Se transferirmos dados dentro do heap para outro local para compactação, precisamos percorrer todo o sistema. Ele pode ocupar dezenas de gigabytes e atualizar todos os ponteiros - isso é loucura.
Segurança total do thread , devido ao fato de toda a comunicação passar por mensagens. Com a rendição de tudo isso, tivemos um
processo de exclusão . Ele entendeu fácil e barato.
Mensagens como base da comunicação. Objetos internos, chamadas de funções comuns e entre objetos de mensagens. A chegada de dados da rede é uma mensagem, a resposta de outro objeto é uma mensagem, outra coisa fora também é uma mensagem em uma fila de entrada. Isso não está no UNIX porque não foi criado.
Chamadas de método. Temos objetos que chamamos de processos. Métodos em processos são chamados por meio de mensagens.
Os métodos de chamada também estão enviando uma mensagem. É ótimo que agora isso possa ser feito com um tempo limite. Se algo nos responde lentamente, chamamos o método em outro objeto. Mas, ao mesmo tempo, dizemos que estamos prontos para esperar não mais de 60 s, porque eu tenho um cliente com um tempo limite de 70 s. Vou precisar dizer a ele "503" - venha amanhã, agora eles não estão esperando por você.
Além disso, a
resposta para a ligação pode ser adiada . Dentro do objeto, você pode aceitar a solicitação para chamar o método e dizer: "Sim, sim, eu vou te largar agora, volte em meia hora, eu responderei." Você não pode falar, mas silenciosamente reserve. Às vezes usamos.
Como trabalhar com uma rede?
Você pode escrever código linear, retornos de chamada ou no estilo de
asyncio.gather
. Um exemplo de como isso ficará.
wait4([ ]) -> [ ]; wait4(List) -> receive {reply, Pid, Price} -> [Price] ++ wait4(List -- [Pid]) after 60000 -> [] end.
Na função
wait4
do exemplo anterior,
wait4
sobre a lista daqueles de quem ainda estamos esperando respostas. Se, usando o método de
receive
,
receive
uma mensagem desse processo, escrevemos na lista. Se a lista terminar, retornamos tudo o que havia e acumulamos a lista. Pedimos ao mesmo tempo três objetos para nos conduzir os dados. Se eles não conseguiram gerenciar juntos em 60 segundos e pelo menos um deles não respondeu OK, teremos uma lista vazia. Mas é importante que tenhamos um tempo limite geral para uma solicitação imediatamente para um monte de objetos.
Alguém pode dizer: "Pense, libcurl tem a mesma coisa." Mas aqui é importante que, por outro lado, possa haver não apenas uma viagem HTTP, mas também uma viagem ao banco de dados, bem como alguns cálculos, por exemplo, calculando algum tipo de número ideal para o cliente.
Tratamento de erros
Os erros foram transmitidos do fluxo para o objeto, que agora são o mesmo . Agora, o próprio erro fica anexado não ao encadeamento, mas ao objeto em que foi executado.
Isso é muito mais lógico. Geralmente, quando desenhamos todos os tipos de pequenos quadrados e círculos no quadro, na esperança de que eles ganhem vida e tragam resultados e dinheiro, geralmente desenhamos objetos, não os fluxos nos quais esses objetos serão executados. Por exemplo, na entrega, podemos receber uma
mensagem automática
sobre a morte de outro objeto .
Introspecção ou depuração na produção
O que poderia ser melhor do que ir ao produto e debitar, especialmente se o erro ocorrer apenas sob carga durante o horário de pico. Na hora do rush, dizemos:
- Vamos lá, vou reiniciar agora!- Saia pela porta e reinicie outra pessoa!Aqui podemos entrar em um sistema vivo que está sendo executado no momento e não está especialmente preparado para isso. Para fazer isso, você não precisa reiniciá-lo com o criador de perfil, com o depurador, reconstruído.
Sem qualquer perda de desempenho em um sistema de produção ao vivo, podemos olhar para uma lista de processos: o que há dentro deles, como tudo funciona, jogá-los no lixo, verificar o que acontece com eles. Tudo isso é gratuito fora da caixa.
Bónus
O código é super confiável. Por exemplo, o Python tem fragilidade com o
old vs async
e permanecerá por cinco anos, nada menos. Considerando a velocidade com que o Python 3 foi implementado, você não deve esperar que seja rápido.
Ler e rastrear mensagens é mais fácil do que depurar retornos de chamada . Isso é importante. Parece que, se ainda temos retornos de chamada para processar mensagens que podemos ver, o que é melhor? Pelo fato de as mensagens serem um dado na memória. Você pode olhar com os olhos e entender o que veio aqui. Pode ser adicionado ao rastreador, obter uma lista de mensagens em um arquivo de texto. Isso é mais conveniente do que retornos de chamada.
Gerenciamento de
introspecção e
múltiplos núcleos deslumbrante dentro de um sistema de produção
ao vivo .
Os problemas
Naturalmente, Erlang também tem problemas.
Perda do desempenho máximo devido ao fato de não podermos mais nos referir a dados em outro processo ou objeto. Temos que movê-los, mas isso não é gratuito.
A sobrecarga de copiar dados entre processos. Podemos escrever um programa em C que será executado em todos os 80 núcleos e processar uma matriz de dados, e assumiremos que o faz corretamente e corretamente. Em Erlang, você não pode fazer isso: você precisa cortar os dados com cuidado, distribuí-los por vários processos, acompanhar tudo. Essa comunicação custa recursos - ciclos do processador.
Quão rápido ou lento é? Estamos escrevendo o código Erlang há 10 anos. O único concorrente que sobreviveu a esses 10 anos é escrito em Java. Com ele, temos uma paridade de desempenho quase completa: alguém diz que somos piores, alguém que eles são. Mas eles têm Java com todos os seus problemas, começando com o JIT.
Estamos escrevendo um programa que atende dezenas de milhares de soquetes e bombeia dezenas de GB de dados por si próprio. De repente, verifica-se que, neste caso, a
correção dos algoritmos e a capacidade de depurar tudo isso na produção são mais importantes do que os possíveis Java buns . Bilhões de dólares foram investidos nele, mas isso não dá ao Java JIT nenhuma vantagem mágica.
Mas se quisermos medir benchmarks estúpidos e sem sentido, como "calcular os números de Fibonacci", então aqui Erlang provavelmente será ainda pior que Python ou comparável.
A sobrecarga da alocação de mensagens. Às vezes dói. Por exemplo, temos algumas partes em C no código e, nesses lugares, não funcionou com Erlang. , , .
Erlang
, , . , ,
receive
send receive
. — , .
, , .
Python
. . , Python - .
,
. - Python, , 20 , 40.
,
. - , , Elixir, , .
Moscow Python Conf++ . , 6 4 . , , ) ) . Call for Papers 13 , 27 .