Usando asyncio para criar drivers de dispositivo assíncronos no MicroPython v.1.12

Estudando as possibilidades do MicroPython para seus propósitos, deparei-me com uma das implementações da biblioteca assíncrona e, após uma breve correspondência com Piter Hinch , o autor da biblioteca, percebi que precisava entender mais profundamente os princípios, conceitos básicos e erros típicos do uso de métodos de programação assíncronos. Além disso, a seção para iniciantes é apenas para mim.

Este guia é destinado a usuários com diferentes níveis de experiência com assíncio , incluindo uma seção especial para iniciantes.

Conteúdo
0. Introdução
0.1 .___ Instalando o uasyncio em um dispositivo vazio (hardware)
1. Planejando a execução conjunta do programa
1.1 .___ Modules
2. biblioteca uasyncio
2.1 .___ Estrutura do programa: ciclo de processamento de eventos
2.2 .___ Corotinas
2.2.1 .______ Filas rotativas para participar do planejamento
2.2.2 .______ Iniciando um retorno de chamada de função ( retorno de chamada )
2.2.3 .______ Notas: corotinas como métodos relacionados. Os valores retornados.
2.3 .___ Atrasos
3. Sincronização e suas classes
3.1 .___ Bloquear Bloquear
3.1.1 .______ Bloqueios e tempos limite
3.2 .___ Evento
3.2.1 .______ Valor do evento
3.3 .___ Barreira Barreira
3.4 .___ Semáforo
3.4.1 .______ Semáforo limitado
3.5 .___ Queue Queue
3.6 .___ Outras classes de sincronização
4. Desenvolvimento de classe para assíncio
4.1 .___ Classes usando aguardar
4.1.1 .______ Uso em gerenciadores de contexto
4.1.2 .______ Aguardar na corotina
4.2 .___ Iteradores assíncronos
4.3 .___ Gerenciadores de contexto assíncrono
5. Exceções a tempos limite e devido a cancelamentos de tarefas
5.1 .___ Exceções
5.2 .___ Exceções devido a tempos limite e devido ao cancelamento de tarefas
5.2.1 .______ Cancelar tarefas
5.2.2 .______ Corotinas com tempo limite
6. Interação com dispositivos de hardware
6.1 .___ Problemas de sincronização
6.2 .___ Dispositivos de votação com corotinas
6.3 .___ Usando o mecanismo de streaming
6.3.1 .______ Exemplo de driver UART
6.4 .___ Desenvolvimento de driver para um dispositivo de streaming
6.5 .___ Exemplo completo: aremote.py Driver para receptor de controle remoto IR.
6.6 .___ Driver para sensor de temperatura e umidade HTU21D.
7. Dicas e truques
7.1 .___ O programa congela
7.2 .___ uasyncio salva estado
7.3 .___ Coleta de lixo
7.4 .___ Testing
7.5 .___ Erro comum. Pode ser difícil de encontrar.
7.6 .___ Programação usando soquetes ( soquetes )
7.6.1 .______ Problemas de WiFi
7.7 .___ Argumentos do construtor de loop de eventos
8. Notas para iniciantes
8.1 .___ Problema 1: loops de eventos
8.2 .___ Problema 2: métodos de bloqueio
8.3 .___ A abordagem uasyncio
8.4 .___ Planejamento no uasyncio
8.5 .___ Por que agendamento colaborativo, não baseado em encadeamento ( _thread )?
8.6 .___ Interação
8.7 .___ Pesquisa

0. Introdução

A maior parte deste documento assume alguma familiaridade com a programação assíncrona. Para iniciantes, uma introdução pode ser encontrada na seção 7.

A biblioteca uasyncio para MicroPython inclui um subconjunto da biblioteca Python assíncrona e deve ser usada em microcontroladores. Como tal, ele ocupa uma pequena quantidade de RAM e está configurado para alternar rapidamente contextos com zero de alocação de RAM.

Este documento descreve o uso do uasyncio com ênfase na criação de drivers para dispositivos de hardware.

O objetivo é projetar os drivers para que o aplicativo continue funcionando enquanto o driver aguarda uma resposta do dispositivo. Ao mesmo tempo, o aplicativo permanece sensível a outros eventos e interação do usuário.

Outra área importante de aplicação do assíncio é a programação em rede: na Internet, você encontra informações suficientes sobre esse tópico.

Observe que o MicroPython é baseado no Python 3.4 com os complementos mínimos do Python 3.5 . Exceto conforme detalhado abaixo, não há suporte para funções de versões assíncronas anteriores a 3,4. Este documento define os recursos suportados neste subconjunto.

O objetivo deste guia é apresentar um estilo de programação compatível com o CPython V3.5 e superior.

0.1 Instalar o uasyncio em um dispositivo vazio (hardware)

É recomendável usar o firmware MicroPython V1.11 ou posterior. Em muitas plataformas, a instalação não é necessária, pois o uasyncio®está compilado na montagem. Para verificar, basta digitar REPL

import uasyncio 

As instruções a seguir abrangem casos em que os módulos não estão pré-instalados. As filas e os módulos sincronizados são opcionais, mas são necessários para executar os exemplos fornecidos aqui.

Dispositivo conectado à Internet

Em um dispositivo conectado à Internet e executando o firmware V1.11 ou posterior, é possível instalar usando a versão upip integrada . Verifique se o dispositivo está conectado à sua rede:

 import upip upip.install ( 'micropython-uasyncio' ) upip.install ( 'micropython-uasyncio.synchro' ) upip.install ( 'micropython-uasyncio.queues' ) 

As mensagens de erro do upip não são muito úteis. Se você receber um erro incompreensível, verifique a conexão com a Internet novamente.

Hardware sem conexão à Internet ( micropip )

Se o dispositivo não tiver uma conexão com a Internet (por exemplo, Pyboard V1.x ), a maneira mais fácil é iniciar a instalação do micropip.py no computador para o diretório de sua escolha e copiar a estrutura de diretórios resultante para o dispositivo de destino. O utilitário micropip.py é executado no Python 3.2 ou posterior e no Linux, Windows e OSX. Mais informações podem ser encontradas aqui .

Chamada típica:

 $ micropip.py install -p ~/rats micropython-uasyncio $ micropip.py install -p ~/rats micropython-uasyncio.synchro $ micropip.py install -p ~/rats micropython-uasyncio.queues 

Um dispositivo sem conexão à Internet (fonte de cópia)

Se você não usar o micropip.py , os arquivos deverão ser copiados da fonte. As instruções a seguir descrevem como copiar o número mínimo de arquivos no dispositivo de destino, bem como o caso em que o uasyncio precisa ser compactado em um assembly compilado na forma de bytecode para reduzir o espaço ocupado. Para a versão mais recente compatível com o firmware oficial, os arquivos devem ser copiados do site oficial do micropython-lib .

Clone a biblioteca no computador com o comando

 $ git clone https://github.com/micropython/micropython-lib.git 

No dispositivo de destino, crie o diretório uasyncio (opcional no diretório lib) e copie os seguintes arquivos nele:

• uasyncio / uasyncio / __ init__.py
• uasyncio.core / uasyncio / core.py
• uasyncio.synchro / uasyncio / synchro.py
• uasyncio.queues / uasyncio / queues.py


Esses módulos uasyncio podem ser compactados no bytecode colocando o diretório uasyncio e seu conteúdo na porta do diretório modules e recompilando o conteúdo.

1. Planejamento conjunto

A técnica de execução conjunta de várias tarefas é amplamente usada em sistemas embarcados, que oferece menos sobrecarga do que o agendamento de threading ( _thread ), evitando muitas armadilhas associadas a encadeamentos verdadeiramente assíncronos.

1.1 Módulos

A seguir, é apresentada uma lista de módulos que podem ser executados no dispositivo de destino.

Bibliotecas

1. asyn.py Fornece Bloqueio, Evento, Barreira, Semáforo, BoundedSemaphore, Condição, coletar primitivas de sincronização. Fornece suporte para o cancelamento de tarefas por meio das classes NamedTask e Cancellable .

2. aswitch.py Representa classes para emparelhar interruptores e botões, bem como um objeto de programa com a possibilidade de atraso repetido. Os botões são uma generalização de switches que fornecem um estado lógico e não físico, além de eventos acionados por um toque duplo e longo.

Programas de demonstração

