E / S assíncrona - Asynchronous I/O

Na ciência da computação, E / S assíncrona (também E / S não sequencial ) é uma forma de processamento de entrada / saída que permite que outro processamento continue antes que a transmissão termine.

As operações de entrada e saída (E / S) em um computador podem ser extremamente lentas em comparação com o processamento de dados. Um dispositivo de E / S pode incorporar dispositivos mecânicos que devem se mover fisicamente, como um disco rígido buscando uma faixa para ler ou gravar; esta é muitas vezes ordens de magnitude mais lenta do que a mudança de corrente elétrica. Por exemplo, durante uma operação de disco que leva dez milissegundos para ser executada, um processador com clock de um gigahertz poderia ter executado dez milhões de ciclos de processamento de instrução.

Uma abordagem simples para I / O seria iniciar o acesso e esperar que ele seja concluído. Mas tal abordagem (chamada de E / S síncrona ou bloqueio de E / S ) bloquearia o progresso de um programa enquanto a comunicação está em andamento, deixando os recursos do sistema ociosos. Quando um programa faz muitas operações de E / S (como um programa principalmente ou amplamente dependente da entrada do usuário ), isso significa que o processador pode gastar quase todo o seu tempo ocioso esperando a conclusão das operações de E / S.

Como alternativa, é possível iniciar a comunicação e, em seguida, executar o processamento que não exige que a E / S seja concluída. Essa abordagem é chamada de entrada / saída assíncrona. Qualquer tarefa que dependa da conclusão da E / S (isso inclui o uso de valores de entrada e operações críticas que afirmam garantir que uma operação de gravação foi concluída) ainda precisa aguardar a conclusão da operação de E / S e, portanto, é ainda está bloqueado, mas outro processamento que não depende da operação de E / S pode continuar.

Existem muitas funções do sistema operacional para implementar E / S assíncronas em vários níveis. Na verdade, uma das funções principais de todos os sistemas operacionais, exceto o mais rudimentar , é realizar pelo menos alguma forma de E / S assíncrona básica, embora isso possa não ser particularmente aparente para o usuário ou o programador. Na solução de software mais simples, o status do dispositivo de hardware é pesquisado em intervalos para detectar se o dispositivo está pronto para sua próxima operação. (Por exemplo, o sistema operacional CP / M foi construído dessa forma. Sua semântica de chamada de sistema não exigia nenhuma estrutura de E / S mais elaborada do que essa, embora a maioria das implementações fosse mais complexa e, portanto, mais eficiente.) Acesso direto à memória (DMA ) pode aumentar muito a eficiência de um sistema baseado em sondagem e interrupções de hardware podem eliminar a necessidade de sondagem totalmente. Os sistemas operacionais multitarefa podem explorar a funcionalidade fornecida pelas interrupções de hardware, ao mesmo tempo que escondem do usuário a complexidade do tratamento de interrupções. O spool foi uma das primeiras formas de multitarefa projetada para explorar a E / S assíncrona. Finalmente, as APIs de E / S assíncronas e multithreading explícitas nos processos do usuário podem explorar ainda mais as E / S assíncronas, ao custo de complexidade extra de software.

A E / S assíncrona é usada para melhorar o rendimento, latência e / ou capacidade de resposta.

Formulários

Formas de I / O e exemplos de funções POSIX:

Bloqueando Sem bloqueio Assíncrono
API escrever ler escrever, ler + pesquisar / selecionar aio_write, aio_read

Todas as formas de E / S assíncronas abrem aplicativos até conflitos de recursos em potencial e falhas associadas. Uma programação cuidadosa (geralmente usando exclusão mútua , semáforos , etc.) é necessária para evitar isso.

Ao expor E / S assíncronas para aplicativos, existem algumas classes amplas de implementação. A forma da API fornecida ao aplicativo não corresponde necessariamente ao mecanismo realmente fornecido pelo sistema operacional; emulações são possíveis. Além disso, mais de um método pode ser usado por uma única aplicação, dependendo de suas necessidades e dos desejos de seu (s) programador (es). Muitos sistemas operacionais fornecem mais de um desses mecanismos, é possível que alguns possam fornecer todos eles.

Processo

