O I / O sem bloqueio é realmente mais rápido do que o I / O com bloqueio multithread? Quão?

117

Eu pesquisei na web alguns detalhes técnicos sobre bloqueio de E / S e E / S sem bloqueio e encontrei várias pessoas afirmando que E / S sem bloqueio seria mais rápido do que E / S de bloqueio. Por exemplo, neste documento .

Se eu usar o bloqueio de E / S, é claro que o thread que está bloqueado no momento não pode fazer mais nada ... Porque está bloqueado. Mas assim que um thread começa a ser bloqueado, o sistema operacional pode alternar para outro thread e não voltar até que haja algo a ser feito para o thread bloqueado. Portanto, enquanto houver outro thread no sistema que precisa da CPU e não está bloqueado, não deve haver mais tempo ocioso da CPU em comparação com uma abordagem sem bloqueio baseada em eventos, certo?

Além de reduzir o tempo de ociosidade da CPU, vejo mais uma opção para aumentar o número de tarefas que um computador pode realizar em um determinado intervalo de tempo: Reduzir a sobrecarga introduzida pela troca de threads. Mas como isso pode ser feito? E a sobrecarga é grande o suficiente para mostrar efeitos mensuráveis? Aqui está uma ideia de como posso imaginá-lo funcionando:

  1. Para carregar o conteúdo de um arquivo, um aplicativo delega essa tarefa a uma estrutura de i / o baseada em eventos, passando uma função de retorno de chamada junto com um nome de arquivo
  2. A estrutura do evento delega ao sistema operacional, que programa um controlador DMA do disco rígido para gravar o arquivo diretamente na memória
  3. A estrutura de eventos permite que mais códigos sejam executados.
  4. Após a conclusão da cópia do disco para a memória, o controlador DMA causa uma interrupção.
  5. O manipulador de interrupção do sistema operacional notifica a estrutura de E / S baseada em eventos sobre o arquivo sendo completamente carregado na memória. Como isso faz? Usando um sinal ??
  6. O código que é executado atualmente na estrutura de e / s de evento termina.
  7. A estrutura de i / o baseada em eventos verifica sua fila e vê a mensagem do sistema operacional da etapa 5 e executa o retorno de chamada que obteve na etapa 1.

É assim que funciona? Se não, como funciona? Isso significa que o sistema de eventos pode funcionar sem nunca ter a necessidade de tocar explicitamente na pilha (como um agendador real que precisaria fazer backup da pilha e copiar a pilha de outro encadeamento na memória enquanto alterna os encadeamentos)? Quanto tempo isso realmente economiza? Existe mais do que isso?

ianque
fonte
5
resposta curta: é mais sobre a sobrecarga de ter um thread por conexão. O io sem bloqueio permite evitar um thread por conexão.
Dan D.
10
O bloqueio de E / S é caro em um sistema onde você não pode criar tantos threads quanto as conexões existentes. No JVM você pode criar alguns milhares de threads, mas e se você tiver mais de 100.000 conexões? Portanto, você deve aderir a uma solução assíncrona. No entanto, existem linguagens onde os tópicos não são caros (por exemplo, tópicos verdes) como em Go / Erlang / Rust, onde não é um problema ter 100.000 tópicos. Quando o número de threads pode ser grande, acredito que o bloqueio de E / S produz tempos de resposta mais rápidos. Mas eu também teria que perguntar aos especialistas se isso é verdade na realidade.
OlliP
@OliverPlow, também acho, porque bloquear IO normalmente significa que deixamos o sistema lidar com o "gerenciamento paralelo", em vez de fazermos nós mesmos usando filas de tarefas e tal.
Pacerier de
1
@DanD., E se a sobrecarga de ter threads for igual à sobrecarga de execução de E / S sem bloqueio? (geralmente verdadeiro no caso de fios verdes)
Pacerier
"copiar a pilha" não acontece. Threads diferentes têm suas pilhas em endereços diferentes. Cada thread tem seu próprio ponteiro de pilha, junto com outros registradores. Uma troca de contexto salva / restaura apenas o estado arquitetônico (incluindo todos os registros), mas não a memória. Entre threads no mesmo processo, o kernel nem mesmo precisa alterar as tabelas de página.
Peter Cordes de

Respostas:

44

A maior vantagem de E / S não bloqueante ou assíncrona é que seu thread pode continuar seu trabalho em paralelo. Claro que você também pode conseguir isso usando um thread adicional. Como você declarou para melhor desempenho geral (sistema), acho que seria melhor usar E / S assíncrona e não vários threads (reduzindo assim a troca de threads).

