Quando você precisaria de "centenas de milhares" de threads?


31

Erlang, Go e Rust afirmam, de uma maneira ou de outra, que eles oferecem suporte à programação simultânea com "threads" / corotinas baratas. As Perguntas frequentes do Go afirmam:

É prático criar centenas de milhares de goroutines no mesmo espaço de endereço.

O Rust Tutorial diz:

Como as tarefas são significativamente mais baratas de criar do que os threads tradicionais, o Rust pode criar centenas de milhares de tarefas simultâneas em um sistema típico de 32 bits.

A documentação de Erlang diz:

O tamanho do heap inicial padrão de 233 palavras é bastante conservador para oferecer suporte aos sistemas Erlang com centenas de milhares ou até milhões de processos.

Minha pergunta: que tipo de aplicativo requer tantos threads simultâneos de execução? Apenas os servidores da Web mais movimentados recebem até milhares de visitantes simultâneos. Os aplicativos do tipo chefe-trabalhador / despachante de emprego que escrevi atingiram retornos decrescentes quando o número de encadeamentos / processos é muito maior que o número de núcleos físicos. Suponho que possa fazer sentido para aplicativos numéricos, mas, na realidade, a maioria das pessoas delega paralelismo a bibliotecas de terceiros escritas em Fortran / C / C ++, não nessas linguagens de nova geração.


5
Acho que a fonte da sua confusão é a seguinte: esses microtreads / tarefas / etc não têm como objetivo principal substituir os processos / threads do SO de que você fala, nem devem ser usados ​​para dividir uma grande parte facilmente paralelizável da trituração de números entre alguns núcleos (como você observou corretamente, não faz sentido ter 100k threads em 4 núcleos para esse fim).
us2012

1
Então, o que eles significam? Talvez eu seja ingênuo, mas nunca encontrei uma situação em que a introdução de corotinas / etc simplificasse um programa de thread único de execução. E consegui atingir "baixos" níveis de simultaneidade com processos, que no Linux eu posso lançar centenas ou milhares sem suar a camisa.
user39019

Não faria sentido ter tantas tarefas realmente funcionando. Isso não significa que você não poderia ter um grande número de tarefas que eram simplesmente bloqueadas, esperando que algo acontecesse.
Loren Pechtel

5
A idéia de assincronia baseada em tarefas versus assincronia baseada em encadeamento é dizer que o código do usuário deve se concentrar nas tarefas que precisam acontecer, em vez de gerenciar os trabalhadores que executam essas tarefas. Pense em um tópico como um trabalhador que você contrata; contratar um trabalhador é caro e, se você o fizer, deseja que ele trabalhe duro no máximo de tarefas possível 100% do tempo. Muitos sistemas podem ser caracterizados como tendo centenas ou milhares de tarefas pendentes, mas você não precisa de centenas ou milhares de trabalhadores.
Eric Lippert

Continuando no comentário de @ EricLippert, existem várias situações em que centenas de milhares de tarefas existiriam. Exemplo # 1: a decomposição de uma tarefa paralela a dados, como processamento de imagem. Exemplo # 2: um servidor que suporta centenas de milhares de clientes, cada um dos quais potencialmente pode emitir um comando a qualquer momento. Cada tarefa exigiria seu próprio "contexto de execução leve" - ​​a capacidade de lembrar em que estado está (protocolos de comunicação) e o comando em execução no momento, e pouco mais. O peso leve é ​​possível desde que cada um tenha uma pilha de chamadas rasa.
Rwong

Respostas:


19

um caso de uso - websockets:
como os websockets têm vida longa em comparação com solicitações simples, em um servidor ocupado, muitos websockets se acumulam com o tempo. os microtreads oferecem uma boa modelagem conceitual e também uma implementação relativamente fácil.

Em geral, os casos em que várias unidades mais ou menos autônomas aguardam a ocorrência de determinados eventos devem ser bons casos de uso.


15