Disponível no início do Unix. Em um sistema operacional multitarefa , o processamento pode ser distribuído entre diferentes processos, que são executados de forma independente, têm sua própria memória e processam seus próprios fluxos de E / S; esses fluxos são normalmente conectados em pipelines . Os processos são bastante caros para criar e manter, portanto, essa solução só funciona bem se o conjunto de processos for pequeno e relativamente estável. Ele também assume que os processos individuais podem operar independentemente, além de processar a E / S uns dos outros; se eles precisam se comunicar de outras maneiras, coordená-los pode se tornar difícil.

Uma extensão dessa abordagem é a programação de fluxo de dados , que permite redes mais complicadas do que apenas as cadeias que os tubos suportam.

Polling

Variações:

  • Erro se ainda não puder ser feito (reeditar mais tarde)
  • Informe quando puder ser feito sem bloqueio (em seguida, emita)

A pesquisa fornece API síncrona sem bloqueio que pode ser usada para implementar alguma API assíncrona. Disponível em Unix e Windows tradicionais . Seu maior problema é que ele pode desperdiçar tempo de CPU em polling repetidamente quando não há mais nada para o processo de emissão fazer, reduzindo o tempo disponível para outros processos. Além disso, como um aplicativo de sondagem é essencialmente de thread único, ele pode não ser capaz de explorar totalmente o paralelismo de E / S do qual o hardware é capaz.

Selecione (/ poll) loops

Disponível em BSD Unix , e quase qualquer outra coisa com uma pilha de protocolo TCP / IP que utiliza ou é modelado após a implementação BSD. Uma variação do tema da votação, um loop de seleção usa a selectchamada do sistema para dormir até que uma condição ocorra em um descritor de arquivo (por exemplo, quando os dados estão disponíveis para leitura), ocorre um tempo limite ou um sinal é recebido (por exemplo, quando um processo filho morre). Examinando os parâmetros de retorno da selectchamada, o loop descobre qual descritor de arquivo foi alterado e executa o código apropriado. Freqüentemente, para facilidade de uso, o loop select é implementado como um loop de evento , talvez usando funções de retorno de chamada ; a situação se presta particularmente bem à programação orientada a eventos .

Embora esse método seja confiável e relativamente eficiente, ele depende muito do paradigma Unix de que " tudo é um arquivo "; qualquer bloqueio de E / S que não envolva um descritor de arquivo bloqueará o processo. O loop de seleção também depende da capacidade de envolver todas as E / S na selectchamada central ; bibliotecas que conduzem seu próprio I / O são particularmente problemáticas a esse respeito. Um problema potencial adicional é que as operações select e I / O ainda estão suficientemente desacopladas de modo que o resultado do select pode efetivamente ser uma mentira: se dois processos estão lendo a partir de um único descritor de arquivo (possivelmente com design ruim), o select pode indicar a disponibilidade de leitura dados que desapareceram no momento em que a leitura foi emitida, resultando em bloqueio; se dois processos estiverem gravando em um único descritor de arquivo (o que não é incomum), o select pode indicar capacidade de gravação imediata, mas a gravação ainda pode bloquear, porque um buffer foi preenchido por outro processo nesse ínterim, ou devido à gravação ser muito grande para o buffer disponível ou de outras formas inadequadas para o destinatário.

O loop de seleção não atinge a eficiência final do sistema possível com, digamos, o método de filas de conclusão , porque a semântica da selectchamada, permitindo o ajuste por chamada do conjunto de eventos aceitável, consome algum tempo por passagem de invocação a matriz de seleção. Isso cria pouca sobrecarga para aplicativos de usuário que podem ter aberto um descritor de arquivo para o sistema de janelas e alguns para arquivos abertos, mas se torna mais problemático à medida que o número de fontes de eventos em potencial aumenta e pode atrapalhar o desenvolvimento de aplicativos de servidor de muitos clientes , como no problema C10k ; outros métodos assíncronos podem ser visivelmente mais eficientes em tais casos. Alguns Unixes fornecem chamadas específicas do sistema com melhor escalonamento; por exemplo, epollno Linux (que preenche o array de seleção de retorno apenas com as fontes de eventos nas quais um evento ocorreu), kqueueno FreeBSD e nas portas de eventos (e /dev/poll) no Solaris .

SVR3 Unix forneceu a pollchamada de sistema. Indiscutivelmente mais bem nomeado do que select, para os propósitos desta discussão é essencialmente a mesma coisa. Os Unixes SVR4 (e, portanto, POSIX ) oferecem ambas as chamadas.

Sinais (interrupções)