Vejamos as possíveis implementações de um programa de servidor de rede que deve lidar com 1000 clientes conectados em paralelo:

  1. Um thread por conexão (pode estar bloqueando E / S, mas também pode ser E / S não bloqueante).
    Cada thread requer recursos de memória (também memória kernel!), Que é uma desvantagem. E cada thread adicional significa mais trabalho para o planejador.
  2. Um thread para todas as conexões.
    Isso retira carga do sistema porque temos menos threads. Mas também impede que você use todo o desempenho de sua máquina, porque você pode acabar levando um processador a 100% e deixando todos os outros processadores ociosos.
  3. Alguns threads em que cada thread lida com algumas das conexões.
    Isso retira carga do sistema porque há menos threads. E pode usar todos os processadores disponíveis. No Windows, essa abordagem é suportada pela API Thread Pool .

É claro que ter mais threads não é um problema per se. Como você deve ter percebido, escolhi um número bastante alto de conexões / threads. Duvido que você veja qualquer diferença entre as três implementações possíveis se estivermos falando sobre apenas uma dúzia de threads (isso também é o que Raymond Chen sugere na postagem do blog do MSDN O Windows tem um limite de 2.000 threads por processo? ).

No Windows, o uso de E / S de arquivo sem buffer significa que as gravações devem ter um tamanho múltiplo do tamanho da página. Eu não testei, mas parece que isso também pode afetar o desempenho de gravação positivamente para gravações síncronas e assíncronas em buffer.

As etapas de 1 a 7 que você descreve dão uma boa ideia de como isso funciona. No Windows, o sistema operacional irá informá-lo sobre a conclusão de uma E / S assíncrona ( WriteFilecom OVERLAPPEDestrutura) usando um evento ou um retorno de chamada. As funções de retorno de chamada só serão chamadas, por exemplo, quando seu código chamar WaitForMultipleObjectsExcom bAlertabledefinido como true.

Mais algumas leituras na web:

Werner Henze
fonte
Do ponto de vista da web, o conhecimento comum (Internet, comentários de especialistas) sugere que o aumento do max. número de threads de solicitação é uma coisa ruim no bloqueio de E / S (tornando o processamento de solicitações ainda mais lento) devido ao aumento de memória e tempo de troca de contexto, mas o Async IO não está fazendo a mesma coisa ao adiar o trabalho para outro thread? Sim, você pode atender a mais solicitações agora, mas tem o mesmo número de threads em segundo plano. Qual é o real benefício disso?
JavierJ
1
@JavierJ Você parece acreditar que se n threads fizerem IO de arquivo assíncrono, outras n threads serão criadas para fazer um arquivo de bloqueio IO? Isso não é verdade. O sistema operacional tem suporte para IO de arquivo assíncrono e não precisa ser bloqueado ao aguardar a conclusão do IO. Ele pode enfileirar solicitações de E / S e, se ocorrer uma interrupção de hardware (por exemplo, DMA), pode marcar a solicitação como concluída e definir um evento que sinaliza o thread do chamador. Mesmo se um encadeamento extra fosse necessário, o sistema operacional seria capaz de usar esse encadeamento para várias solicitações de IO de vários encadeamentos.
Werner Henze
Obrigado, faz sentido envolver o suporte IO do arquivo assíncrono do sistema operacional, mas quando escrevo o código para uma implementação real disso (do ponto de vista da web), digamos com o Java Servlet 3.0 NIO, ainda vejo um thread para a solicitação e um thread de segundo plano ( assíncrono) para ler um arquivo, banco de dados ou qualquer outro.
JavierJ
1
@piyushGoyal Eu refiz minha resposta. Espero que esteja mais claro agora.
Werner Henze
1
No Windows, o uso de E / S de arquivo assíncrono significa que as gravações devem ter um tamanho múltiplo do tamanho da página. - não, não faz. Você está pensando em E / S sem buffer. (Eles são freqüentemente usados ​​juntos, mas não precisam ser.)
Harry Johnston
29

E / S inclui vários tipos de operações, como leitura e gravação de dados de discos rígidos, acesso a recursos de rede, chamada de serviços da web ou recuperação de dados de bancos de dados. Dependendo da plataforma e do tipo de operação, a E / S assíncrona geralmente tirará proveito de qualquer hardware ou suporte de sistema de baixo nível para realizar a operação. Isso significa que será executado com o menor impacto possível na CPU.