Os dois primeiros são mais úteis, pois fornecem resultados visíveis ao acessar o hardware do Pyboard .

  1. aledflash.py Pisca quatro indicadores do Pyboard de forma assíncrona por 10 segundos. A demonstração mais simples de uasyncio . Importe-o para executar.
  2. apoll.py Driver de dispositivo para o acelerômetro Pyboard . Demonstra o uso de corotinas para consultar um dispositivo. Trabalha por 20 s. Importe-o para executar. Requer Pyboard V1.x.
  3. astests.py Programas de teste / demonstração para o módulo aswitch .
  4. asyn_demos.py Demonstrações simples de cancelamento de tarefas.
  5. roundrobin.py Demonstração de planejamento circular. Também a referência para o planejamento de desempenho.
  6. waititable.py Demonstração de uma classe com uma espera. Uma maneira de implementar um driver de dispositivo que pesquisa uma interface.
  7. chain.py Copiado da documentação do Python . Demonstração da cadeia de corotina.
  8. aqtest.py Demonstração da classe Queue da biblioteca uasyncio .
  9. aremote.py Exemplo de driver de dispositivo para o protocolo NEC IR.
  10. auart.py Demonstração do fluxo de entrada e saída através do Pyboard UART .
  11. auart_hd.py Usando o Pyboard UART para se comunicar com um dispositivo usando o protocolo half-duplex. Adequado para dispositivos, por exemplo, usando o conjunto de comandos do modem AT.
  12. iorw.py Demonstração de um dispositivo leitor / gravador usando E / S de streaming.

Programas de teste

  1. asyntest.py Testa classes de sincronização em asyn.py.
  2. testes de cancelamento de trabalho.

Utilitário

1. check_async_code.py O utilitário foi escrito em Python3 para detectar erros de codificação específicos que podem ser difíceis de encontrar. Veja a seção 7.5.

Controlar

O diretório benchmarks contém scripts para verificar e caracterizar o planejador uasyncio .


2. biblioteca uasyncio

O conceito de assíncio baseia-se na organização do planejamento para a execução conjunta de várias tarefas, que neste documento são denominadas corotinas .

2.1 Estrutura do programa: loop de eventos

Considere o seguinte exemplo:

 import uasyncio as asyncio async def bar (): count = 0, while True : count + = 1 print ( count ) await asyncio.sleep ( 1 ) #  1 loop = asyncio.get_event_loop () loop.create_task ( bar ()) #     loop.run_forever () 

A execução do programa continua até que loop.run_forever seja chamado . Neste ponto, a execução é controlada pelo planejador. A linha após loop.run_forever nunca será executada. O planejador executa o código de barras porque foi colocado na fila no planejador loop.create_task . Neste exemplo trivial, há apenas uma barra de rotina. Se houvesse outros, o agendador os executaria durante os períodos em que a barra estivesse em pausa.

A maioria dos aplicativos incorporados possui um loop de eventos contínuo. Um loop de eventos também pode ser iniciado de uma maneira que permita a conclusão usando o método do loop de eventos run_until_complete ; É usado principalmente em testes. Exemplos podem ser encontrados no módulo astests.py .

Uma instância de loop de eventos é um único objeto criado pela primeira chamada para asyncio.get_event_loop () com dois argumentos inteiros opcionais que indicam o número de corotinas nas duas filas - o início e a espera. Normalmente, os dois argumentos terão o mesmo valor, igual a pelo menos o número de corotinas executadas simultaneamente no aplicativo. Geralmente, o valor padrão de 16 é suficiente.Se valores não padrão forem usados, consulte Argumentos do construtor de loop de eventos (seção 7.7.)

Se a corotina precisar chamar o método do loop de eventos (geralmente create_task ), chamar asyncio.get_event_loop () (sem argumentos) retornará efetivamente.

2.2 Corotinas

Uma rotina é criada da seguinte maneira:

 async def foo ( delay_secs ): await asyncio.sleep ( delay_secs ) print ( 'Hello' ) 

Uma corotina pode permitir que outras corotinas sejam lançadas usando a instrução de espera . Uma corotina deve conter pelo menos uma instrução de espera . Isso faz com que a corotina seja executada antes da conclusão, antes da execução prosseguir para a próxima instrução. Considere um exemplo:

 await asyncio.sleep ( delay_secs ) await asyncio.sleep ( 0 ) 

A primeira linha faz com que o código seja pausado por um tempo de atraso, enquanto outras corotinas usam esse tempo para sua execução. Um atraso de 0 faz com que todas as rotinas em execução sejam executadas em uma ordem cíclica até a próxima linha ser executada. Veja o exemplo de roundrobin.py .

2.2.1 Fila para planejar uma rotina

  • EventLoop.create_task Argumento: Coroutine para executar. O planejador enfileira a corotina para iniciar o mais rápido possível. A chamada create_task retorna imediatamente. A corotina no argumento é especificada usando a sintaxe da chamada de função com os argumentos necessários.
  • EventLoop.run_until_complete Argumento: Coroutine para executar. O planejador enfileira a corotina para iniciar o mais rápido possível. A corotina no argumento é especificada usando a sintaxe da chamada de função com os argumentos necessários. A chamada un_until_complete retorna quando a corotina é concluída: esse método fornece uma maneira de sair do planejador.
  • aguardar Argumento: Uma rotina a ser executada, especificada usando a sintaxe da chamada de função. Inicia uma corrotina o mais rápido possível. A corotina pendente é bloqueada até que uma das corotinas esperadas seja concluída.

O acima é compatível com CPython . Métodos adicionais de uasyncio são discutidos nas Notas (Seção 2.2.3.).

2.2.2 Iniciando a função de retorno de chamada

Os retornos de chamada devem ser funções Python projetadas para serem executadas em um curto período de tempo. Isso se deve ao fato de as corotinas não conseguirem trabalhar durante toda a duração da execução dessa função.

Os seguintes métodos da classe EventLoop usam retornos de chamada:

  1. call_soon - ligue o mais rápido possível. Args: retorno de chamada retorno de chamada a executar, * args quaisquer argumentos posicionais podem ser seguidos por vírgula.
  2. call_later - chama após um atraso em segundos. Args: atraso, retorno de chamada, * args
  3. call_later_ms - liga após um atraso em ms. Args: atraso, retorno de chamada, * args .

 loop = asyncio.get_event_loop () loop.call_soon ( foo , 5 ) #    'foo'      5. loop.call_later ( 2 , foo , 5 ) #   2 . loop.call_later_ms ( 50 , foo , 5 ) #   50 . loop.run_forever () 

2.2.3 Notas

Uma corotina pode conter uma declaração de retorno com valores de retorno arbitrários. Para obter esse valor:

 result = await my_coro () 

Uma corotina pode ser limitada por métodos e deve conter pelo menos uma instrução de espera .

2.3 Atrasos

Existem duas opções para organizar atrasos nas corotinas. Para atrasos mais longos e nos casos em que a duração não precisa ser precisa, você pode usar:

 async def foo( delay_secs , delay_ms ): await asyncio.sleep ( delay_secs ) print ( 'Hello' ) await asyncio.sleep_ms ( delay_ms ) 

Durante esses atrasos, o planejador executará outras corotinas. Isso pode introduzir incerteza de tempo, uma vez que a rotina de chamada somente será iniciada quando a que estiver em execução no momento for executada. A quantidade de atraso depende do desenvolvedor do aplicativo, mas provavelmente será da ordem de dezenas ou centenas de ms; isso é discutido mais adiante na interação com dispositivos de hardware (seção 6).

Atrasos muito precisos podem ser executados usando as funções utime - sleep_ms e sleep_us . Eles são mais adequados para pequenos atrasos, pois o agendador não poderá executar outras corotinas enquanto o atraso estiver em andamento.

3.Sync

Freqüentemente, é necessário garantir a sincronização entre corotinas. Um exemplo comum é evitar as chamadas "condições de corrida" quando várias corotinas requerem simultaneamente acesso ao mesmo recurso. Um exemplo é fornecido em astests.py e é discutido na documentação . Outro perigo é o "abraço da morte", quando cada corotina aguarda a conclusão da outra.

Em aplicativos simples, a sincronização pode ser alcançada usando sinalizadores globais ou variáveis ​​relacionadas. Uma abordagem mais elegante é usar classes de sincronização. O módulo asyn.py oferece implementações “micro” das classes Event, Barrier, Semaphore e Conditios , destinadas a serem usadas apenas com asyncio . Eles não são orientados a encadeamentos e não devem ser usados ​​com o módulo _thread ou com o manipulador de interrupções, a menos que especificado de outra forma. A classe Lock também é implementada, o que é uma alternativa à implementação oficial.

Outro problema de sincronização surge com os produtores e consumidores de corotina. Um produtor de corotina gera dados que um consumidor de corotina usa. Para fazer isso, o asyncio fornece a classe Queue . O produtor de corotina coloca os dados na fila, enquanto o consumidor de corotina está aguardando sua conclusão (com outras operações agendadas no prazo). A classe Queue fornece garantias para remover itens na ordem em que foram recebidos. Como alternativa, você pode usar a classe Barrier se a corotina produtora precisar esperar até que a corotina consumidora esteja pronta para acessar os dados.

Uma breve visão geral das aulas é dada abaixo. Mais detalhes na documentação completa .

3.1 Bloqueio

O bloqueio garante acesso exclusivo a um recurso compartilhado. O exemplo de código a seguir cria uma instância da classe Lock que é passada a todos os clientes que desejam acessar o recurso compartilhado. Cada corotina tenta capturar o bloqueio, interrompendo a execução até que seja bem-sucedido:

 import uasyncio as asyncio from uasyncio.synchro import Lock async def task(i, lock): while 1: await lock.acquire() print("Acquired lock in task", i) await asyncio.sleep(0.5) lock.release() async def killer(): await asyncio.sleep(10) loop = asyncio.get_event_loop() lock = Lock() # The global Lock instance loop.create_task(task(1, lock)) loop.create_task(task(2, lock)) loop.create_task(task(3, lock)) loop.run_until_complete(killer()) #  10s 