Disponível em BSD e POSIX Unix. A E / S é emitida de forma assíncrona e, quando é concluída, um sinal ( interrupção ) é gerado. Como na programação do kernel de baixo nível, os recursos disponíveis para uso seguro dentro do manipulador de sinal são limitados e o fluxo principal do processo poderia ter sido interrompido em quase qualquer ponto, resultando em estruturas de dados inconsistentes conforme visto pelo manipulador de sinal. O manipulador de sinal geralmente não é capaz de emitir E / S assíncronas adicionais por si mesmo.

A abordagem de sinal , embora relativamente simples de implementar dentro do sistema operacional, traz para o programa de aplicativo a bagagem indesejável associada à gravação de um sistema de interrupção de kernel do sistema operacional. Sua pior característica é que toda chamada de sistema de bloqueio (síncrono) é potencialmente interruptível; o programador geralmente deve incorporar o código de nova tentativa em cada chamada.

Funções de retorno de chamada

Disponível no clássico Mac OS , VMS e Windows . Possui muitas das características do método de sinal , pois é fundamentalmente a mesma coisa, embora raramente seja reconhecido como tal. A diferença é que cada solicitação de E / S geralmente pode ter sua própria função de conclusão, enquanto o sistema de sinal tem um único retorno de chamada. Este método é usado por alguma estrutura de rede, incluindo Node.js , devido à facilidade de implementação e devido à ausência de suporte de linguagem necessário; no entanto, pode resultar em código aninhado e confuso, apelidado de "inferno de retorno de chamada".

Por outro lado, um problema potencial de usar retornos de chamada é que a profundidade da pilha pode aumentar de forma não gerenciável, pois uma coisa extremamente comum de se fazer quando uma E / S é concluída é agendar outra. Se isso for satisfeito imediatamente, o primeiro retorno de chamada não é 'desenrolado' da pilha antes que o próximo seja invocado. Os sistemas para evitar isso (como a programação 'intermediária' de um novo trabalho) adicionam complexidade e reduzem o desempenho. Na prática, entretanto, isso geralmente não é um problema porque a nova E / S normalmente retornará assim que a nova E / S for iniciada, permitindo que a pilha seja 'desenrolada'. O problema também pode ser evitado evitando-se quaisquer outros retornos de chamada, por meio de uma fila, até que o primeiro retorno de chamada retorne.

Processos ou threads leves

Processos leves (LWPs) ou threads estão disponíveis em Unixes mais modernos, originários do Plano 9 . Como o método do processo , mas sem o isolamento dos dados que dificulta a coordenação dos fluxos. Essa falta de isolamento apresenta seus próprios problemas, geralmente exigindo mecanismos de sincronização fornecidos pelo kernel e bibliotecas thread-safe . Cada LWP ou thread em si usa E / S síncrona de bloqueio tradicional. O requisito de pilha separada por thread pode impedir implementações em grande escala usando um grande número de threads. A separação dos fluxos textuais (código) e temporais (eventos) fornece um terreno fértil para erros.

Essa abordagem também é usada no sistema de tempo de execução da linguagem de programação Erlang . A máquina virtual Erlang usa E / S assíncrona usando um pequeno pool de apenas alguns threads ou às vezes apenas um processo, para lidar com E / S de até milhões de processos Erlang. O tratamento de E / S em cada processo é escrito principalmente usando o bloqueio de E / S síncrona. Desta forma, o alto desempenho de E / S assíncronas é mesclado com a simplicidade de E / S normais (cf modelo Ator ). Muitos problemas de E / S em Erlang são mapeados para a passagem de mensagens, que pode ser facilmente processada usando o recebimento seletivo integrado.

Fibras / corrotinas podem ser vistas como uma abordagem similarmente leve para fazer E / S assíncronas fora do sistema de tempo de execução Erlang, embora não forneçam exatamente as mesmas garantias que os processos Erlang.

Filas / portas de conclusão

Disponível em Microsoft Windows , Solaris , AmigaOS , DNIX e Linux (usando io_uring, disponível em 5.1 e superior). As solicitações de E / S são emitidas de forma assíncrona, mas as notificações de conclusão são fornecidas por meio de um mecanismo de fila de sincronização na ordem em que são concluídas. Geralmente associada a uma estruturação de máquina de estado do processo principal ( programação orientada a eventos ), que pode ter pouca semelhança com um processo que não usa E / S assíncrona ou que usa uma das outras formas, dificultando a reutilização de código. Não requer mecanismos de sincronização especiais adicionais ou bibliotecas thread-safe , nem são os fluxos textual (código) e tempo (evento) separados.