No nível do aplicativo, a E / S assíncrona evita que os threads tenham que esperar a conclusão das operações de E / S. Assim que uma operação de E / S assíncrona é iniciada, ela libera o encadeamento no qual foi iniciada e um retorno de chamada é registrado. Quando a operação é concluída, o retorno de chamada é enfileirado para execução no primeiro encadeamento disponível.

Se a operação de E / S for executada de forma síncrona, ele mantém seu thread em execução sem fazer nada até que a operação seja concluída. O tempo de execução não sabe quando a operação de E / S é concluída, portanto, ele fornecerá periodicamente algum tempo de CPU para o encadeamento em espera, tempo de CPU que poderia ser usado por outros encadeamentos que têm operações reais vinculadas à CPU para executar.

Portanto, como @ user1629468 mencionou, a E / S assíncrona não oferece melhor desempenho, mas melhor escalabilidade. Isso é óbvio ao executar em contextos que têm um número limitado de threads disponíveis, como é o caso com aplicativos da web. Os aplicativos da Web geralmente usam um pool de threads a partir do qual atribuem threads a cada solicitação. Se as solicitações forem bloqueadas em operações de E / S de longa execução, há o risco de esgotar o pool da web e fazer com que o aplicativo da web congele ou demore para responder.

Uma coisa que percebi é que a E / S assíncrona não é a melhor opção ao lidar com operações de E / S muito rápidas. Nesse caso, o benefício de não manter um encadeamento ocupado enquanto espera pela conclusão da operação de E / S não é muito importante e o fato de a operação ser iniciada em um encadeamento e concluída em outro adiciona uma sobrecarga à execução geral.

Você pode ler uma pesquisa mais detalhada que fiz recentemente sobre o tópico de E / S assíncrona vs. multithreading aqui .

Florin Dumitrescu
fonte
Eu me pergunto se valeria a pena fazer uma distinção entre as operações de I / O que devem ser concluídas e coisas que podem não [por exemplo, "obter o próximo caractere que chega em uma porta serial", nos casos em que o dispositivo remoto pode ou não envie qualquer coisa]. Se uma operação de E / S deve ser concluída dentro de um tempo razoável, pode-se atrasar a limpeza dos recursos relacionados até que a operação seja concluída. No entanto, se a operação nunca for concluída, esse atraso não seria razoável.
supercat
@supercat o cenário que você está descrevendo é usado em aplicativos e bibliotecas de nível inferior. Os servidores contam com ele, pois esperam continuamente por conexões de entrada. E / S assíncrona, conforme descrito acima, não pode caber neste cenário porque é baseado no início de uma operação específica e no registro de um retorno de chamada para sua conclusão. No caso que você está descrevendo, você precisa registrar um retorno de chamada em um evento do sistema e processar todas as notificações. Você está continuamente processando a entrada em vez de realizar operações. Como disse, isso geralmente é feito em baixo nível, quase nunca em seus aplicativos.
Florin Dumitrescu
O padrão é bastante comum com aplicativos que vêm com vários tipos de hardware. As portas seriais não são tão comuns como costumavam ser, mas os chips USB que emulam portas seriais são muito populares no design de hardware especializado. Os caracteres de tais coisas são tratados no nível do aplicativo, uma vez que o sistema operacional não terá como saber que uma sequência de caracteres de entrada significa, por exemplo, uma caixa registradora foi aberta e uma notificação deve ser enviada em algum lugar.
supercat de
Não acho que a parte sobre o custo de CPU de bloqueio de IO seja precisa: quando no estado de bloqueio, um thread que acionou o bloqueio de IO é colocado em espera pelo SO e não custa períodos de CPU até que o IO seja totalmente concluído, somente após o qual o SO (notifica por interrupções) retoma o thread bloqueado. O que você descreveu (espera ocupada por longa sondagem) não é como o bloqueio de IO é implementado em quase qualquer tempo de execução / compilador.
Lifu Huang
4

O principal motivo para usar AIO é a escalabilidade. Quando visto no contexto de alguns tópicos, os benefícios não são óbvios. Mas quando o sistema é dimensionado para 1000 threads, o AIO oferece um desempenho muito melhor. A ressalva é que a biblioteca AIO não deve apresentar mais gargalos.

zona de fissura
fonte
4

Para presumir uma melhoria de velocidade devido a qualquer forma de multi-computação, você deve presumir que várias tarefas baseadas em CPU estão sendo executadas simultaneamente em vários recursos de computação (geralmente núcleos de processador) ou então que nem todas as tarefas dependem do uso simultâneo de o mesmo recurso - ou seja, algumas tarefas podem depender de um subcomponente do sistema (armazenamento em disco, por exemplo), enquanto algumas tarefas dependem de outro (receber comunicação de um dispositivo periférico) e outras ainda podem exigir o uso de núcleos de processador.