3.1.1. Bloqueio e tempos limite

No momento da redação deste artigo (5 de janeiro de 2018), o desenvolvimento da classe uasycio Lock ainda não estava oficialmente concluído. Se a corotina tiver um tempo limite (seção 5.2.2.) , Ao aguardar um bloqueio quando acionado, o tempo limite será ineficaz. Ele não receberá um TimeoutError até receber um bloqueio. O mesmo se aplica ao cancelamento de uma tarefa.

O módulo asyn.py oferece a classe Lock , que funciona nessas situações. Essa implementação da classe é menos eficiente que a classe oficial, mas suporta interfaces adicionais de acordo com a versão do CPython , incluindo o uso do gerenciador de contexto.

3.2 Evento

O evento cria uma oportunidade para uma ou várias corotinas pausarem, enquanto outra dá um sinal sobre sua continuação. Uma instância de Event fica disponível para todas as corotinas que o utilizam:

 import asyn event = asyn.Event () 

Uma corotina aguarda um evento declarando um evento de espera , após o qual a execução pausa até que outras corotinas declarem event.set () . Informação completa .

Um problema pode surgir se event.set () for emitido em uma construção em loop; o código deve aguardar até que todos os objetos pendentes tenham acesso ao evento antes de configurá-lo novamente. No caso em que um coro espera um evento, isso pode ser alcançado ao receber um evento coro limpando o evento:

 async def eventwait ( event ): await event event.clear() 

A corrotina que aciona o evento verifica se ele foi atendido:

 async def foo ( event ): while True : #   - while event.is_set (): await asyncio.sleep ( 1 ) # ,  coro   event.set () 

No caso de vários coros aguardarem a sincronização de um evento, o problema pode ser resolvido usando o evento de confirmação. Cada coro precisa de um evento separado.

 async def eventwait (  , ack_event ): await event ack_event.set () 

Um exemplo disso é fornecido na função event_test em asyntest.py . Isso é complicado Na maioria dos casos - mesmo com um coro em espera - a classe Barreira , apresentada abaixo, oferece uma abordagem mais simples.
Um evento também pode fornecer um meio de comunicação entre o manipulador de interrupções e o coro . O manipulador mantém o hardware e define o evento, que é verificado pelo coro já no modo normal.

3.2.1 Valores do evento

O método event.set () pode usar um valor de dados opcional de qualquer tipo. Coro , aguardando um evento, pode obtê-lo com event.value () . Observe que event.clear () será definido como None . Um uso típico disso para a configuração de coro do evento é emitir event.set (utime.ticks_ms ()) . Qualquer coro que aguarde um evento pode determinar o atraso que ocorreu, por exemplo, para compensar isso.

3.3 Barreira

Existem dois usos para a classe Barreira .

Primeiro, ele pode suspender uma corotina até que uma ou várias outras corotinas sejam concluídas.

Em segundo lugar, permite que várias corotinas se encontrem em um determinado ponto. Por exemplo, um produtor e um consumidor podem sincronizar no ponto em que o produtor possui dados e o consumidor está pronto para usá-los. No momento da execução, a Barreira pode emitir um retorno de chamada adicional antes que a barreira seja removida e todos os eventos pendentes possam continuar.

O retorno de chamada pode ser uma função ou uma rotina. Na maioria das aplicações, a função provavelmente será usada: pode ser garantida a execução antes da conclusão, antes da remoção da barreira.

Um exemplo é a função barreira_teste em asyntest.py . No trecho de código deste programa:

 import asyn def callback(text): print(text) barrier = asyn.Barrier(3, callback, ('Synch',)) async def report(): for i in range(5): print('{} '.format(i), end='') await barrier 

várias instâncias da rotina do relatório imprimem seus resultados e pausam até que outras instâncias também estejam concluídas e aguardam a barreira continuar. Neste ponto, um retorno de chamada está sendo feito. Após a conclusão, a rotina original é retomada.

3.4 Semáforo

O semáforo limita o número de corotinas que podem acessar o recurso. Pode ser usado para limitar o número de instâncias de uma determinada rotina que pode ser executada simultaneamente. Isso é feito usando um contador de acesso, que é inicializado pelo construtor e reduzido a cada vez que a corotina recebe um semáforo.

A maneira mais fácil de usá-lo em um gerenciador de contexto:

 import asyn sema = asyn.Semaphore(3) async def foo(sema): async with sema: #    

Um exemplo é a função semaphore_test em asyntest.py .

3.4.1 Semáforo ( limitado )

Funciona de maneira semelhante à classe Semaphore, exceto que, se o método de liberação fizer com que o contador de acesso exceda seu valor inicial, um ValueError será definido.

3.5 Fila

A classe Queue é mantida pelo uasycio oficial e o programa de amostra aqtest.py demonstra seu uso. A fila é criada da seguinte maneira:

 from uasyncio.queues import Queue q = Queue () 

Uma corotina típica do fabricante pode funcionar da seguinte maneira:

 async def producer(q): while True: result = await slow_process() #       await q.put(result) #  ,        

e a rotina do consumidor pode funcionar da seguinte maneira:

 async def consumer(q): while True: result = await(q.get()) # ,  q  print('Result was {}'.format(result)) 

A classe Queue fornece funcionalidade adicional significativa quando o tamanho das filas pode ser limitado e o status pode ser pesquisado. O comportamento com uma fila vazia (se o tamanho for limitado) e o comportamento com uma fila cheia podem ser controlados.A documentação sobre isso está no código.

3.6 Outras classes de sincronização

A biblioteca asyn.py fornece uma micro implementação de alguns dos outros recursos do CPython .

A classe Condição permite que uma corotina notifique outras corotinas que aguardam um recurso bloqueado. Após receber a notificação, eles terão acesso ao recurso e serão desbloqueados por vez. Uma corotina de notificação pode limitar o número de corotinas a serem notificadas.

A classe Gather permite executar uma lista de corotinas. Após a conclusão deste último, uma lista de resultados será retornada. Essa implementação "micro" usa uma sintaxe diferente. Os tempos limites podem ser aplicados a qualquer uma das corotinas.

4 Desenvolvendo classes para assíncio

No contexto do desenvolvimento de drivers de dispositivo, o objetivo é garantir que eles funcionem sem bloqueio. Um driver de corotina deve garantir que outras corotinas sejam executadas enquanto o driver aguarda o dispositivo executar operações de hardware. Por exemplo, uma tarefa aguardando a chegada de dados no UART ou um usuário pressionando um botão deve permitir que outros eventos sejam agendados até que o evento ocorra.

4.1 Classes usando aguardar espera Uma corotina

pode pausar a execução enquanto aguarda um objeto aguardável . No CPython, uma classe aguardável personalizada é criada implementando o método __await__ especialqual o gerador retorna. A classe aguardável é usada da seguinte maneira:

 import uasyncio as asyncio class Foo(): def __await__(self): for n in range(5): print('__await__ called') yield from asyncio.sleep(1) #     return 42 __iter__ = __await__ # .   async def bar(): foo = Foo() # Foo - awaitable  print('waiting for foo') res = await foo #   print('done', res) loop = asyncio.get_event_loop() loop.run_until_complete(bar()) 