Talvez ajude pensar no que Erlang foi originalmente projetado para fazer, que era gerenciar telecomunicações. Atividades como roteamento, comutação, coleta / agregação de sensores, etc.

Trazendo isso para o mundo da web - considere um sistema como o Twitter . O sistema provavelmente não usaria microfones na geração de páginas da Web, mas poderia usá-los em sua coleção / armazenamento em cache / distribuição de tweets.

Este artigo pode ser de ajuda adicional.


11

Em um idioma em que você não tem permissão para modificar variáveis, o simples ato de manter o estado requer um contexto de execução separado (que a maioria das pessoas chamaria de um encadeamento e Erlang chama um processo). Basicamente, tudo é um trabalhador.

Considere esta função Erlang, que mantém um contador:

counter(Value) ->
    receive                               % Sit idle until a message is received
        increment -> counter(Value + 1);  % Restart with incremented value
        decrement -> counter(Value - 1);  % Restart with decremented value
        speak     ->
            io:fwrite("~B~n", [Value]),
            counter(Value);               % Restart with unaltered value
        _         -> counter(Value)       % Anything else?  Do nothing.
    end.

Em uma linguagem OO convencional como C ++ ou Java, você faria isso tendo uma classe com um membro da classe privada, métodos públicos para obter ou alterar seu estado e um objeto instanciado para cada contador. Erlang substitui a noção do objeto instanciado por um processo, a noção de métodos por mensagens e manutenção de estado com chamadas finais que reiniciam a função com quaisquer valores que compõem o novo estado. O benefício oculto desse modelo - e a maior parte da razão de ser de Erlang - é que o idioma serializa automaticamente o acesso ao valor do contador através do uso de uma fila de mensagens, tornando o código simultâneo muito fácil de implementar com um alto grau de segurança .

Você provavelmente está acostumado à ideia de que as alternâncias de contexto são caras, o que ainda é verdade da perspectiva do sistema operacional host. O tempo de execução Erlang é, por si só, um pequeno sistema operacional ajustado para que a alternância entre seus próprios processos seja rápida e eficiente, mantendo o número de alternâncias de contexto que o SO faz no mínimo. Por esse motivo, ter muitos milhares de processos não é um problema e é incentivado.


1
Seu último aplicativo de counter/1deve usar um minúsculo c;) Tentei corrigi-lo, mas o StackExchange não gosta de edições de 1 caractere.
d11wtq

4

Minha pergunta: que tipo de aplicativo requer tantos threads simultâneos de execução?

1) O fato de uma linguagem "escalar" significa que há menos chances de você abandonar essa linguagem quando as coisas ficarem mais complexas no futuro. (Isso é chamado de conceito "Produto Inteiro".) Muitas pessoas estão abandonando o Apache for Nginx por esse mesmo motivo. Se você estiver perto do "limite máximo" imposto pelo overhead de threads, ficará assustado e começará a pensar em maneiras de superar isso. Os sites nunca podem prever quanto tráfego obterão, portanto, gastar um pouco de tempo tornando as coisas escaláveis ​​é razoável.

2) Uma goroutine por solicitação apenas no início. Existem muitas razões para usar goroutines internamente.

  • Considere um aplicativo Web com solicitações simultâneas de 100, mas cada solicitação gera centenas de solicitações de back-end. O exemplo óbvio é um agregador de mecanismo de pesquisa. Mas qualquer aplicativo pode criar goroutines para cada "área" na tela e gerá-las independentemente, em vez de sequencialmente. Por exemplo, todas as páginas da Amazon.com são compostas por mais de 150 solicitações de back-end, montadas apenas para você. Você não percebe porque eles são paralelos, não sequenciais, e cada "área" é seu próprio serviço da web.
  • Considere qualquer aplicativo em que a confiabilidade e a latência sejam fundamentais. Você provavelmente deseja que cada solicitação recebida ative algumas solicitações de back-end e retorne os dados que retornarem primeiro .
  • Considere qualquer "associação ao cliente" realizada no seu aplicativo. Em vez de dizer "para cada elemento, obtenha dados", você pode criar várias goroutines. Se você tiver um monte de DBs escravos para consultar, você irá magicamente N tempo mais rápido. Caso contrário, não será mais lento.