Bandeiras de eventos

Disponível em VMS e AmigaOS (geralmente usado em conjunto com uma porta de conclusão). Possui muitas das características do método da fila de conclusão , pois é essencialmente uma fila de conclusão de profundidade um. Para simular o efeito da 'profundidade' da fila, um sinalizador de evento adicional é necessário para cada evento potencial não processado (mas concluído), ou as informações do evento podem ser perdidas. Esperar pelo próximo evento disponível em tal aglomeração requer mecanismos de sincronização que podem não escalar bem para um grande número de eventos potencialmente paralelos.

Canal I / O

Disponível em mainframes pela IBM , Groupe Bull e Unisys . O canal I / O foi projetado para maximizar a utilização e o rendimento da CPU, descarregando a maior parte do I / O em um coprocessador. O coprocessador tem DMA integrado, lida com interrupções de dispositivo, é controlado pela CPU principal e só interrompe a CPU principal quando é realmente necessário. Essa arquitetura também suporta os chamados programas de canal que são executados no processador de canal para fazer trabalho pesado para atividades e protocolos de E / S.

E / S registrada

Disponível no Windows Server 2012 e Windows 8 . Otimizado para aplicativos que processam um grande número de pequenas mensagens para obter operações de E / S mais altas por segundo com jitter e latência reduzidos.

Implementação

A grande maioria do hardware de computação de uso geral depende inteiramente de dois métodos de implementação de E / S assíncrona: polling e interrupções. Normalmente, os dois métodos são usados ​​juntos, o equilíbrio depende muito do design do hardware e de suas características de desempenho exigidas. (O DMA não é em si outro método independente, é apenas um meio pelo qual mais trabalho pode ser feito por votação ou interrupção.)

Os sistemas de votação puros são inteiramente possíveis, pequenos microcontroladores (como os sistemas que usam o PIC ) geralmente são construídos dessa maneira. Os sistemas CP / M também podem ser construídos desta forma (embora raramente o sejam), com ou sem DMA. Além disso, quando o desempenho máximo é necessário para apenas algumas tarefas, às custas de quaisquer outras tarefas em potencial, a votação também pode ser apropriada, pois a sobrecarga de interromper pode ser indesejável. (Atender uma interrupção requer tempo [e espaço] para salvar pelo menos parte do estado do processador, junto com o tempo necessário para retomar a tarefa interrompida.)

A maioria dos sistemas de computação de uso geral depende fortemente de interrupções. Um sistema de interrupção pura pode ser possível, embora geralmente algum componente de pesquisa também seja necessário, pois é muito comum que várias fontes potenciais de interrupções compartilhem uma linha de sinal de interrupção comum, caso em que a pesquisa é usada dentro do driver de dispositivo para resolver o fonte real. (Este tempo de resolução também contribui para a penalidade de desempenho do sistema de interrupção. Ao longo dos anos, muito trabalho foi feito para tentar minimizar a sobrecarga associada à manutenção de uma interrupção. Os sistemas de interrupção atuais são um tanto indiferentes quando comparados a alguns sistemas anteriores altamente ajustados , mas o aumento geral no desempenho do hardware mitigou muito isso.)

Abordagens híbridas também são possíveis, em que uma interrupção pode acionar o início de alguma rajada de E / S assíncrona e a pesquisa é usada dentro da própria rajada. Essa técnica é comum em drivers de dispositivo de alta velocidade, como rede ou disco, onde o tempo perdido no retorno à tarefa de pré-interrupção é maior do que o tempo até a próxima manutenção necessária. (Hardware de E / S comum em uso hoje em dia depende fortemente de DMA e grandes buffers de dados para compensar um sistema de interrupção de desempenho relativamente baixo. Estes normalmente usam polling dentro dos loops de driver e podem apresentar uma taxa de transferência tremenda. Idealmente, por datum as pesquisas são sempre bem-sucedidas ou, no máximo, repetidas um pequeno número de vezes.)