Atualmente MicroPython não suporta __await__ ( edição # 2678 ) e para a solução a ser utilizada __iter__ . A cadeia __iter__ = __await__ fornece portabilidade entre CPython e MicroPython . exemplos de código, consulte as classes de evento, barreira, canceláveis, a condição em asyn.py .

4.1.1 Uso em gerenciadores de contexto

Objetos esperados podem ser usados ​​em gerenciadores de contexto síncronos ou assíncronos, fornecendo os métodos especiais necessários. Sintaxe:

 with await awaitable as a: #  'as'   #    async with awaitable as a: #    (.) #  - 

Para conseguir isso, o gerador __await__ deve retornar self . É transmitida em qualquer variável como a sentença e permite técnicas especiais. Consulte asyn.Condition e asyntest.condition_test onde a classe Condition usa aguardam e podem ser usados ​​em um gerenciador de contexto síncrono.

4.1.2 coroutine Await em

linguagem Python requer __await__ era a função do gerador. No MicroPython, os geradores e as rotinas são idênticos; portanto, a solução é usar o rendimento do coro (args) .

O objetivo deste guia é oferecer código portável para o CPython 3.5 ou posterior. No CPython, geradores e corotinas são diferentes em significado. No CPython, uma corotina possui um método especial __await__ que o gerador recupera. Isso é portátil:

 up = False #   MicroPython? try: import uasyncio as asyncio up = True #    sys.implementation.name except ImportError: import asyncio async def times_two(n): # Coro   await asyncio.sleep(1) return 2 * n class Foo(): def __await__(self): res = 1 for n in range(5): print('__await__ called') if up: # MicroPython res = yield from times_two(res) else: # CPython res = yield from times_two(res).__await__() return res __iter__ = __await__ async def bar(): foo = Foo() # foo is awaitable print('waiting for foo') res = await foo #   print('done', res) loop = asyncio.get_event_loop() loop.run_until_complete(bar()) 

Observe que __await__, yield de asyncio.sleep (1) é permitido pelo CPython . Ainda não entendo como isso é alcançado.

4.2 Iteradores

assíncronos Os iteradores assíncronos fornecem um meio de retornar uma sequência finita ou infinita de valores e podem ser usados ​​como um meio de recuperar elementos de dados sequenciais quando eles vêm de um dispositivo somente leitura. Um iterador assíncrono chama código assíncrono em seu próximo método . A classe deve atender aos seguintes requisitos:

  • Possui um método __aiter__ definido em def assíncrono e retornando um iterador assíncrono.
  • Possui um método __anext__ , que por si só é uma corrotina - isto é, definido via async def e contendo pelo menos uma instrução de espera . Para parar a iteração, ele deve gerar uma exceção StopAsyncIteration .

Os valores de série são recuperados usando assíncrono, conforme mostrado abaixo:

 class AsyncIterable: def __init__(self): self.data = (1, 2, 3, 4, 5) self.index = 0 async def __aiter__(self): return self async def __anext__(self): data = await self.fetch_data() if data: return data else: raise StopAsyncIteration async def fetch_data(self): await asyncio.sleep(0.1) #     if self.index >= len(self.data): return None x = self.data[self.index] self.index += 1 return x async def run(): ai = AsyncIterable() async for x in ai: print(x) 

4.3 Gerenciadores de contexto assíncronos

As classes podem ser projetadas para oferecer suporte a gerenciadores de contexto assíncronos que possuem procedimentos de entrada e saída que são co-programas. Um exemplo é a classe de bloqueio descrita acima. Possui a rotina __aenter__ , necessária logicamente para a operação assíncrona. Para oferecer suporte ao protocolo assíncrono do gerenciador de contexto, seu método __aexit__ também deve ser uma corotina, o que é alcançado ao incluir waitit asyncio.sleep (0) . Essas classes são acessíveis a partir de uma rotina com a seguinte sintaxe:

 async def bar ( lock ): async with lock: print ( « bar » ) 

Como é o caso dos gerenciadores de contexto regulares, é garantido que o método de saída seja chamado quando o gerenciador de contexto concluir seu trabalho, como de costume, e por meio de uma exceção. Para atingir esse objetivo, os métodos especiais __aenter__ e __aexit__ são usados ​​e devem ser definidos como corotinas que aguardam outra corotina ou objeto aguardável . Este exemplo é retirado da classe Lock :

  async def __aenter__(self): await self.acquire() # a coro    async def return self async def __aexit__(self, *args): self.release() #   await asyncio.sleep_ms(0) 

Se assíncrona com contém a proposta como com a variável , a variável é atribuído o valor retornado pelo __aenter__ .

Para garantir o comportamento correto, o firmware deve ser V1.9.10 ou posterior.

5. Exceções a tempos limite e devido ao cancelamento de tarefas

Estes tópicos estão relacionados: o uasyncio inclui cancelar tarefas e aplicar um tempo limite a uma tarefa, lançando uma exceção para a tarefa de uma maneira especial.

5.1 Exceções

Se ocorrer uma exceção em uma corotina, ela deve ser processada nessa corotina ou em uma corotina aguardando sua conclusão. Isso garante que a exceção não se aplique ao planejador. Se ocorrer uma exceção, o planejador interromperá o trabalho passando a exceção para o código iniciado pelo planejador. Portanto, para evitar que o planejador pare, a corotina iniciada com loop.create_task () deve capturar quaisquer exceções dentro.

Usar throw ou close para lançar uma exceção em uma rotina é irracional. Isso destrói o uasyncio , fazendo com que a corotina inicie e possivelmente saia quando ainda está na fila de execução.

O exemplo acima ilustra essa situação. Se permitido trabalhar até o fim, funcionará conforme o esperado.

 import uasyncio as asyncio async def foo(): await asyncio.sleep(3) print('About to throw exception.') 1/0 async def bar(): try: await foo() except ZeroDivisionError: print('foo  -   0') # ! raise #     . except KeyboardInterrupt: print('foo was interrupted by ctrl-c') #   ! raise async def shutdown(): print('Shutdown is running.') #     await asyncio.sleep(1) print('done') loop = asyncio.get_event_loop() try: loop.run_until_complete(bar()) except ZeroDivisionError: loop.run_until_complete(shutdown()) except KeyboardInterrupt: print('Keyboard interrupt at loop level.') loop.run_until_complete(shutdown()) 

No entanto, emitir uma interrupção do teclado faz com que a exceção entre no loop de eventos. Isso ocorre porque a execução do uasyncio.sleep é passada para o loop de eventos. Portanto, os aplicativos que exigem um código claro em resposta a uma interrupção do teclado devem capturar uma exceção no nível do loop de eventos.

5.2 Cancelamento e tempos limite

Como mencionado acima, essas funções funcionam lançando uma exceção para a tarefa de uma maneira especial usando o método especial MicroPython coroutine pend_throw . Como funciona depende da versão. No uasyncio oficial v.2.0, uma exceção não é processada até a próxima tarefa agendada. Isso impõe um atraso se a tarefa espera dormirentrada-saída Os tempos limite podem ir além do período nominal. A tarefa de desfazer de outras tarefas não pode determinar quando o desfazer é concluído.

Atualmente, há uma solução alternativa e duas soluções.

  • Solução alternativa : a biblioteca assíncrona fornece um meio de aguardar o cancelamento de tarefas ou grupos de tarefas. Consulte Cancelar um trabalho (seção 5.2.1.).
  • A biblioteca Paul Sokolovsky fornece o uasyncio v2.4 , mas isso requer seu firmware Pycopy .
  • Fast_io biblioteca uasyncio resolve este problema no Python (menos forma elegante) e rodando firmware oficial.

A hierarquia de exceções usada aqui é Exception-CanceledError-TimeoutError .

5.2.1 Cancelando um trabalho O

uasyncio fornece uma função de cancelamento (coro) . Isso funciona lançando uma exceção para usar a corotina pend_throw . Também funciona com corotinas aninhadas. O uso é o seguinte:

 async def foo(): while True: #  -  10 secs await asyncio.sleep(10) async def bar(loop): foo_instance = foo() #   coro loop.create_task(foo_instance) # code omitted asyncio.cancel(foo_instance) 

Se este exemplo é executado sob uasyncio v2.0 , quando o bar vai emitir cancelar isso não terá efeito até a próxima agendada foo e casos foo pode haver um atraso de até 10 segundos. Outra fonte de atraso ocorrerá se foo estiver aguardando E / S. Onde quer que o atraso ocorra, a barra não poderá determinar se o foo foi cancelado. É importante em alguns casos de uso.

Ao usar as bibliotecas Paul Sokolovsky ou fast_io, é suficiente usar sleep (0):

 async def foo(): while True: #  -  10 secs await asyncio.sleep(10) async def bar(loop): foo_instance = foo() #   coro loop.create_task(foo_instance) #    asyncio.cancel(foo_instance) await asyncio.sleep(0) #    

Isso também funcionará no uasyncio v2.0 se foo (e qualquer coroutine pendente foo ) nunca retornasse o sono e não esperasse E / S.

Comportamento que pode surpreender o descuidado ocorre quando é esperado que uma corotina executada por create_task e no modo de espera cancele . Considere este trecho:

 async def foo(): while True: #  -  10 secs await asyncio.sleep(10) async def foo_runner(foo_instance): await foo_instance print('   ') async def bar(loop): foo_instance = foo() loop.create_task(foo_runner(foo_instance)) #    asyncio.cancel(foo_instance) 

Quando foo é cancelado, ele é removido da fila do planejador; porque falta uma declaração de retorno , o procedimento de chamada foo_runner nunca é retomado. É recomendável que você sempre capture a exceção no escopo mais externo da função a ser desfeita:

 async def foo(): try: while True: await asyncio.sleep(10) await my_coro except asyncio.CancelledError: return 

Nesse caso, o my_coro não precisa capturar a exceção, pois ela será propagada para o canal de chamada e capturada lá.

NotaÉ proibido usar métodos close ou throw de corotinas quando elas são usadas fora do planejador. Isso prejudica o agendador, forçando a corotina a executar o código, mesmo que não esteja agendado. Isso pode ter consequências indesejáveis.

5.2.2 coroutines com tempos limite

Timeouts implementado usando uasyncio métodos .wait_for () e .wait_for_ms () . Eles tomam a rotina e a latência em segundos ou ms, respectivamente, como argumentos. Se o tempo limite expirar, um TimeoutError será lançado na corotina usando pend_throw. Essa exceção deve ser detectada pelo usuário ou pelo chamador. Isso é necessário pelo motivo descrito acima: se o tempo limite expirar, ele será cancelado. A menos que o erro seja capturado e retornado, a única maneira de o chamador continuar é capturar a própria exceção.

Onde a exceção foi capturada pela corotina, tive falhas pouco claras se a exceção não foi capturada no escopo externo, conforme mostrado abaixo:

 import uasyncio as asyncio async def forever(): try: print('Starting') while True: await asyncio.sleep_ms(300) print('Got here') except asyncio.TimeoutError: print('Got timeout') # And return async def foo(): await asyncio.wait_for(forever(), 5) await asyncio.sleep(2) loop = asyncio.get_event_loop() loop.run_until_complete(foo()) 

Como alternativa, você pode interceptar a função de chamada:

 import uasyncio as asyncio async def forever(): print('Starting') while True: await asyncio.sleep_ms(300) print('Got here') async def foo(): try: await asyncio.wait_for(forever(), 5) except asyncio.TimeoutError: pass print('Timeout elapsed.') await asyncio.sleep(2) loop = asyncio.get_event_loop() loop.run_until_complete(foo()) 

Nota para o Uasyncio v2.0 .

Isso não se aplica às bibliotecas Paul Sokolovsky ou fast_io .

Se a corotina iniciar, aguarde asyncio.sleep (t) , com um longo atraso t, a corotina não será reiniciada até t expirar . Se o tempo limite expirar antes que o sono termine , ocorrerá um erro de tempo limite quando a corotina for recarregada - ou seja, quando t expirar . Em tempo real e da perspectiva do chamador, sua resposta TimeoutError será atrasada.

Se isso for importante para o aplicativo, crie um longo atraso enquanto aguarda um curto no loop. Coroutineo asyn.sleep suporta isso.

6 Interação com o equipamento

A base da interação entre uasyncio e eventos assíncronos externos é a pesquisa de opinião . O hardware que requer uma resposta rápida pode usar uma interrupção. Mas a interação entre a rotina de interrupção (ISR) e a rotina do usuário será baseada em pesquisas. Por exemplo, um ISR pode chamar Event ou definir um sinalizador global, enquanto uma corrotina que aguarda um resultado pesquisa um objeto cada vez que uma solicitação é agendada.

A interrogação pode ser realizada de duas maneiras, explícita ou implícita. O último é feito usando E / S de fluxoum mecanismo, que é um sistema projetado para dispositivos de streaming como UART e soquetes . Na pesquisa explícita mais simples, o seguinte código pode consistir:

 async def poll_my_device(): global my_flag #   ISR while True: if my_flag: my_flag = False # service the device await asyncio.sleep(0) 

Em vez de um sinalizador global, você pode usar uma variável de instância da classe Event ou uma instância de uma classe que use wait . Uma pesquisa explícita é discutida abaixo.

A pesquisa implícita consiste em desenvolver um driver que funcionará como um dispositivo de E / S de streaming, como um soquete UART ou E / S de streaming , que pesquisa os dispositivos usando o sistema Python select.poll : como a pesquisa é realizada em C, é mais rápida e eficiente do que pesquisa explícita. O uso de E / S de fluxo é discutido na seção 6.3.

Devido à sua eficácia, a pesquisa implícita oferece uma vantagem aos drivers de dispositivo de E / S mais rápidos: os drivers de streaming podem ser criados para muitos dispositivos que geralmente não são considerados dispositivos de streaming. Isso é discutido em mais detalhes na seção 6.4.

6.1 Problemas de sincronização

Atualmente, pesquisas explícitas e implícitas são baseadas em planejamento cíclico. Suponha que a E / S funcione simultaneamente com N rotinas personalizadas, cada uma das quais é executada com atraso zero. Quando a E / S é servida, ela será pesquisada assim que todas as operações do usuário forem agendadas. O atraso estimado deve ser considerado ao projetar. Os canais de E / S podem exigir buffer, com o equipamento de manutenção ISR em tempo real a partir de buffers e corotinas, preenchendo ou liberando buffers em um tempo mais lento.

Também é necessário considerar a possibilidade de ir além: esse é o caso quando algo interrogado pela corotina acontece mais de uma vez antes de ser realmente planejado pela corotina.

Outra questão de tempo é a precisão da latência. Se a corotina emitir

 await asyncio.sleep_ms ( t ) #   

o planejador garante que a execução seja suspensa por pelo menos t ms. O atraso real pode ser maior que t, dependendo da carga atual do sistema. Se, nesse momento, outras corotinas aguardarem a conclusão de atrasos diferentes de zero, a próxima linha será imediatamente agendada para execução. Porém, se outras corotinas também estiverem aguardando execução (porque emitiram um atraso zero ou porque o tempo também expirou), elas podem ser agendadas para serem executadas mais cedo. Isso introduz incerteza de sincronização nas funções sleep () e sleep_ms () . O valor do pior caso para esse estouro pode ser calculado somando os valores de tempo de execução de todas essas rotinas para determinar o tempo de transmissão do pior caso para o planejador.

A versão fast_io do uasyncio , neste contexto, fornece uma maneira de garantir que a E / S de streaming seja pesquisada a cada iteração do planejador. Espera-se que o uasyncio oficial aceite as emendas relevantes em devido tempo.

6.2 Interrogando dispositivos usando corotinas

Esta é uma abordagem simples, mais adequada para dispositivos que podem ser interrogados a uma velocidade relativamente baixa. Isso ocorre principalmente porque a pesquisa com um intervalo de pesquisa curto (ou zero) pode levar ao fato de que a corotina consome mais tempo do processador do que o desejável para cair no intervalo.

O exemplo apoll.py demonstra essa abordagem consultando o acelerômetro Pyboardcom um intervalo de 100 ms. Ele executa uma filtragem simples para ignorar o ruído e imprime uma mensagem a cada dois segundos, se nenhum movimento ocorrer.

O exemplo aswitch.py fornece drivers para comutadores e dispositivos de botão.

Um exemplo de driver para um dispositivo capaz de ler e escrever é mostrado abaixo. Para facilitar o teste, o Pyboard UART 4 ​​emula um dispositivo condicional. Driver implementa a classe RecordOrientedUart, em que os dados são fornecidos em registros de tamanho variável que consistem em instâncias de bytes. O objeto adiciona um delimitador antes de enviar e armazena em buffer os dados recebidos até que um delimitador adicionado seja recebido. Esta é apenas uma demonstração e uma maneira ineficiente de usar o UART em comparação com a entrada / saída de streaming.

Para demonstrar a transferência assíncrona, assumimos que o dispositivo emulado tem um meio de verificar se a transferência está concluída e que o aplicativo exige que esperemos. Nenhuma das suposições é verdadeira neste exemplo, mas o código o falsifica aguardando asyncio.sleep (0.1) .

Para começar, não esqueça de conectar as saídas do Pyboard X1 e X2 (UART Txd e Rxd)

 import uasyncio as asyncio from pyb import UART class RecordOrientedUart(): DELIMITER = b'\0' def __init__(self): self.uart = UART(4, 9600) self.data = b'' def __iter__(self): # Not __await__ issue #2678 data = b'' while not data.endswith(self.DELIMITER): yield from asyncio.sleep(0) # ,  : while not self.uart.any(): yield from asyncio.sleep(0) # timing may mean this is never called data = b''.join((data, self.uart.read(self.uart.any()))) self.data = data async def send_record(self, data): data = b''.join((data, self.DELIMITER)) self.uart.write(data) await self._send_complete() #          #        await asyncio.sleep(0) async def _send_complete(self): await asyncio.sleep(0.1) def read_record(self): # Synchronous: await the object before calling return self.data[0:-1] # Discard delimiter async def run(): foo = RecordOrientedUart() rx_data = b'' await foo.send_record(b'A line of text.') for _ in range(20): await foo #  coros       foo rx_data = foo.read_record() print('Got: {}'.format(rx_data)) await foo.send_record(rx_data) rx_data = b'' loop = asyncio.get_event_loop() loop.run_until_complete(run()) 

6.3 Usando o mecanismo de streaming ( Stream )

O exemplo demonstra a entrada e saída simultânea em um UART do microprocessador Pyboard .

Para começar, conecte as saídas do Pyboard X1 e X2 (UART Txd e Rxd)

 import uasyncio as asyncio from pyb import UART uart = UART(4, 9600) async def sender(): swriter = asyncio.StreamWriter(uart, {}) while True: await swriter.awrite('Hello uart\n') await asyncio.sleep(2) async def receiver(): sreader = asyncio.StreamReader(uart) while True: res = await sreader.readline() print('Received', res) loop = asyncio.get_event_loop() loop.create_task(sender()) loop.create_task(receiver()) loop.run_forever() 

O código de suporte pode ser encontrado em __init__.py na biblioteca uasyncio . O mecanismo funciona porque o driver de dispositivo (escrito em C ) implementa os seguintes métodos: ioctl, read, readline e write . Seção 6.4: A criação de um driver de dispositivo de streaming revela detalhes de como esses drivers podem ser escritos em Python .

O UART pode receber dados a qualquer momento. O mecanismo de E / S de streaming verifica se há caracteres pendentes sempre que o agendador obtém controle. Quando a corotina está em execução, a rotina de interrupção armazena em buffer os caracteres recebidos; eles serão excluídos quando a corotina der lugar ao agendador. Portanto, os aplicativos UART devem ser projetados para que as corotinas minimizem o tempo entre as transferências para o planejador, a fim de evitar estouros de buffer e perda de dados. Isso pode ser aprimorado usando um buffer de leitura UART maior ou uma taxa de dados mais baixa. Como alternativa, o controle de fluxo de hardware fornecerá uma solução se a fonte de dados o suportar.

6.3.1 Exemplo de UART controlador

programa auart_hd.pyilustra um método de comunicação com um dispositivo half-duplex, como um dispositivo que responde ao conjunto de comandos do modem AT. Half duplex significa que o dispositivo nunca envia dados não solicitados: suas transferências são sempre realizadas em resposta a um comando recebido do mestre.

O dispositivo é emulado executando um teste em um Pyboard com duas conexões com fio.

O dispositivo emulado (muito simplificado) responde a qualquer comando enviando quatro linhas de dados com uma pausa entre cada uma para simular o processamento lento.

O assistente envia um comando, mas não sabe antecipadamente quantas linhas de dados serão retornadas. Ele inicia um timer de reinicialização que é reiniciado toda vez que uma linha é recebida. Quando o cronômetro expirar, presume-se que o dispositivo tenha concluído a transmissão e uma lista de linhas recebidas seja retornada.

Também é demonstrado um caso de falha do dispositivo, o que é alcançado pulando uma transmissão antes de esperar por uma resposta. Após o tempo limite, uma lista vazia é retornada. Veja os comentários do código para mais detalhes.

6.4 Desenvolvimento de fluxo contínuo (drivers de fluxo ) da unidade

de entrada de fluxo / mecanismo de saída ( fluxo de I / O ) para controlar o funcionamento da transmissão de dispositivos I / O, tais como UART e soquetes ( soquete) O mecanismo pode ser usado pelos drivers de qualquer dispositivo consultado regularmente, delegando ao agendador que usa select , consultando a disponibilidade de qualquer dispositivo na fila. Isso é mais eficiente do que executar várias operações de corotina, cada uma pesquisando o dispositivo, em parte porque select é gravada em C e também porque a corotina que realiza a pesquisa é atrasada até que o objeto pesquisado retorne um estado pronto.

Um driver de dispositivo capaz de atender ao mecanismo de entrada / saída de streaming deve preferencialmente suportar os métodos StreamReader, StreamWriter. Um dispositivo legível deve fornecer pelo menos um dos seguintes métodos. Observe que esses são métodos síncronos. O método ioctl (veja abaixo) garante que eles sejam chamados apenas quando os dados estiverem disponíveis. Os métodos devem ser retornados o mais rápido possível, usando o máximo de dados disponíveis.

readline () Retorna o máximo de caracteres possível, até qualquer caractere de nova linha. Necessário se você estiver usando StreamReader.readline ()

read (n) Retorne o máximo de caracteres possível, mas não mais que n . Necessário se estiver usando StreamReader.read () ou StreamReader.readexactly ()

O driver criado deve fornecer o seguinte método síncrono com retorno imediato:

escreva com argumentos buf, off, sz .

Onde:

buf é o buffer para gravação.
off - offset para o buffer do primeiro caractere a ser gravado.
sz - o número solicitado de caracteres para escrever.
O valor de retorno é o número de caracteres realmente gravados (talvez 1 se o dispositivo estiver lento).
O método ioctl garante que ele será chamado apenas quando o dispositivo estiver pronto para receber dados.

Todos os dispositivos devem fornecer um método ioctl que controla o equipamento para determinar seu status de disponibilidade. Um exemplo típico para um driver de leitura / gravação:

 import io MP_STREAM_POLL_RD = const(1) MP_STREAM_POLL_WR = const(4) MP_STREAM_POLL = const(3) MP_STREAM_ERROR = const(-1) class MyIO(io.IOBase): #    def ioctl(self, req, arg): # see ports/stm32/uart.c ret = MP_STREAM_ERROR if req == MP_STREAM_POLL: ret = 0 if arg & MP_STREAM_POLL_RD: if hardware_has_at_least_one_char_to_read: ret |= MP_STREAM_POLL_RD if arg & MP_STREAM_POLL_WR: if hardware_can_accept_at_least_one_write_character: ret |= MP_STREAM_POLL_WR return ret 

A seguir, é apresentada uma descrição do atraso de espera da classe MillisecTimer :

 import uasyncio as asyncio import utime import io MP_STREAM_POLL_RD = const(1) MP_STREAM_POLL = const(3) MP_STREAM_ERROR = const(-1) class MillisecTimer(io.IOBase): def __init__(self): self.end = 0 self.sreader = asyncio.StreamReader(self) def __iter__(self): await self.sreader.readline() def __call__(self, ms): self.end = utime.ticks_add(utime.ticks_ms(), ms) return self def readline(self): return b'\n' def ioctl(self, req, arg): ret = MP_STREAM_ERROR if req == MP_STREAM_POLL: ret = 0 if arg & MP_STREAM_POLL_RD: if utime.ticks_diff(utime.ticks_ms(), self.end) >= 0: ret |= MP_STREAM_POLL_RD return ret 

que pode ser usado da seguinte maneira:

 async def timer_test ( n ): timer = ms_timer.MillisecTimer () await timer ( 30 ) #  30  

Comparado ao uasyncio oficial , essa implementação não oferece nenhuma vantagem em comparação com o asyncio.sleep_ms () . O uso de fast_io fornece atrasos significativamente mais precisos no padrão de uso normal, quando as corotinas esperam um atraso zero.

Você pode usar o agendamento de E / S para associar um evento a um retorno de chamada. Isso é mais eficiente que o ciclo de pesquisa, porque a pesquisa não é agendada até que o ioctl retorne pronto. Em seguida, um retorno de chamada é executado quando o retorno de chamada muda de estado.

 import uasyncio as asyncio import io MP_STREAM_POLL_RD = const(1) MP_STREAM_POLL = const(3) MP_STREAM_ERROR = const(-1) class PinCall(io.IOBase): def __init__(self, pin, *, cb_rise=None, cbr_args=(), cb_fall=None, cbf_args=()): self.pin = pin self.cb_rise = cb_rise self.cbr_args = cbr_args self.cb_fall = cb_fall self.cbf_args = cbf_args self.pinval = pin.value() self.sreader = asyncio.StreamReader(self) loop = asyncio.get_event_loop() loop.create_task(self.run()) async def run(self): while True: await self.sreader.read(1) def read(self, _): v = self.pinval if v and self.cb_rise is not None: self.cb_rise(*self.cbr_args) return b'\n' if not v and self.cb_fall is not None: self.cb_fall(*self.cbf_args) return b'\n' def ioctl(self, req, arg): ret = MP_STREAM_ERROR if req == MP_STREAM_POLL: ret = 0 if arg & MP_STREAM_POLL_RD: v = self.pin.value() if v != self.pinval: self.pinval = v ret = MP_STREAM_POLL_RD return ret 

E novamente - no uasyncio oficial , o atraso pode ser alto. Dependendo do design do aplicativo, a versão fast_io pode ser mais eficiente.

A demonstração do iorw.py ilustra um exemplo completo. Observe que, no momento da redação do artigo no uasyncio oficial, há um erro devido ao qual isso não funciona . Existem duas soluções. A solução alternativa é gravar dois drivers separados, um para somente leitura e outro para somente gravação. O segundo é usar o fast_io , que resolve esse problema.

No uasyncio oficial , a entrada / saída é planejada muito raramente .

6.5 Exemplo completo: aremote.py

O driver foi projetado para receber / decodificar sinais de um controle remoto infravermelho. O próprio driver aremote.py . As notas a seguir são significativas em relação ao uso de assíncio .

A interrupção no contato registra a hora da alteração de estado (em microssegundos) e define o evento, ignorando a hora em que a primeira alteração de estado ocorreu. A corotina aguarda um evento, relata a duração do pacote de dados e decodifica os dados armazenados antes de chamar o retorno de chamada especificado pelo usuário.

A passagem do tempo para uma instância de Evento permite que a corotina compense qualqueratraso assíncrono ao definir o período de atraso.

6.6 sensor ambiental HTU21D

O driver de chip HTU21D fornece medições precisas de temperatura e umidade.

O chip precisa de cerca de 120 ms para receber os dois elementos de dados. O driver trabalha de forma assíncrona, iniciando o recebimento e o uso de wait asyncio.sleep (t) antes de ler os dados, atualiza as variáveis ​​de temperatura e umidade, que podem ser acessadas a qualquer momento, o que permite que outras corotinas sejam ativadas enquanto o driver do chip estiver em execução.

7.Dicas e truques

7.1 O programa congela O congelamento

geralmente ocorre porque a tarefa é bloqueada sem concessão: isso levará ao congelamento de todo o sistema. Ao desenvolver, é útil ter uma rotina rotativa que acenda periodicamente o LED embutido. Isso fornece confirmação de que o planejador ainda está em execução.

7.2 uasyncio salva estado

Ao iniciar programas usando o uasyncio no REPL, execute uma reinicialização suave (ctrl-D) entre as partidas. Devido ao fato de o uasyncio manter o estado entre as partidas, um comportamento imprevisível pode ocorrer na próxima partida.

7.3 Coleta de lixo

Você pode executar uma corotina especificando import gc primeiro :

 gc.collect () gc.treshold ( gc.mem_free () // 4 + gc.mem_alloc ()) 

O objetivo disso é discutido aqui na seção heap.

7.4 Teste

É recomendável garantir que o driver do dispositivo mantenha o controle quando necessário, o que pode ser feito executando uma ou mais cópias de corotinas fictícias que iniciam o ciclo de impressão de mensagens e verificando se ele é executado durante períodos em que o driver está no modo de espera:

 async def rr(n): while True: print('Roundrobin ', n) await asyncio.sleep(0) 

Como um exemplo do tipo de perigo que pode surgir, no exemplo acima, o método RecordOrientedUart __await__ foi originalmente escrito como:

 def __await__(self): data = b'' while not data.endswith(self.DELIMITER): while not self.uart.any(): yield from asyncio.sleep(0) data = b''.join((data, self.uart.read(self.uart.any()))) self.data = data 

Como resultado, a execução é esticada até que todo o registro seja recebido, bem como o fato de que uart.any () sempre retorna um número diferente de zero de caracteres recebidos. No momento da chamada, todos os caracteres já podem ter sido recebidos. Esta situação pode ser resolvida usando um loop externo:

 def __await__(self): data = b'' while not data.endswith(self.DELIMITER): yield from asyncio.sleep(0) # ,  : while not self.uart.any(): yield from asyncio.sleep(0) #        data = b''.join((data, self.uart.read(self.uart.any()))) self.data = data 

Vale a pena notar que esse erro não seria óbvio se os dados tivessem sido enviados para o UART em uma velocidade mais baixa, em vez de usar um teste de feedback. Bem-vindo às alegrias da programação em tempo real.

7.5 Erro comum

Se uma função ou método é definido por def assíncrono e subsequentemente chamado como se fosse uma chamada regular (síncrona), o MicroPython não exibe uma mensagem de erro. Isso é por design. Geralmente, isso leva ao fato de que o programa silenciosamente não funciona corretamente:

 async def foo(): # code loop.create_task(foo) #  1 1: foo     foo() #  2: . 

Eu tenho uma sugestão que sugere corrigir a situação na opção 1 usando fast_io .

O módulo check_async_code.py tenta detectar casos de uso duvidoso de corotinas. Está escrito em Python3 e foi projetado para funcionar em um PC. Utilizado em scripts escritos de acordo com as diretrizes descritas neste guia com corotinas declaradas usando async def . O módulo usa um argumento, o caminho para o arquivo MicroPython de origem (ou --help).

Observe que é um tanto rude e se destina a ser usado em um arquivo sintaticamente correto, que não inicia por padrão. Use uma ferramenta, como o pylint, para verificar a sintaxe geral (o pylint atualmente não possui esse erro).

O script produz falsos positivos. De acordo com o plano, as corotinas são objetos de primeiro nível, podem ser transferidas para funções e armazenadas em estruturas de dados. Dependendo da lógica do programa, você pode salvar a função ou o resultado de sua execução. O script não pode determinar a intenção. O objetivo é ignorar os casos que parecem corretos ao identificar outros casos a serem considerados. Suponha foo onde a corotina é declarada como def assíncrona :

 loop.run_until_complete(foo()) #   bar(foo) #     ,      bar(foo()) z = (foo,) z = (foo(),) foo() #  :   . 

Acho útil, mas as melhorias são sempre bem-vindas.

7.6 Programação com sockets ( soquetes )

Existem duas abordagens básicas para a programação soquetes uasyncio . Por padrão, os soquetes são bloqueados até que a operação de leitura ou gravação especificada seja concluída. O Uasyncio suporta o bloqueio de soquete usando o select.poll para impedir que o planejador os bloqueie. Na maioria dos casos, esse mecanismo é mais fácil de usar. Um exemplo de código do cliente e do servidor pode ser encontrado no diretório client_server . Userver usa o aplicativo select.poll pesquisando explicitamente o soquete do servidor.

Os soquetes do cliente o usam implicitamente no sentido em que o mecanismo de streaming uasyncio o usa diretamente.

Observe que o socket.getaddrinfo está atualmente bloqueado. O tempo no código de exemplo será mínimo, mas se for necessária uma pesquisa de DNS, o período de bloqueio poderá ser significativo.

Uma segunda abordagem para a programação de soquetes é usar soquetes sem bloqueio. Isso aumenta a complexidade, mas é necessário em alguns aplicativos, especialmente se a conexão for via Wi-Fi (veja abaixo).

No momento da redação deste artigo (março de 2019), o suporte ao TLS para soquetes sem bloqueio estava em desenvolvimento. Seu status exato é desconhecido (para mim).

O uso de soquetes sem bloqueio exige alguma atenção aos detalhes. Se leituras sem bloqueio ocorrerem devido à latência do servidor, não há garantia de que todos (ou alguns) dos dados solicitados serão retornados. Da mesma forma, as entradas podem não ser concluídas.

Portanto, os métodos assíncronos de leitura e gravação devem executar iterativamente uma operação sem bloqueio até que os dados necessários sejam lidos ou gravados. Na prática, pode ser necessário um tempo limite para lidar com interrupções no servidor.
Outra complicação é que a porta ESP32 teve problemas que exigiam invasões bastante desagradáveis ​​para uma operação sem erros. Não testei se esse ainda é o caso.
Módulo Sock_nonblock.pyilustra os métodos necessários. Esta não é uma demonstração funcional e é provável que as decisões sejam dependentes do aplicativo.

7.6.1 Problemas com o WiFi

O mecanismo de streaming uasyncio não é a melhor opção ao detectar falhas no WiFi. Achei necessário usar soquetes sem bloqueio para fornecer operação à prova de falhas e reconectar o cliente em caso de falhas.

Este documento descreve os problemas encontrados em aplicativos WiFi que mantêm os soquetes abertos por longos períodos e descrevem a solução.

Pltcmoferece um cliente MQTT assíncrono robusto que fornece integridade de mensagem durante falhas de WiFi. É descrito um link serial full-duplex assíncrono simples entre um cliente sem fio e um servidor com fio com entrega garantida de mensagens.

7.7 Argumentos do construtor de loop de eventos

Pode ocorrer um pequeno erro se você precisar criar um loop de eventos com valores diferentes dos valores padrão. Esse loop deve ser declarado antes de executar qualquer outro código usando assíncrono, pois esses valores podem ser necessários nesse código. Caso contrário, o código será inicializado com os valores padrão:

 import uasyncio as asyncio import some_module bar = some_module.Bar() #   get_event_loop() #     loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) 

Como a importação de um módulo pode executar código, a maneira mais segura é instanciar um loop de eventos imediatamente após a importação do uasyncio .

 import uasyncio as asyncio loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) import some_module bar = some_module.Bar() # get_event_loop()    

Ao escrever módulos para uso por outros programas, prefiro evitar a execução de código uasyncio na importação. Escreva funções e métodos para aguardar um loop de eventos como argumento. Em seguida, verifique se apenas os aplicativos de nível superior chamam get_event_loop :

 import uasyncio as asyncio import my_module #      loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) bar = my_module.Bar(loop) 

Esta questão é discutida aqui .

8 notas para iniciantes

Essas notas destinam-se a iniciantes em código assíncrono e começam com uma descrição dos problemas que os planejadores tentam resolver, além de fornecer uma visão geral da abordagem de soluções do uasyncio .

A Seção 8.5 discute os méritos relativos dos módulos uasyncio e _ thread , bem como por que você pode preferir usar as corotinas uasyncio com agendamento proativo (_thread).

8.1 Problema 1: loops de eventos

Um aplicativo típico de firmware funciona continuamente e, ao mesmo tempo, deve responder a eventos externos, que podem incluir uma alteração de voltagem no ADC, a aparência de uma interrupção de hardware ou um símbolo recebido no UART ou dados disponíveis no soquete. Esses eventos ocorrem de forma assíncrona e o código deve poder responder independentemente da ordem em que ocorrem. Além disso, podem ser necessárias tarefas dependentes do tempo, como LEDs piscando.

A maneira óbvia de fazer isso é com o loop de eventos uasycio . Este exemplo não é um código prático, mas serve para ilustrar a forma geral do loop de eventos.

 def event_loop(): led_1_time = 0 led_1_period = 20 led_2_time = 0 led_2_period = 30 switch_state = switch.state() #    while True: time_now = utime.time() if time_now >= led_1_time: #  LED #1 led1.toggle() led_1_time = time_now + led_1_period if time_now >= led_2_time: #  LED #2 led2.toggle() led_2_time = time_now + led_2_period #    LEDs if switch.value() != switch_state: switch_state = switch.value() #  - if uart.any(): #    UART 

Esse loop funciona para exemplos simples, mas à medida que o número de eventos aumenta, o código rapidamente se torna complicado. Eles também violam os princípios da programação orientada a objetos combinando a maior parte da lógica do programa em um só lugar, em vez de vincular o código a um objeto controlado. Queremos desenvolver uma classe para um LED intermitente que possa ser inserido em um módulo e importado. A abordagem OOP do LED piscando pode ser assim:

 import pyb class LED_flashable(): def __init__(self, led_no): self.led = pyb.LED(led_no) def flash(self, period): while True: self.led.toggle() # -     period, #          

O planejador no uasyncio permite criar essas classes.

8.2 Problema 2: métodos de bloqueio

Suponha que você precise ler um determinado número de bytes de um soquete. Se você chamar socket.read (n) com um soquete de bloqueio por padrão, ele "bloqueará" (ou seja, não poderá terminar) até que n bytes sejam recebidos . Durante esse período, o aplicativo não responderá a outros eventos.

Usando o soquete uasyncio sem bloqueio , você pode escrever um método de leitura assíncrono. Uma tarefa que requer dados será (necessariamente) bloqueada até que seja recebida, mas outras tarefas serão executadas durante esse período, o que permitirá que o aplicativo permaneça responsivo.

8.3 Abordagens de Uasyncio

A próxima aula tem um LED que pode ser ligado e desligado, e você também pode piscar a qualquer velocidade. A instância LED_async usa o método run , que pode ser usado para operação contínua. O comportamento dos LEDs pode ser controlado usando os métodos ligado (), desligado () e flash (segundos) .

 import pyb import uasyncio as asyncio class LED_async(): def __init__(self, led_no): self.led = pyb.LED(led_no) self.rate = 0 loop = asyncio.get_event_loop() loop.create_task(self.run()) async def run(self): while True: if self.rate <= 0: await asyncio.sleep_ms(200) else: self.led.toggle() await asyncio.sleep_ms(int(500 / self.rate)) def flash(self, rate): self.rate = rate def on(self): self.led.on() self.rate = 0 def off(self): self.led.off() self.rate = 0 

Note-se que on (), off () e flash () são métodos síncronos comuns. Eles mudam o comportamento do LED, mas retornam imediatamente. Piscando ocorre "em segundo plano". Isso é explicado em detalhes na próxima seção.

A classe está em conformidade com o princípio OOP, que consiste em armazenar a lógica associada ao dispositivo na classe. Ao mesmo tempo, o uso do uasyncio garante que o aplicativo possa responder a outros eventos enquanto o LED estiver piscando. O programa abaixo pisca com quatro LEDs do Pyboard em diferentes frequências e também responde ao botão USR, que o completa.

 import pyb import uasyncio as asyncio from led_async import LED_async # ,   async def killer(): # ,      sw = pyb.Switch() while not sw.value(): await asyncio.sleep_ms(100) leds = [LED_async(n) for n in range(1, 4)] for n, led in enumerate(leds): led.flash(0.7 + n/4) loop = asyncio.get_event_loop() loop.run_until_complete(killer()) 

Ao contrário do primeiro exemplo de um loop de eventos, a lógica associada ao comutador está em uma função separada da funcionalidade do LED. Preste atenção ao código usado para iniciar o planejador:

 loop = asyncio.get_event_loop() loop.run_until_complete(killer()) #    #       killer (), #   . 

8.4 Planejamento no uasyncio

Python 3.5 e MicroPython suportam o conceito de uma função assíncrona, também conhecida como uma rotina ou tarefa. Uma corrotina deve incluir pelo menos uma declaração de espera .

 async def hello(): for _ in range(10): print('Hello world.') await asyncio.sleep(1) 

Esta função imprime uma mensagem dez vezes em intervalos de um segundo. Enquanto a função é pausada em antecipação a um atraso, o agendador assíncrono executará outras tarefas, criando a ilusão de executá-las simultaneamente.

Quando um problema de rotineira aguarda asyncio.sleep_ms () ou asyncio.sleep (), a tarefa atual é pausada e colocada em uma fila que é ordenada por horário e a execução prossegue para a tarefa na parte superior da fila. A fila é projetada de maneira que, mesmo que o modo de suspensão especificado seja zero, outras tarefas relevantes serão executadas até que a corrente seja retomada. Esse é um planejamento "circular honesto". É prática comum executar loops aguardando asyncio.sleep (0) .para que a tarefa não atrase a execução. A seguir, um loop de espera ocupada aguardando outra tarefa para definir a variável de flag global . Infelizmente, monopoliza o processador, impedindo o lançamento de outras corotinas:

 async def bad_code(): global flag while not flag: pass #  flag = False #     

O problema aqui é que, até que o loop flag flag False passe o controle para o planejador, nenhuma outra tarefa será iniciada. A abordagem correta:

 async def good_code(): global flag while not flag: await asyncio.sleep(0) #  flag = False #     

Pelo mesmo motivo, é uma má prática definir atrasos, por exemplo, utime.sleep (1) porque bloqueia outras tarefas por 1 s; é mais correto usar o wait asyncio.sleep (1) .
Observe que os atrasos gerados pelos métodos uasyncio sleep e sleep_ms podem realmente exceder o tempo especificado. Isso se deve ao fato de que outras tarefas serão executadas durante o atraso. Após o período de atraso, a execução não será retomada até que os problemas da tarefa em execução aguardem ou sejam finalizados. Uma rotina bem comportada sempre declara aguardarem intervalos regulares. Nos casos em que é necessário um atraso exato, especialmente se houver menos de alguns ms, pode ser necessário usar utime.sleep_us (us) .

8.5 Por que agendamento colaborativo, não baseado em encadeamento ( _thread )?

A reação inicial dos iniciantes à ideia de co-planejar corotinas é muitas vezes decepcionante. Certamente o planejamento de streaming é melhor? Por que eu deveria ceder explicitamente o controle se a máquina virtual Python pode fazer isso por mim?

Quando se trata de sistemas embarcados, o modelo de colaboração tem duas vantagens.
O primeiro é leve. É possível ter um grande número de corotinas, porque, diferentemente dos encadeamentos agendados, as corotinas suspensas ocupam menos espaço.
Em segundo lugar, isso evita alguns dos problemas sutis associados ao agendamento de streaming.

Na prática, a multitarefa colaborativa é amplamente usada, especialmente em aplicativos de interface com o usuário.

Em defesa do modelo de planejamento de streaming, mostrarei uma vantagem: se alguém escrever

 for x in range ( 1000000 ): #  -  

não irá bloquear outras tarefas. O modelo de colaboração pressupõe que o loop conceda explicitamente ao controle de cada tarefa um certo número de iterações, por exemplo, colocando código em uma rotina e emitindo periodicamente aguardando asyncio.sleep (0) .

Infelizmente, essa vantagem empalidece em comparação com as desvantagens. Alguns deles são descritos na documentação para escrever manipuladores de interrupção.. Em um modelo de agendamento de streaming, cada thread pode interromper qualquer outro thread, alterando os dados que podem ser usados ​​em outros threads. Como regra, é muito mais fácil encontrar e corrigir um bloqueio que ocorre devido a um erro que não gera resultado do que a detecção de erros às vezes muito sutis e raramente encontrados, que podem ocorrer no código escrito na estrutura de um modelo com planejamento de streaming.

Simplificando, se você escrever uma corotina MicroPython , pode ter certeza de que as variáveis ​​não serão alteradas repentinamente por outra corotina: sua corotina tem controle total até retornar enquanto aguarda asyncio.sleep (0) .

Lembre-se de que os manipuladores de interrupção são preventivos. Isso se aplica às interrupções de hardware e software que podem ocorrer em qualquer lugar do seu código.

Uma discussão eloquente sobre problemas de planejamento de streaming pode ser encontrada aqui .

8.6 Interação

Em aplicações não triviais, as corotinas devem interagir. Métodos convencionais de Python podem ser usados . Isso inclui o uso de variáveis ​​globais ou a declaração de corotinas como métodos de objeto: eles podem compartilhar variáveis ​​de instância. Como alternativa, um objeto mutável pode ser passado como argumento para uma corotina.

O modelo de planejamento de streaming requer especialistas para garantir que as classes forneçam uma conexão segura; em um modelo de colaboração, isso raramente é necessário.

8.7 Poll ( polling )

Alguns dispositivo de hardware, tais como um acelerómetro Pyboard , não suportam as interrupções e, portanto, devem ser consultadas (i periodicamente verificado). A pesquisa também pode ser usada em conjunto com manipuladores de interrupção: o manipulador de interrupção mantém o equipamento e define um sinalizador. A corotina consulta o sinalizador - se estiver definido, os dados são processados ​​e o sinalizador é redefinido. A melhor abordagem é usar a classe Event .

Source: https://habr.com/ru/post/pt484472/


All Articles