atinge retornos decrescentes quando o número de threads / processos é muito maior que o número de núcleos físicos

O desempenho não é o único motivo para dividir um programa no CSP . Na verdade, ele pode facilitar a compreensão do programa e alguns problemas podem ser resolvidos com muito menos código.

Como nos slides vinculados acima, ter simultaneidade no seu código é uma maneira de organizar o problema. Não ter goroutines é como não ter uma estrutura de dados Map / Dictonary / Hash no seu idioma. Você pode sobreviver sem ele. Mas uma vez que você o possui, você começa a usá-lo em qualquer lugar, e isso realmente simplifica seu programa.

No passado, isso significava "rolar o seu próprio" programação multithread. Mas isso era complexo e perigoso - ainda não existem muitas ferramentas para garantir que você não esteja criando corridas. E como você evita que um futuro mantenedor cometa um erro? Se você observar programas grandes / complexos, verá que eles gastam MUITOS recursos nessa direção.

Como a concorrência não é uma parte de primeira classe da maioria dos idiomas, os programadores de hoje têm um ponto cego para saber por que isso seria útil para eles. Isso só se tornará mais aparente à medida que todo telefone e relógio de pulso se aproxima de 1000 núcleos. Navegue com uma ferramenta incorporada de detector de corrida.


2

Para Erlang, é comum ter um processo por conexão ou outra tarefa. Por exemplo, um servidor de streaming de áudio pode ter 1 processo por usuário conectado.

A Erlang VM é otimizada para lidar com milhares ou mesmo centenas de milhares de processos, tornando as alternâncias de contexto muito baratas.


1

Conveniência. Quando eu comecei a programar multi-threads, eu fazia muita simulação e desenvolvimento de jogos por diversão. Eu achei que era de grande conveniência apenas girar um thread para cada objeto e deixá-lo fazer suas próprias coisas, em vez de processar cada um por um loop. Se o seu código não for perturbado por um comportamento não determinístico e você não tiver colisões, isso poderá facilitar a codificação. Com a energia disponível para nós agora, se eu voltar a isso, posso facilmente imaginar girando alguns milhares de threads devido à capacidade e memória de processamento suficientes para lidar com tantos objetos discretos!


1

Um exemplo simples para Erlang, projetado para comunicação: transferência de pacotes de rede. Ao fazer uma solicitação http, você pode ter milhares de pacotes TCP / IP. Acrescente a isso que todos se conectam ao mesmo tempo e você tem seu caso de uso.

Considere muitos aplicativos usados ​​internamente por qualquer grande empresa para lidar com seus pedidos ou o que for necessário. Servidores da Web não são a única coisa que precisa de threads.


-2

Algumas tarefas de renderização vêm à mente aqui. Se você estiver executando uma longa cadeia de operações em todos os pixels de uma imagem e se essas operações forem paralelizáveis, mesmo uma imagem relativamente pequena de 1024x768 estará no campo "centenas de milhares".


2
Há alguns anos, passei alguns anos processando imagens em tempo real FLIR, processando imagens de 256x256 a 30 quadros por segundo. A menos que você tenha MUITOS processadores HARDWARE e uma maneira SEM EMENDA de particionar seus dados entre eles, a ÚLTIMA coisa que você deseja fazer é adicionar a alternância de contexto, a contenção de memória e o cache dos custos computacionais reais.
John R. Strohm

Depende do trabalho que está sendo feito. Se tudo o que você está fazendo é entregar um trabalho a uma unidade de núcleo / execução de hardware, após o qual você pode efetivamente esquecê-lo (e observe que é assim que as GPUs funcionam, portanto esse não é um cenário hipotético), então a abordagem é: válido.
Maximus Minimus
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.