Ao mesmo tempo, esse tipo de abordagem híbrida era comum em drivers de disco e rede onde não havia DMA ou buffer significativo disponível. Como as velocidades de transferência desejadas eram mais rápidas do que poderiam tolerar o loop mínimo de quatro operações por datum (teste de bits, ramificação condicional para si mesmo, busca e armazenamento), o hardware costumava ser construído com geração automática de estado de espera no dispositivo de E / S, enviando a pesquisa de dados prontos para fora do software e para o hardware de busca ou armazenamento do processador e reduzindo o loop programado a duas operações. (Na verdade, usando o próprio processador como um mecanismo DMA.) O processador 6502 ofereceu um meio incomum de fornecer um loop de três elementos por datum, pois tinha um pino de hardware que, quando afirmado, faria com que o bit de overflow do processador fosse definir diretamente. (Obviamente, é necessário tomar muito cuidado no design do hardware para evitar substituir o bit Overflow fora do driver do dispositivo!)

Síntese

Usando apenas essas duas ferramentas (polling e interrupções), todas as outras formas de E / S assíncronas discutidas acima podem ser (e de fato são) sintetizadas.

Em um ambiente como uma Java Virtual Machine (JVM), a E / S assíncrona pode ser sintetizada mesmo que o ambiente em que a JVM está sendo executada possa não oferecer isso. Isso se deve à natureza interpretada da JVM. A JVM pode pesquisar (ou fazer uma interrupção) periodicamente para instituir um fluxo interno de mudança de controle, efetuando o aparecimento de vários processos simultâneos, pelo menos alguns dos quais presumivelmente existem para executar E / S assíncronas. (Claro, no nível microscópico o paralelismo pode ser bastante grosso e exibir algumas características não ideais, mas na superfície parecerá ser o desejado.)

Esse, na verdade, é o problema de usar polling em qualquer forma para sintetizar uma forma diferente de E / S assíncrona. Cada ciclo de CPU que é uma pesquisa é desperdiçado e perdido para sobrecarga, em vez de realizar uma tarefa desejada. Cada ciclo de CPU que não seja uma pesquisa representa um aumento na latência de reação ao I / O pendente. Encontrar um equilíbrio aceitável entre essas duas forças opostas é difícil. (É por isso que os sistemas de interrupção de hardware foram inventados em primeiro lugar.)

O truque para maximizar a eficiência é minimizar a quantidade de trabalho que deve ser feito ao receber uma interrupção para despertar a aplicação apropriada. Secundariamente (mas talvez não menos importante) é o método que o próprio aplicativo usa para determinar o que precisa fazer.

Particularmente problemáticos (para eficiência do aplicativo) são os métodos de votação expostos, incluindo os mecanismos de seleção / votação. Embora os eventos de E / S subjacentes nos quais eles estão interessados ​​sejam, provavelmente, orientados por interrupções, a interação com esse mecanismo é pesquisada e pode consumir uma grande quantidade de tempo na pesquisa. Isso é particularmente verdadeiro no caso de pesquisas em grande escala, possíveis por meio de seleção (e pesquisa). As interrupções mapeiam muito bem para sinais, funções de retorno de chamada, filas de conclusão e sinalizadores de evento, tais sistemas podem ser muito eficientes.

Exemplos

Os exemplos a seguir mostram os conceitos de três abordagens de E / S na operação de leitura. Objetos e funções são abstratos.

1. Bloqueio, síncrono:

device = IO.open()
data = device.read() # thread will be blocked until there is data in the device
print(data)

2. Sem bloqueio, síncrono:

device = IO.open()
ready = False
while not ready:
    print("There is no data to read!")
    ready = IO.poll(device, IO.INPUT, 5) # returns control if 5 seconds have elapsed or there is data to read (INPUT)
data = device.read()
print(data)

3. Sem bloqueio, assíncrono:

ios = IO.IOService()
device = IO.open(ios)

def inputHandler(data, err):
    "Input data handler"
    if not err:
        print(data)

device.readSome(inputHandler)
ios.loop() # wait till all operations have been completed and call all appropriate handlers

Aqui está o exemplo com o padrão Reactor :

device = IO.open()
reactor = IO.Reactor()

def inputHandler(data):
    "Input data handler"
    print(data)
    reactor.stop()

reactor.addHandler(inputHandler, device, IO.INPUT)
reactor.run() # run reactor, which handles events and calls appropriate handlers

Veja também

Referências

  1. ^ Corbet, Jonathan. "Tocando em uma nova API de E / S assíncrona" . LWN.net . Página visitada em 27 de julho de 2020 .
  2. ^ "Extensões API Registered Input-Output (RIO)" . technet.microsoft.com .

links externos