O primeiro cenário é freqüentemente conhecido como programação "paralela". O segundo cenário é frequentemente referido como programação "simultânea" ou "assíncrona", embora "simultâneo" às vezes também seja usado para se referir ao caso de meramente permitir que um sistema operacional intercale a execução de várias tarefas, independentemente de tal execução ter coloque em série ou se vários recursos puderem ser usados ​​para alcançar a execução paralela. Neste último caso, "simultâneo" geralmente se refere à maneira como a execução é escrita no programa, em vez da perspectiva da simultaneidade real da execução da tarefa.

É muito fácil falar sobre tudo isso com suposições tácitas. Por exemplo, alguns são rápidos em fazer uma afirmação como "E / S assíncrona será mais rápida do que E / S multiencadeada". Essa afirmação é duvidosa por vários motivos. Em primeiro lugar, pode ser o caso de que algum determinado framework de E / S assíncrono seja implementado precisamente com multi-threading, caso em que eles são um no mesmo e não faz sentido dizer que um conceito "é mais rápido que" o outro .

Em segundo lugar, mesmo no caso em que há uma implementação de thread único de uma estrutura assíncrona (como um loop de evento de thread único), você ainda deve fazer uma suposição sobre o que esse loop está fazendo. Por exemplo, uma coisa boba que você pode fazer com um loop de evento de thread único é solicitar que ele conclua de forma assíncrona duas tarefas diferentes exclusivamente relacionadas à CPU. Se você fizesse isso em uma máquina com apenas um único núcleo de processador idealizado (ignorando as otimizações de hardware modernas), executar essa tarefa "de forma assíncrona" não teria um desempenho diferente do que executá-la com dois threads gerenciados independentemente ou apenas com um único processo - - a diferença pode se resumir à troca de contexto de thread ou otimizações de agendamento do sistema operacional, mas se ambas as tarefas forem para a CPU, serão semelhantes em ambos os casos.

É útil imaginar muitos dos casos incomuns ou estúpidos que você pode encontrar.

"Assíncrono" não precisa ser simultâneo, por exemplo, como acima: você executa "de forma assíncrona" duas tarefas associadas à CPU em uma máquina com exatamente um núcleo de processador.

A execução multi-threaded não precisa ser simultânea: você gera duas threads em uma máquina com um único núcleo de processador ou pede a duas threads para adquirir qualquer outro tipo de recurso escasso (imagine, digamos, um banco de dados de rede que só pode estabelecer um conexão de cada vez). A execução dos threads pode ser intercalada, no entanto, o planejador do sistema operacional achar necessário, mas seu tempo de execução total não pode ser reduzido (e será aumentado a partir da troca de contexto do thread) em um único núcleo (ou mais geralmente, se você gerar mais threads do que o existente núcleos para executá-los ou ter mais threads pedindo um recurso do que o recurso pode sustentar). A mesma coisa também se aplica ao multiprocessamento.

Portanto, nem E / S assíncrona nem multi-threading oferecem qualquer ganho de desempenho em termos de tempo de execução. Eles podem até mesmo desacelerar as coisas.

Se você definir um caso de uso específico, no entanto, como um programa específico que faz uma chamada de rede para recuperar dados de um recurso conectado à rede, como um banco de dados remoto, e também faz alguns cálculos ligados à CPU local, você pode começar a raciocinar sobre as diferenças de desempenho entre os dois métodos, considerando uma suposição particular sobre o hardware.

As perguntas a serem feitas: Quantas etapas computacionais eu preciso executar e quantos sistemas independentes de recursos existem para executá-las? Existem subconjuntos das etapas computacionais que requerem o uso de subcomponentes de sistema independentes e podem se beneficiar ao fazer isso simultaneamente? Quantos núcleos de processador eu tenho e qual é a sobrecarga de usar vários processadores ou threads para concluir tarefas em núcleos separados?

Se suas tarefas dependem amplamente de subsistemas independentes, uma solução assíncrona pode ser boa. Se o número de threads necessários para lidar com isso fosse grande, de forma que a troca de contexto se tornasse não trivial para o sistema operacional, uma solução assíncrona de thread único seria melhor.

Sempre que as tarefas são vinculadas ao mesmo recurso (por exemplo, várias necessidades para acessar simultaneamente a mesma rede ou recurso local), o multi-threading provavelmente irá introduzir sobrecarga insatisfatória, e enquanto a assincronia de thread único pode introduzir menos sobrecarga, em tal recurso- situação limitada, também não pode produzir uma aceleração. Nesse caso, a única opção (se você quiser uma aceleração) é fazer várias cópias desse recurso disponíveis (por exemplo, vários núcleos de processador se o recurso escasso for CPU; um banco de dados melhor que suporte mais conexões simultâneas se o recurso escasso é um banco de dados com conexão limitada, etc.).

Outra maneira de colocar isso é: permitir que o sistema operacional intercale o uso de um único recurso para duas tarefas não pode ser mais rápido do que simplesmente deixar uma tarefa usar o recurso enquanto a outra espera, então deixar a segunda tarefa terminar em série. Além disso, o custo do programador de intercalação significa que, em qualquer situação real, ele realmente cria uma desaceleração. Não importa se o uso intercalado ocorre da CPU, um recurso de rede, um recurso de memória, um dispositivo periférico ou qualquer outro recurso do sistema.

ely
fonte
2

Uma possível implementação de E / S sem bloqueio é exatamente o que você disse, com um pool de threads de segundo plano que bloqueiam E / S e notificam a thread do originador da E / S por meio de algum mecanismo de retorno de chamada. Na verdade, é assim que funciona o módulo AIO na glibc. Aqui estão alguns detalhes vagos sobre a implementação.

Embora esta seja uma boa solução que é bastante portátil (contanto que você tenha threads), o sistema operacional normalmente é capaz de atender a E / S sem bloqueio com mais eficiência. Este artigo da Wikipedia lista as possíveis implementações além do pool de threads.

Miguel
fonte
2

Atualmente, estou no processo de implementação do async io em uma plataforma embarcada usando protothreads. O io sem bloqueio faz a diferença entre rodar a 16000fps e 160fps. O maior benefício de não bloquear o io é que você pode estruturar seu código para fazer outras coisas enquanto o hardware faz seu trabalho. Mesmo a inicialização de dispositivos pode ser feita em paralelo.

Martin

user2826084
fonte
1

No Node, vários threads estão sendo iniciados, mas é uma camada inferior no tempo de execução do C ++.

"Sim, o NodeJS é de thread único, mas isso é meia verdade, na verdade ele é orientado a eventos e thread único com workers em segundo plano. O loop de evento principal é de thread único, mas a maioria dos trabalhos de E / S são executados em threads separados, porque as APIs de E / S no Node.js são assíncronas / sem bloqueio por design, para acomodar o loop de eventos. "

https://codeburst.io/how-node-js-single-thread-mechanism-work-understanding-event-loop-in-nodejs-230f7440b0ea

"Node.js não bloqueia, o que significa que todas as funções (retornos de chamada) são delegadas ao loop de eventos e são (ou podem ser) executadas por threads diferentes. Isso é tratado pelo tempo de execução do Node.js."

https://itnext.io/multi-threading-and-multi-process-in-node-js-ffa5bb5cde98 

A explicação "Node é mais rápido porque não bloqueia ..." é um pouco de marketing e essa é uma ótima pergunta. É eficiente e escalonável, mas não exatamente de thread único.

SmokestackLightning
fonte
0

A melhoria, tanto quanto eu sei é que usos Asynchronous I / O (Estou falando de MS Sistema, só para esclarecer) o assim chamado portas de conclusão de E / S . Usando a chamada assíncrona, a estrutura potencializa essa arquitetura automaticamente, e isso deve ser muito mais eficiente do que o mecanismo de threading padrão. Como experiência pessoal, posso dizer que você sentiria sensivelmente seu aplicativo mais reativo se preferisse AsyncCalls em vez de bloquear threads.

Felice Pollano
fonte
0

Deixe-me dar um contra-exemplo de que a E / S assíncrona não funciona. Estou escrevendo um proxy semelhante ao abaixo - usando boost :: asio. https://github.com/ArashPartow/proxy/blob/master/tcpproxy_server.cpp

No entanto, o cenário do meu caso é que as mensagens de entrada (do lado dos clientes) são rápidas, enquanto as mensagens de saída (para o servidor) são lentas para uma sessão, para acompanhar a velocidade de entrada ou para maximizar a taxa de transferência total do proxy, temos que usar várias sessões em uma conexão.

Portanto, essa estrutura de E / S assíncrona não funciona mais. Precisamos de um pool de threads para enviar ao servidor, atribuindo a cada thread uma sessão.

Zhidian Du
fonte