Al estudiar las posibilidades de MicroPython para sus propósitos, me encontré con una de las implementaciones de la biblioteca asincio y, después de una breve correspondencia con Piter Hinch , el autor de la biblioteca, me di cuenta de que necesitaba comprender más profundamente los principios, conceptos básicos y errores típicos del uso de métodos de programación asincrónica. Además, la sección para principiantes es solo para mí.Esta guía está destinada a usuarios con diferentes niveles de experiencia con
asyncio , incluida una sección especial para principiantes.
Contenido0. Introducción0.1 .___ Instalación de
uasyncio en un dispositivo vacío (hardware)
1. Planificación para la ejecución conjunta del programa.1.1 .___ Módulos
2. biblioteca uasyncio2.1 .___ Estructura del programa: ciclo de procesamiento de eventos
2.2 .___ Corutinas
2.2.1 .______ Colas de colas
para participar en la planificación2.2.2 .______
Iniciar una devolución de llamada de función ( devolución de llamada )2.2.3 .______
Notas: corutinas como métodos relacionados. Los valores de retorno.2.3 .___ Retrasos
3. Sincronización y sus clases.3.1 .___ Bloqueo
Bloqueo3.1.1 .______
Cerraduras y tiempos de espera3.2 .___
Evento3.2.1 .______
Valor del evento
3.3 .___ Barrera
Barrera3.4 .___
Semáforo3.4.1 .______
Semáforo limitado3.5 .___ Cola
Cola3.6 .___ Otras clases de sincronización
4. Desarrollo de clase para asyncio4.1 .___ Clases usando await
4.1.1 .______
Uso en gestores de contexto4.1.2 .______
Aguardar en la rutina4.2 .___ Iteradores asincrónicos
4.3 .___ Gerentes de contexto asíncrono
5. Excepciones a los tiempos de espera y debido a cancelaciones de tareas5.1 .___ Excepciones
5.2 .___ Excepciones debido a tiempos de espera y debido a la cancelación de tareas
5.2.1 .______
Cancelar tareas5.2.2 .______
Corutinas con tiempos de espera6. Interacción con dispositivos de hardware.6.1 .___ Problemas de sincronización
6.2 .___ Dispositivos de sondeo con corutinas
6.3 .___ Uso del motor de transmisión
6.3.1 .______
Ejemplo de controlador UART6.4 .___ Desarrollo de controladores para un dispositivo de transmisión
6.5 .___ Ejemplo completo: controlador
aremote.py para receptor de control remoto IR.
6.6 .___ Controlador para sensor de temperatura y humedad HTU21D.
7. Consejos y trucos7.1 .___ El programa se congela
7.2 .___
uasyncio guarda estado
7.3 .___ Recolección de basura
7.4 .___ Pruebas
7.5 .___ Error común. Puede ser difícil de encontrar.
7.6 .___ Programación usando sockets (
sockets )
7.6.1 .______
Problemas de WiFi7.7 .___ Argumentos del constructor del bucle de eventos
8. Notas para principiantes8.1 .___ Problema 1: bucles de eventos
8.2 .___ Problema 2: métodos de bloqueo
8.3 .___ El enfoque
uasyncio8.4 .___ Planificación en
uasyncio8.5 .___ ¿Por qué la programación conjunta, no basada en subprocesos (
_thread )?
8.6 .___ Interacción
8.7 .___
Sondeo0. IntroducciónLa mayor parte de este documento supone cierta familiaridad con la programación asincrónica. Para principiantes, se puede encontrar una introducción en la sección 7.
La biblioteca
uasyncio para
MicroPython incluye un subconjunto de la biblioteca
asyncio Python y está diseñada para usarse en microcontroladores. Como tal, ocupa una pequeña cantidad de RAM y está configurado para cambiar rápidamente los contextos con cero asignación de RAM.
Este documento describe el uso de
uasyncio con énfasis en la creación de controladores para dispositivos de hardware.
El objetivo es diseñar los controladores para que la aplicación continúe funcionando mientras el controlador espera una respuesta del dispositivo. Al mismo tiempo, la aplicación sigue siendo sensible a otros eventos y a la interacción del usuario.
Otra área importante de aplicación de
asyncio es la programación de red: en Internet puede encontrar suficiente información sobre este tema.
Tenga en cuenta que
MicroPython se basa en
Python 3.4 con los complementos mínimos de
Python 3.5 . Excepto como se detalla a continuación, las funciones de
las versiones de
asyncio anteriores a 3.4 no son compatibles. Este documento define las características admitidas en este subconjunto.
El propósito de esta guía es presentar un estilo de programación compatible con
CPython V3.5 y superior.
0.1 Instalar uasyncio en un dispositivo vacío (hardware)Se recomienda utilizar el firmware
MicroPython V1.11 o posterior. En muchas plataformas, la instalación no es necesaria, ya que
uasyncio® ya
está compilado en el ensamblaje. Para verificar, simplemente escriba REPL
import uasyncio
Las siguientes instrucciones cubren casos en los que los módulos no están preinstalados. Las
colas y los módulos de
sincronización son opcionales, pero son necesarios para ejecutar los ejemplos que se proporcionan aquí.
Dispositivo conectado a internetEn un dispositivo conectado a Internet y que ejecute el firmware V1.11 o posterior, puede instalar utilizando la versión
incorporada de
upip . Asegúrese de que el dispositivo esté conectado a su red:
import upip upip.install ( 'micropython-uasyncio' ) upip.install ( 'micropython-uasyncio.synchro' ) upip.install ( 'micropython-uasyncio.queues' )
Los mensajes de error de
upip no
son muy útiles. Si obtiene un error incomprensible, verifique nuevamente la conexión a Internet.
Hardware sin conexión a internet ( micropip )Si el dispositivo no tiene una conexión a Internet (por ejemplo,
Pyboard V1.x ), la forma más fácil es comenzar la instalación de
micropip.py en la computadora en el directorio que elija y luego copiar la estructura del directorio resultante en el dispositivo de destino. La utilidad
micropip.py se ejecuta en
Python 3.2 o posterior y se ejecuta en Linux, Windows y OSX. Más información se puede encontrar
aquí .
Llamada tipica:
$ micropip.py install -p ~/rats micropython-uasyncio $ micropip.py install -p ~/rats micropython-uasyncio.synchro $ micropip.py install -p ~/rats micropython-uasyncio.queues
Un dispositivo sin conexión a Internet (fuente de copia)Si no utiliza
micropip.py , los archivos deben copiarse de la fuente. Las siguientes instrucciones describen cómo copiar el número mínimo de archivos en el dispositivo de destino, así como el caso en que
uasyncio necesita comprimirse en un ensamblaje compilado en forma de
código de bytes para reducir el espacio ocupado. Para la última versión compatible con firmware oficial, los archivos deben copiarse del sitio
web oficial de
micropython-lib .
Clone la biblioteca a la computadora con el comando
$ git clone https://github.com/micropython/micropython-lib.git
En el dispositivo de destino, cree el directorio
uasyncio (opcional en el directorio lib) y copie los siguientes archivos en él:
• uasyncio / uasyncio / __ init__.py
• uasyncio.core / uasyncio / core.py
• uasyncio.synchro / uasyncio / synchro.py
• uasyncio.queues / uasyncio / queues.py
Estos módulos
uasyncio pueden comprimirse en bytecode colocando el directorio
uasyncio y sus contenidos en el puerto del directorio de
módulos y volviendo a compilar los contenidos.
1. Planificación conjuntaLa técnica de ejecución conjunta de varias tareas se usa ampliamente en sistemas embebidos, que ofrecen menos gastos generales que la programación de subprocesos (
_thread ), evitando muchas dificultades asociadas con subprocesos verdaderamente asincrónicos.
1.1 MódulosLa siguiente es una lista de módulos que pueden ejecutarse en el dispositivo de destino.
Bibliotecas1.
asyn.py Proporciona
bloqueo, evento, barrera, semáforo, semáforo limitado, condición, primitivas de sincronización de
recopilación . Proporciona soporte para cancelar tareas a través de las clases
NamedTask y
Cancellable .
2.
aswitch.py Representa clases para emparejar interruptores y botones, así como un objeto de programa con la posibilidad de retrasos repetidos. Los botones son una generalización de interruptores que proporcionan un estado lógico en lugar de físico, así como eventos activados por una pulsación doble y prolongada.
Programas de demostraciónLos dos primeros son más útiles ya que dan resultados visibles al acceder al
hardware de Pyboard .
- aledflash.py Parpadea cuatro indicadores de Pyboard de forma asincrónica durante 10 segundos. La demostración más simple de uasyncio . Importarlo para ejecutar.
- apoll.py Controlador del dispositivo para el acelerómetro Pyboard . Demuestra el uso de corutinas para consultar un dispositivo. Funciona por 20 s. Importarlo para ejecutar. Requiere Pyboard V1.x.
- astests.py Programas de prueba / demostración para el módulo aswitch .
- asyn_demos.py Demos simples de cancelar tareas.
- roundrobin.py Demostración de planificación circular. También el punto de referencia para la planificación del rendimiento.
- awaitable.py Demostración de una clase con una espera. Una forma de implementar un controlador de dispositivo que sondea una interfaz.
- chain.py Copiado de la documentación de Python . Demostración de la cadena de rutina.
- aqtest.py Demostración de la clase Queue de la biblioteca uasyncio .
- aremote.py Ejemplo de controlador de dispositivo para el protocolo NEC IR.
- auart.py Demostración de transmisión de entrada-salida a través de Pyboard UART .
- auart_hd.py Usando Pyboard UART para comunicarse con un dispositivo usando el protocolo half-duplex. Adecuado para dispositivos, por ejemplo, que utilizan el conjunto de comandos del módem AT.
- iorw.py Demostración de un dispositivo lector / grabador utilizando E / S de transmisión.
Programas de prueba- asyntest.py Pruebas para clases de sincronización en asyn.py.
- cantest.py Pruebas de cancelación de trabajos.
Utilidad1.
check_async_code.py La utilidad está escrita en
Python3 para detectar errores de codificación específicos que pueden ser difíciles de encontrar. Ver sección 7.5.
ControlEl directorio
benchmarks contiene scripts para verificar y caracterizar el
planificador uasyncio .
2. biblioteca uasyncioEl concepto de
asincio se basa en la organización de la planificación para la ejecución conjunta de varias tareas, que en este documento se denominan
corutinas .
2.1 Estructura del programa: bucle de eventosConsidere el siguiente ejemplo:
import uasyncio as asyncio async def bar (): count = 0, while True : count + = 1 print ( count ) await asyncio.sleep ( 1 )
La ejecución del programa continúa hasta que
se llama a loop.run_forever . En este punto, la ejecución es controlada por el planificador. La línea después de
loop.run_forever nunca se ejecutará. El planificador ejecuta el código de
barras porque se ha puesto en cola en el
planificador loop.create_task . En este ejemplo trivial, solo hay una
barra corutina. Si hubiera otros, el planificador los ejecutaría durante los períodos en que la
barra estaba en pausa.
La mayoría de las aplicaciones integradas tienen un ciclo de eventos continuo. Un ciclo de eventos también se puede iniciar de una manera que permita su finalización utilizando el método de ciclo de eventos
run_until_complete ; Se utiliza principalmente en pruebas. Se pueden encontrar ejemplos en el módulo
astests.py .
Una instancia de bucle de evento es un único objeto creado por la primera llamada a
asyncio.get_event_loop () con dos argumentos enteros opcionales que indican el número de corutinas en las dos colas: el inicio y la espera. Típicamente, ambos argumentos tendrán el mismo valor, igual al menos al número de rutinas ejecutadas simultáneamente en la aplicación. Por lo general, el valor predeterminado de 16 es suficiente. Si se utilizan valores no predeterminados, consulte Argumentos del constructor del bucle de eventos (sección 7.7.).
Si la rutina necesita llamar al método de bucle de eventos (generalmente
create_task ), llamar a
asyncio.get_event_loop () (sin argumentos) lo devolverá efectivamente.
2.2 CorutinasUna corutina se crea de la siguiente manera:
async def foo ( delay_secs ): await asyncio.sleep ( delay_secs ) print ( 'Hello' )
Una corutina puede permitir que otras corridas se inicien utilizando la instrucción de
espera . Una corutina debe contener al menos una declaración de
espera . Esto hace que la rutina se ejecute antes de completarse, antes de que la ejecución avance a la siguiente instrucción. Considere un ejemplo:
await asyncio.sleep ( delay_secs ) await asyncio.sleep ( 0 )
La primera línea hace que el código se detenga por un tiempo de retraso, mientras que otras rutinas usan este tiempo para su ejecución. Un retraso de 0 hace que todas las corutinas pendientes se ejecuten en un orden cíclico hasta que se ejecute la siguiente línea. Vea el ejemplo de
roundrobin.py .
2.2.1. Cola para planificar una corutina- EventLoop.create_task Argumento: Corutina a ejecutar. El planificador pone en cola la rutina para que se inicie lo antes posible. La llamada create_task vuelve inmediatamente. La rutina en el argumento se especifica utilizando la sintaxis de la llamada a la función con los argumentos necesarios.
- EventLoop.run_until_complete Argumento: Coroutine to run. El planificador pone en cola la rutina para que se inicie lo antes posible. La rutina en el argumento se especifica utilizando la sintaxis de la llamada a la función con los argumentos necesarios. La llamada un_until_complete regresa cuando se completa la rutina: este método proporciona una forma de salir del programador.
- Aguarde Argumento: Una rutina para ejecutar, especificada utilizando la sintaxis de llamada de función. Inicia una corutina lo antes posible. La corutina pendiente se bloquea hasta que se complete una de las esperadas.
Lo anterior es compatible con
CPython .
Métodos adicionales de
uasyncio se discuten en las Notas (Sección 2.2.3.).
2.2.2 Inicio de la función de devolución de llamadaLas devoluciones de llamada deben ser funciones de
Python diseñadas para ejecutarse en un corto período de tiempo. Esto se debe al hecho de que las corutinas no podrán funcionar durante todo el tiempo que se realiza la función.
Los siguientes
métodos de la clase
EventLoop usan devoluciones de llamada:
- call_soon : llame lo antes posible. Args: devolución de llamada de devolución de llamada para ejecutar, * args cualquier argumento posicional puede ir seguido de una coma.
- call_later : llamada después de un retraso en segundos. Args: retraso, devolución de llamada, * args
- call_later_ms : llamada después de un retraso en ms. Args: retraso, devolución de llamada, * args .
loop = asyncio.get_event_loop () loop.call_soon ( foo , 5 )
2.2.3 NotasUna corutina puede contener una declaración de
retorno con valores de retorno arbitrarios. Para obtener este valor:
result = await my_coro ()
Una corutina puede estar limitada por métodos y debe contener al menos una declaración de
espera .
2.3 RetrasosHay dos opciones para organizar retrasos en las rutinas. Para retrasos más largos y en casos donde la duración no necesita ser precisa, puede usar:
async def foo( delay_secs , delay_ms ): await asyncio.sleep ( delay_secs ) print ( 'Hello' ) await asyncio.sleep_ms ( delay_ms )
Durante tales demoras, el planificador ejecutará otras corutinas. Esto puede introducir incertidumbre en el tiempo, ya que la rutina de llamada solo se iniciará cuando se ejecute la que se está ejecutando actualmente. La cantidad de retraso depende del desarrollador de la aplicación, pero probablemente será del orden de decenas o cientos de ms; esto se discute más adelante en la Interacción con dispositivos de hardware (Sección 6).
Se pueden realizar retrasos muy precisos utilizando las funciones
utime :
sleep_ms y
sleep_us . Son más adecuados para retrasos cortos, ya que el planificador no podrá ejecutar otras rutinas mientras el retraso esté en progreso.
3 sincronizaciónA menudo existe la necesidad de garantizar la sincronización entre las rutinas. Un ejemplo común es evitar las llamadas "condiciones de carrera" cuando varias corutinas requieren simultáneamente acceso al mismo recurso. Se proporciona un ejemplo en
astests.py y se describe en la
documentación . Otro peligro son los "abrazos de la muerte" cuando cada corutina espera a que se complete la otra.
En aplicaciones simples, la sincronización se puede lograr utilizando indicadores globales o variables relacionadas. Un enfoque más elegante es usar clases de sincronización. El módulo
asyn.py ofrece implementaciones "micro" de las clases
Event, Barrier, Semaphore y
Conditios , destinadas a usarse solo con
asyncio . No están orientados a subprocesos y no deben usarse con el módulo
_thread o el controlador de interrupciones, a menos que se especifique lo contrario. También se implementa la clase
Lock , que es una alternativa a la implementación oficial.
Otro problema de sincronización surge con los productores de coroutine y los consumidores de coroutine. Un productor de rutina genera datos que utiliza un consumidor de rutina. Para hacer esto,
asyncio proporciona la clase
Queue . El productor de rutina coloca los datos en la cola, mientras que el consumidor de rutina espera su finalización (con otras operaciones programadas a tiempo). La clase
Queue ofrece garantías para eliminar elementos en el orden en que fueron recibidos. Alternativamente, puede usar la clase
Barrera si la rutina del productor debe esperar hasta que la rutina del consumidor esté lista para acceder a los datos.
A continuación se ofrece una breve descripción de las clases. Más detalles en la
documentación completa .
3.1 BloqueoLock garantiza un acceso único a un recurso compartido. El siguiente ejemplo de código crea una instancia de la clase de
bloqueo Bloqueo que se pasa a todos los clientes que desean acceder al recurso compartido. Cada corutina intenta capturar el bloqueo, pausando la ejecución hasta que tenga éxito:
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()
3.1.1. Bloqueo y tiempos de esperaAl momento de escribir (5 de enero de 2018), el desarrollo de la clase
uasycio Lock no se ha completado oficialmente. Si la rutina tiene un
tiempo de espera (sección 5.2.2.) , Al esperar un bloqueo cuando se activa, el tiempo de espera será ineficaz. No recibirá un
TimeoutError hasta que reciba un bloqueo. Lo mismo se aplica a la cancelación de una tarea.
El módulo
asyn.py ofrece la clase
Lock , que funciona en estas situaciones. Esta implementación de la clase es menos eficiente que la clase oficial, pero admite interfaces adicionales de acuerdo con la versión de
CPython , incluido el uso del administrador de contexto.
3.2 EventoEl evento crea una oportunidad para que una o varias corutinas pausen, mientras que otra da una señal sobre su continuación. Una instancia de
Evento está disponible para todas las corutinas que lo usan:
import asyn event = asyn.Event ()
Una corutina espera un evento declarando un
evento en espera , después de lo cual la ejecución se detiene hasta que otras corutinas declaran
event.set () .
Información completaPuede surgir un problema si
event.set () se emite en una construcción en bucle; el código debe esperar hasta que todos los objetos pendientes tengan acceso al evento antes de configurarlo nuevamente. En el caso de que un
coro espere un evento, esto se puede lograr al recibir un evento
coro que borre el evento:
async def eventwait ( event ): await event event.clear()
La rutina que desencadena el evento verifica que se haya atendido:
async def foo ( event ): while True :
En el caso de que varios
coros estén esperando la sincronización de un evento, el problema se puede resolver utilizando el evento de confirmación. Cada
coro necesita un evento separado.
async def eventwait ( , ack_event ): await event ack_event.set ()
Un ejemplo de esto se da en la función
event_test en
asyntest.py . Esto es engorroso En la mayoría de los casos, incluso con un
coro de espera, la clase
Barrera , presentada a continuación, ofrece un enfoque más simple.
Un evento también puede proporcionar un medio de comunicación entre el controlador de interrupciones y el
coro . El controlador mantiene el hardware y establece el evento, que
Coro ya verifica en modo normal.
3.2.1 Valores de eventoEl método
event.set () puede tomar un valor de datos opcional de cualquier tipo.
Coro , esperando un evento, puede obtenerlo con
event.value () . Tenga en cuenta que
event.clear () se establecerá en
Ninguno . Un uso típico de esto para el
coro que configura el evento es emitir
event.set (utime.ticks_ms ()) . Cualquier
coro que espera un evento puede determinar la demora que ha ocurrido, por ejemplo, para compensar esto.
3.3 BarreraHay dos usos para la clase
Barrera .
Primero, puede suspender una corutina hasta que se completen una o varias otras.
En segundo lugar, permite que varias corutinas se encuentren en un punto determinado. Por ejemplo, un productor y un consumidor pueden sincronizar en el punto donde el productor tiene datos, y el consumidor está listo para usarlos. En el momento de la ejecución, la
barrera puede emitir una devolución de llamada adicional antes de eliminar la barrera y todos los eventos pendientes pueden continuar.
La devolución de llamada puede ser una función o una rutina. En la mayoría de las aplicaciones, lo más probable es que se utilice la función: se puede garantizar que se ejecute antes de completarse, antes de que se elimine la barrera.
Un ejemplo es la función
barrera_prueba en
asyntest.py . En el fragmento de código de este 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
varias instancias de la rutina del
informe imprimen su resultado y hacen una pausa hasta que otras instancias también estén completas y esperen a que continúe la
barrera . En este punto, se está realizando una devolución de llamada. Al finalizar, se reanuda la rutina original.
3.4 SemáforoEl semáforo limita el número de corutinas que pueden acceder al recurso. Se puede usar para limitar el número de instancias de una corutina particular que puede ejecutarse simultáneamente. Esto se realiza mediante un contador de acceso, que el constructor inicializa y reduce cada vez que la rutina recibe un semáforo.
La forma más fácil de usarlo en un administrador de contexto:
import asyn sema = asyn.Semaphore(3) async def foo(sema): async with sema:
Un ejemplo es la función
semaphore_test en
asyntest.py .
3.4.1 semáforo ( limitado )Funciona de manera similar a la clase
Semaphore , excepto que si el método de
liberación hace que el contador de acceso exceda su valor inicial,
se establece un
ValueError .
3.5 ColaEl
uasycio oficial mantiene la clase
Queue y el programa de ejemplo
aqtest.py demuestra su uso. La cola se crea de la siguiente manera:
from uasyncio.queues import Queue q = Queue ()
Una rutina de fabricante típica puede funcionar de la siguiente manera:
async def producer(q): while True: result = await slow_process()
y la rutina del consumidor puede funcionar de la siguiente manera:
async def consumer(q): while True: result = await(q.get())
La clase
Queue proporciona una funcionalidad adicional significativa cuando el tamaño de las colas puede ser limitado y el estado puede ser consultado. Se puede controlar el comportamiento con una cola vacía (si el tamaño es limitado) y el comportamiento con una cola completa.
La documentación sobre esto está en el código.3.6 Otras clases de sincronizaciónLa biblioteca asyn.py proporciona una micro implementación de algunas de las otras características de CPython .La clase Condición permite que una corutina notifique a otras corutinas que esperan en un recurso bloqueado. Después de recibir la notificación, obtendrán acceso al recurso y se desbloquearán a su vez. Una corutina de notificación puede limitar el número de corutinas a notificar.La clase Gather le permite ejecutar una lista de corutinas. Una vez completado este último, se devolverá una lista de resultados. Esta implementación "micro" utiliza una sintaxis diferente. Los tiempos de espera se pueden aplicar a cualquiera de las corutinas.4 Desarrollo de clases para asyncioEn el contexto del desarrollo de controladores de dispositivos, el objetivo es garantizar que funcionen sin bloqueo. Un controlador de rutina debe asegurarse de que se ejecutan otras rutinas mientras el controlador está esperando que el dispositivo realice operaciones de hardware. Por ejemplo, una tarea que espera datos que llegan a UART, o un usuario que presiona un botón debe permitir que se programen otros eventos hasta que ocurra el evento.4.1 Clases que utilizan esperar en espera Una rutinapuede pausar la ejecución mientras espera un objeto en espera . Bajo CPython, se crea una clase de espera personalizada al implementar el método especial __await__que el generador devuelve. La clase de espera se utiliza de la siguiente manera: import uasyncio as asyncio class Foo(): def __await__(self): for n in range(5): print('__await__ called') yield from asyncio.sleep(1)
Actualmente MicroPython no soporta __await__ ( edición # 2678 ) y para que la solución sea usada __iter__ . La cadena __iter__ = __await__ proporciona portabilidad entre CPython y MicroPython . Los ejemplos de código, ver las clases de eventos, de barrera, cancelables, Estado de asyn.py .4.1.1 Uso en gestores de contextoLos objetos esperados se pueden utilizar en gestores de contexto síncronos o asíncronos, proporcionando los métodos especiales necesarios. Sintaxis: with await awaitable as a:
Para lograr esto, el generador __await__ debe regresar self . Esto se pasa a cualquier variable en la cláusula as , y también permite que funcionen métodos especiales. Vea asyn.Condition y asyntest.condition_test donde la clase de condición usa wait y se puede usar en un administrador de contexto síncrono.4.1.2 corrutina esperan enel lenguaje Python requiere __await__ era la función del generador. En MicroPython, los generadores y las corutinas son idénticos, por lo que la solución es utilizar el rendimiento de coro (args) .El propósito de esta guía es ofrecer código que sea portátil para CPython 3.5 o posterior. En CPython, los generadores y las corutinas tienen un significado diferente. En CPython, una corutina tiene un método especial __await__ que el generador recupera. Esto es portátil: up = False
Nota que __await__, rendimiento a partir de asyncio.sleep (1) permitió la CPython . Todavía no entiendo cómo se logra esto.4.2 iteradoresasincrónicos Los iteradores asincrónicos proporcionan un medio para devolver una secuencia de valores finita o infinita y pueden usarse como un medio para recuperar elementos de datos secuenciales cuando provienen de un dispositivo de solo lectura. Un iterador asincrónico llama a un código asincrónico en su próximo método . La clase debe cumplir los siguientes requisitos:- Tiene un método __aiter__ definido en def asíncrono y que devuelve un iterador asíncrono.
- Tiene un método __anext__ , que en sí mismo es una rutina, es decir, se define a través de la definición asíncrona y contiene al menos una instrucción de espera . Para detener la iteración, debe generar una excepción StopAsyncIteration .
Los valores en serie se recuperan usando asíncrono como se muestra a continuación: 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)
4.3 Administradores de contexto asíncronoLas clases se pueden diseñar para admitir administradores de contexto asíncrono que tienen procedimientos de entrada y salida que son coprogramas. Un ejemplo es la clase de bloqueo descrita anteriormente. Tiene la corutina __aenter__ , que es lógicamente necesaria para la operación asincrónica. Para admitir el protocolo asincrónico del administrador de contexto, su método __aexit__ también debe ser una rutina, que se logra mediante la inclusión de wait asyncio.sleep (0) . Se puede acceder a estas clases desde una rutina con la siguiente sintaxis: async def bar ( lock ): async with lock: print ( « bar » )
Al igual que con los administradores de contexto regulares, se garantiza que se llamará al método de salida cuando el administrador de contexto complete su trabajo, como de costumbre, y mediante una excepción. Para lograr esto, se utilizan los métodos especiales __aenter__ y __aexit__, que deben definirse como corutinas que esperan otra corutina u objeto en espera . Este ejemplo está tomado de la clase Lock : async def __aenter__(self): await self.acquire()
Si async with contiene una cláusula as variable , la variable obtiene el valor devuelto por __aenter__ .Para garantizar un comportamiento correcto, el firmware debe ser V1.9.10 o posterior.5. Excepciones a los tiempos de espera y debido a la cancelación de tareasEstos temas están relacionados: uasyncio incluye la cancelación de tareas y la aplicación de un tiempo de espera a una tarea, lanzando una excepción para la tarea de una manera especial.5.1 ExcepcionesSi se produce una excepción en una corutina, debe procesarse en esta corutina o en una corutina en espera de su finalización. Esto garantiza que la excepción no se aplique al planificador. Si se produce una excepción, el planificador dejará de funcionar pasando la excepción al código que inició el planificador. Por lo tanto, para evitar que el planificador se detenga, la corutina lanzada con loop.create_task () debe detectar cualquier excepción en su interior.Usar lanzar o cerrar para lanzar una excepción en una rutina no es razonable. Esto destruye uasyncio , haciendo que la rutina comience y posiblemente salga cuando todavía está en la cola de ejecución.El ejemplo anterior ilustra esta situación. Si se le permite trabajar hasta el final, funciona como se esperaba. 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')
Sin embargo, emitir una interrupción de teclado hace que la excepción entre en el bucle de eventos. Esto se debe a que la ejecución de uasyncio.sleep se pasa al bucle de eventos. Por lo tanto, las aplicaciones que requieren un código claro en respuesta a una interrupción del teclado deben detectar una excepción en el nivel del bucle de eventos.5.2 Cancelar y tiempos de esperaComo se mencionó anteriormente, estas funciones funcionan lanzando una excepción para la tarea de una manera especial utilizando el método especial MicroPython coroutine pend_throw . Cómo funciona depende de la versión. En uasyncio v.2.0 oficial , una excepción no se procesa hasta la próxima tarea programada. Esto impone un retraso si la tarea espera dormirentrada-salida Los tiempos de espera pueden ir más allá de su período nominal. La tarea de deshacer de otras tareas no puede determinar cuándo se completa la operación de deshacer.Actualmente hay una solución alternativa y dos soluciones.- Solución alternativa : la biblioteca asínica proporciona un medio para esperar a que se cancelen las tareas o los grupos de tareas. Consulte Cancelar un trabajo (sección 5.2.1.).
- La biblioteca Paul Sokolovsky proporciona uasyncio v2.4 , pero esto requiere su firmware Pycopy .
- Fast_io biblioteca uasyncio soluciona este problema de la Python (menos de manera elegante) y el corredor firmware oficial.
La jerarquía de excepciones utilizada aquí es Exception-CanceledError-TimeoutError .5.2.1 Cancelar un trabajouasyncio proporciona una función de cancelación (coro) . Esto funciona lanzando una excepción para usar la rutina pend_throw . También funciona con corutinas anidadas. El uso es el siguiente: async def foo(): while True:
Si este ejemplo se ejecuta en uasyncio v2.0 , cuando la barra devuelva cancelar, no tendrá efecto hasta el próximo foo programado y puede producirse un retraso de hasta 10 segundos cuando se cancela el foo . Otra fuente de retraso ocurrirá si foo está esperando E / S. Dondequiera que ocurra el retraso, la barra no podrá determinar si foo ha sido cancelado. Importa en algunos casos de uso.Cuando se usan las bibliotecas Paul Sokolovsky o fast_io, es suficiente usar sleep (0): async def foo(): while True:
Esto también funcionará en uasyncio v2.0 si foo (y cualquier foo de rutina pendiente ) nunca volvió a dormir y no esperó la E / S.El comportamiento que puede sorprender a los descuidados ocurre cuando se espera que se cancele una rutina que se ejecuta mediante create_task y está en modo de espera . Considere este fragmento: async def foo(): while True:
Cuando se cancela foo , se elimina de la cola del planificador; porque carece de una declaración de devolución , el procedimiento de llamada foo_runner nunca se reanuda. Se recomienda que siempre detecte la excepción en el ámbito más externo de la función que se va a deshacer: async def foo(): try: while True: await asyncio.sleep(10) await my_coro except asyncio.CancelledError: return
En este caso, my_coro no necesita detectar la excepción, ya que se propagará al canal de llamada y se capturará allí.Nota
Está prohibido usar métodos de cierre o lanzamiento de corutinas cuando las corutinas se usan fuera del programador. Esto socava el planificador, obligando a la rutina a ejecutar código, incluso si no está programado. Esto puede tener consecuencias indeseables.5.2.2 corrutinas con tiempos de esperaTiempos de espera implementarse usando uasyncio métodos .wait_for () y .wait_for_ms () . Toman la rutina y la latencia en segundos o ms, respectivamente, como argumentos. Si el tiempo de espera caduca, se lanzará un TimeoutError a la rutina usando pend_throw. Esta excepción debe ser detectada por el usuario o por la persona que llama. Esto es necesario por la razón descrita anteriormente: si el tiempo de espera expira, se cancela. A menos que el error sea detectado y devuelto, la única forma en que la persona que llama puede continuar es detectar la excepción en sí.Cuando la excepción fue detectada por la rutina, tuve fallas claras si la excepción no se detectó en el alcance externo, como se muestra a continuación: 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')
Alternativamente, puede interceptar la función de llamada: 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 Uasyncio v2.0 .Esto no se aplica a las bibliotecas Paul Sokolovsky o fast_io .Si la rutina comienza a esperar asyncio.sleep (t) , con un largo retraso t, la rutina no se reiniciará hasta que t expire . Si ha transcurrido el tiempo de espera antes de que finalice el sueño , se producirá un error de tiempo de espera cuando se recargue la rutina, es decir cuando t expira . En tiempo real y desde la perspectiva de la persona que llama, su respuesta TimeoutError se retrasará.Si esto es importante para la aplicación, cree un retraso largo mientras espera uno corto en el bucle. Corutinaasyn.sleep apoya esto.6 Interacción con el equipoLa base de la interacción entre uasyncio y eventos asíncronos externos es el sondeo. El hardware que requiere una respuesta rápida puede usar una interrupción. Pero la interacción entre la rutina de interrupción (ISR) y la rutina del usuario se basará en encuestas. Por ejemplo, un ISR puede llamar a Evento o establecer un indicador global, mientras que una rutina que espera un resultado sondea un objeto cada vez que se programa una solicitud.La interrogación puede llevarse a cabo de dos maneras, explícita o implícita. Esto último se realiza mediante la transmisión de E / Sun mecanismo, que es un sistema diseñado para dispositivos de transmisión como UART y sockets . En la encuesta explícita más simple, el siguiente código puede consistir: async def poll_my_device(): global my_flag
En lugar de un indicador global, puede usar una variable de instancia de la clase Event o una instancia de una clase que use wait . Una encuesta explícita se discute a continuación.El sondeo implícito consiste en desarrollar un controlador que actuará como un dispositivo de E / S de transmisión, como un UART o un socket de E / S de transmisión , que sondea dispositivos que usan el sistema select.poll Python : dado que el sondeo se realiza en C, es más rápido y más eficiente que encuesta explícita El uso del flujo de E / S se trata en la sección 6.3.Debido a su efectividad, el sondeo implícito brinda una ventaja a los controladores de dispositivos de E / S más rápidos: los controladores de transmisión se pueden crear para muchos dispositivos que generalmente no se consideran dispositivos de transmisión. Esto se discute con más detalle en la sección 6.4.6.1 Problemas de sincronizaciónTanto las encuestas explícitas como las implícitas se basan actualmente en la planificación cíclica. Supongamos que I / O funciona simultáneamente con N corutinas personalizadas, cada una de las cuales se ejecuta con cero retraso. Cuando se sirve la E / S, se sondeará tan pronto como se programen todas las operaciones del usuario. El retraso estimado debe considerarse al diseñar. Los canales de E / S pueden requerir almacenamiento en búfer, con el equipo de servicio ISR en tiempo real desde las memorias intermedias y las rutinas, llenando o liberando las memorias intermedias en un momento más lento.También es necesario considerar la posibilidad de ir más allá: este es el caso cuando algo interrogado por la rutina ocurre más de una vez antes de que realmente sea planificado por la rutina.Otro problema de tiempo es la precisión de latencia. Si los problemas de rutina await asyncio.sleep_ms ( t )
el planificador garantiza que la ejecución se suspenderá durante al menos t ms. El retraso real puede ser mayor que t, que depende de la carga actual del sistema. Si en este momento otras corutinas esperan la finalización de demoras distintas de cero, la próxima línea se programará inmediatamente para su ejecución. Pero si otras corutinas también están esperando su ejecución (ya sea porque emitieron un retraso cero o porque su tiempo también ha expirado), se puede programar para que se ejecuten antes. Esto introduce incertidumbre de sincronización en las funciones sleep () y sleep_ms () . El valor del caso más desfavorable para este desbordamiento puede calcularse sumando los valores de tiempo de ejecución de todas esas rutinas para determinar el tiempo de transmisión del caso más desfavorable al planificador.La versión fast_io de uasyncio en este contexto proporciona una forma de garantizar que la transmisión de E / S se sondeará en cada iteración del planificador. Se espera que el uasyncio oficial acepte las enmiendas pertinentes a su debido tiempo.6.2 Interrogación de dispositivos utilizando corutinasEste es un enfoque simple que es más adecuado para dispositivos que pueden ser interrogados a una velocidad relativamente baja. Esto se debe principalmente al hecho de que el sondeo con un intervalo de sondeo corto (o cero) puede llevar al hecho de que la rutina consume más tiempo del procesador del que es deseable para caer en el intervalo.El ejemplo apoll.py demuestra este enfoque al consultar el acelerómetro Pyboardcon un intervalo de 100 ms. Realiza un filtrado simple para ignorar el ruido e imprime un mensaje cada dos segundos si no se produce ningún movimiento.El ejemplo de aswitch.py proporciona controladores para interruptores y dispositivos de botón.A continuación se muestra un ejemplo de controlador para un dispositivo capaz de leer y escribir. Para facilitar la prueba, Pyboard UART 4 emula un dispositivo condicional. El controlador implementa la clase RecordOrientedUart, en el que los datos se suministran en registros de longitud variable que consisten en instancias de bytes. El objeto agrega un delimitador antes de enviar y almacena los datos entrantes hasta que se recibe un delimitador agregado. Esta es solo una demostración y una forma ineficiente de usar UART en comparación con la entrada / salida de transmisión.Para demostrar la transferencia asincrónica, suponemos que el dispositivo emulado tiene un medio para verificar que la transferencia se haya completado y que la aplicación requiera que esperemos. Ninguno de los supuestos es cierto en este ejemplo, pero el código lo falsifica esperando asyncio.sleep (0.1) .Para comenzar, no olvide conectar las salidas del Pyboard X1 y X2 (UART Txd y 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):
6.3 Uso de la transmisión de mecanismo ( Corriente )Este ejemplo demuestra la entrada simultánea y de salida en un solo microprocesador UART Pyboard .Para comenzar, conecte las salidas de Pyboard X1 y X2 (UART Txd y 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()
El código de soporte se puede encontrar en __init__.py en la biblioteca uasyncio . El mecanismo funciona porque el controlador del dispositivo (escrito en C ) implementa los siguientes métodos: ioctl, read, readline y write . La Sección 6.4: Escribir un controlador de dispositivo de transmisión revela detalles sobre cómo se pueden escribir dichos controladores en Python .UART puede recibir datos en cualquier momento. El mecanismo de transmisión de E / S verifica si hay caracteres entrantes pendientes cada vez que el programador obtiene el control. Cuando se ejecuta la rutina, la rutina de interrupción amortigua los caracteres entrantes; se eliminarán cuando la rutina pase al planificador. Por lo tanto, las aplicaciones UART deben diseñarse de modo que las rutinas minimicen el tiempo entre transferencias al planificador para evitar desbordamientos del búfer y pérdida de datos. Esto se puede mejorar utilizando un búfer de lectura UART más grande o una velocidad de datos más baja. Alternativamente, el control de flujo de hardware proporcionará una solución si la fuente de datos lo admite.6.3.1 Ejemplo de UART controladorprograma auart_hd.pyilustra un método de comunicación con un dispositivo semidúplex, como un dispositivo que responde al conjunto de comandos del módem AT. Half duplex significa que el dispositivo nunca envía datos no solicitados: sus transferencias siempre se llevan a cabo en respuesta a un comando recibido del maestro.El dispositivo se emula ejecutando una prueba en un Pyboard con dos conexiones cableadas.El dispositivo emulado (muy simplificado) responde a cualquier comando enviando cuatro líneas de datos con una pausa entre cada una para simular un procesamiento lento.El asistente envía un comando, pero no sabe de antemano cuántas filas de datos se devolverán. Inicia un temporizador de reinicio que se reinicia cada vez que se recibe una línea. Cuando expira el temporizador, se supone que el dispositivo ha completado la transmisión y se devuelve una lista de líneas recibidas.También se demuestra un caso de falla del dispositivo, que se logra omitiendo una transmisión antes de esperar una respuesta. Después del tiempo de espera, se devuelve una lista vacía. Vea los comentarios del código para más detalles.6.4 Desarrollo de transmisión (drivers Corriente ) unidad deentrada de corriente / mecanismo de salida ( corriente I / O ) para controlar el funcionamiento de la transmisión de dispositivos I / O tales como UART y tomas de corriente ( socket) El mecanismo puede ser utilizado por los controladores de cualquier dispositivo sondeado regularmente delegando al programador que utiliza select , sondeando la disponibilidad de cualquier dispositivo en la cola. Esto es más eficiente que realizar varias operaciones de rutina, cada una de las cuales sondea el dispositivo, en parte porque select está escrito en C , y también porque la rutina que realiza el sondeo se retrasa hasta que el objeto sondeado devuelve un estado listo.Un controlador de dispositivo capaz de dar servicio al mecanismo de entrada / salida de transmisión debería admitir preferiblemente los métodos StreamReader, StreamWriter. Un dispositivo legible debe proporcionar al menos uno de los siguientes métodos. Tenga en cuenta que estos son métodos sincrónicos. El método ioctl (ver más abajo) asegura que se invoquen solo cuando haya datos disponibles. Los métodos deben devolverse lo más rápido posible, utilizando tantos datos como estén disponibles.readline () Devuelve tantos caracteres como sea posible, hasta cualquier carácter de nueva línea. Se requiere si se usa StreamReader.readline ()read (n) Devuelve tantos caracteres como sea posible, pero no más de n . Obligatorio si se usa StreamReader.read () o StreamReader.readexactly ()El controlador creado debe proporcionar el siguiente método síncrono con retorno inmediato:escribir con argumentos buf, off, sz .Donde:buf es el búfer para escribir.off : desplazamiento al búfer del primer carácter a escribir.sz : el número solicitado de caracteres para escribir.El valor de retorno es el número de caracteres realmente escritos (tal vez 1 si el dispositivo es lento).El método ioctl garantiza que solo se invocará cuando el dispositivo esté listo para recibir datos.Todos los dispositivos deben proporcionar un método ioctl que sondee el equipo para determinar su estado de disponibilidad. Un ejemplo típico para un controlador de lectura / escritura: 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):
La siguiente es una descripción del retraso de espera de la clase 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 se puede usar de la siguiente manera: async def timer_test ( n ): timer = ms_timer.MillisecTimer () await timer ( 30 )
En comparación con uasyncio oficial , dicha implementación no ofrece ninguna ventaja en comparación con wait asyncio.sleep_ms () . El uso de fast_io proporciona retrasos significativamente más precisos en el patrón de uso normal, cuando las rutinas esperan un retraso cero.Puede usar la programación de E / S para asociar un evento con una devolución de llamada. Esto es más eficiente que el ciclo de sondeo, porque el sondeo no está programado hasta que ioctl regrese listo. A continuación, se ejecuta una devolución de llamada cuando la devolución de llamada cambia 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
Y de nuevo, en el uasyncio oficial , la demora puede ser alta. Dependiendo del diseño de la aplicación, la versión fast_io puede ser más eficiente.La demostración de iorw.py ilustra un ejemplo completo. Tenga en cuenta que al momento de escribir el artículo en el uasyncio oficial hay un error debido al cual esto no funciona . Hay dos soluciones La solución alternativa es escribir dos controladores separados, uno para solo lectura y otro para solo escritura. El segundo es usar fast_io , que resuelve este problema.En uasyncio oficial , la entrada / salida se planifica muy raramente .6.5 Ejemplo completo: aremote.pyEl controlador está diseñado para recibir / decodificar señales de un control remoto infrarrojo. El controlador aremote.py en sí . Las siguientes notas son significativas con respecto al uso de asyncio .La interrupción en el contacto registra la hora del cambio de estado (en microsegundos) y establece el evento, omitiendo la hora en que ocurrió el primer cambio de estado. La rutina espera un evento, informa la duración del paquete de datos y luego decodifica los datos almacenados antes de llamar a la devolución de llamada especificada por el usuario.Pasar el tiempo a una instancia de Evento permite que la rutina compense cualquierretraso de asincio al establecer el período de retraso.6.6 Sensor ambiental HTU21DEl controlador de chip HTU21D proporciona mediciones precisas de temperatura y humedad.El chip necesita unos 120 ms para recibir ambos elementos de datos. El controlador funciona de forma asincrónica, iniciando la recepción y el uso de await asyncio.sleep (t) antes de leer los datos, actualiza las variables de temperatura y humedad, a las que se puede acceder en cualquier momento, lo que permite que se ejecuten otras rutinas mientras se ejecuta el controlador del chip.7.Consejos y trucos7.1 El programa se congela El congelamientogeneralmente ocurre porque la tarea se bloquea sin concesión: esto conducirá a la congelación de todo el sistema. Al desarrollar, es útil tener una rutina que encienda periódicamente el LED integrado. Esto proporciona confirmación de que el planificador todavía se está ejecutando.7.2 uasyncio guarda el estadoAl iniciar programas que usan uasyncio en REPL, realice un restablecimiento parcial (ctrl-D) entre los inicios. Debido al hecho de que uasyncio mantiene el estado entre inicios, puede ocurrir un comportamiento impredecible en el próximo inicio.7.3 Recolección de basuraPuede ejecutar una rutina especificando import gc primero : gc.collect () gc.treshold ( gc.mem_free () // 4 + gc.mem_alloc ())
El propósito de esto se discute aquí en la sección de montón.7.4 PruebasEs aconsejable asegurarse de que el controlador del dispositivo mantenga el control cuando sea necesario, lo que puede hacerse ejecutando una o más copias de las rutinas ficticias que inician el ciclo de impresión de mensajes y comprobando que se ejecuta durante los períodos en que el controlador está en modo de espera: async def rr(n): while True: print('Roundrobin ', n) await asyncio.sleep(0)
Como ejemplo del tipo de peligro que puede surgir, en el ejemplo anterior, el método RecordOrientedUart __await__ se escribió originalmente 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, la ejecución se alarga hasta que se recibe todo el registro, así como el hecho de que uart.any () siempre devuelve un número distinto de cero de caracteres recibidos. En el momento de la llamada, es posible que ya se hayan recibido todos los caracteres. Esta situación se puede resolver utilizando un bucle externo: def __await__(self): data = b'' while not data.endswith(self.DELIMITER): yield from asyncio.sleep(0)
Vale la pena señalar que este error no habría sido obvio si los datos se hubieran enviado al UART a una velocidad menor, en lugar de utilizar una prueba de retroalimentación. Bienvenido a las alegrías de la programación en tiempo real.7.5 Error comúnSi una función o método se define mediante async def y posteriormente se llama como si fuera una llamada regular (sincrónica), MicroPython no muestra un mensaje de error. Esto es por diseño. Por lo general, esto lleva al hecho de que el programa en silencio no funciona correctamente: async def foo():
Tengo una sugerencia que sugiere arreglar la situación en la opción 1 usando fast_io .El módulo check_async_code.py intenta detectar casos de uso dudoso de las rutinas. Está escrito en Python3 y está diseñado para funcionar en una PC. Se usa en scripts escritos de acuerdo con las pautas descritas en esta guía con corutinas declaradas usando async def . El módulo toma un argumento, la ruta al archivo fuente MicroPython (o --help).Tenga en cuenta que es algo grosero y está destinado a ser utilizado en un archivo sintácticamente correcto, que no se inicia por defecto. Use una herramienta, como pylint, para la verificación general de sintaxis ( pylint actualmente no tiene este error).El guión produce falsos positivos. Según el plan, las corutinas son objetos del primer nivel; pueden transferirse a funciones y almacenarse en estructuras de datos. Dependiendo de la lógica del programa, puede guardar la función o el resultado de su ejecución. El script no puede determinar la intención. Su objetivo es ignorar los casos que parecen correctos al identificar otros casos a considerar. Supongamos que foo donde la corutina se declara como def asíncrona : loop.run_until_complete(foo())
Me parece útil como es, pero las mejoras siempre son bienvenidas.7.6 Programación con tomas de corriente ( enchufes )Hay dos enfoques básicos para la programación de sockets uasyncio . De forma predeterminada, los sockets están bloqueados hasta que se completa la operación de lectura o escritura especificada. Uasyncio admite el bloqueo de socket mediante select.poll para evitar que el programador los bloquee. En la mayoría de los casos, este mecanismo es más fácil de usar. Puede encontrar un ejemplo de código de cliente y servidor en el directorio client_server . Userver utiliza la aplicación select.poll sondeando explícitamente el socket del servidor.Los sockets del cliente lo usan implícitamente en el sentido de que el motor de transmisión uasyncio lo usa directamente.Tenga en cuenta que socket.getaddrinfo está actualmente bloqueado. El tiempo en el código de muestra será mínimo, pero si se requiere una búsqueda de DNS, el período de bloqueo puede ser significativo.Un segundo enfoque para la programación de sockets es usar sockets sin bloqueo. Esto agrega complejidad, pero es necesario en algunas aplicaciones, especialmente si la conexión es a través de WiFi (ver más abajo).En el momento de escribir este artículo (marzo de 2019), el soporte TLS para sockets sin bloqueo estaba en desarrollo. Su estado exacto es desconocido (para mí).El uso de enchufes sin bloqueo requiere cierta atención al detalle. Si se producen lecturas sin bloqueo debido a la latencia del servidor, no hay garantía de que se devuelvan todos (o alguno) de los datos solicitados. Del mismo modo, las entradas pueden no completarse.Por lo tanto, los métodos asíncronos de lectura y escritura deben realizar iterativamente una operación sin bloqueo hasta que se lean o escriban los datos requeridos. En la práctica, puede ser necesario un tiempo de espera para hacer frente a las interrupciones del servidor.Otra complicación es que el puerto ESP32 tenía problemas que requerían robos bastante desagradables para una operación libre de errores. No he probado si este sigue siendo el caso.Módulo Sock_nonblock.pyilustra los métodos requeridos. Esta no es una demostración funcional y es probable que las decisiones dependan de la aplicación.7.6.1 Problemas con WiFiEl mecanismo de transmisión uasyncio no es la mejor opción al detectar interrupciones de WiFi. Encontré que era necesario usar enchufes sin bloqueo para proporcionar una operación a prueba de fallas y volver a conectar al cliente en caso de fallas.Este documento describe los problemas que he encontrado en aplicaciones WiFi que mantienen abiertos los sockets durante períodos prolongados y describe la solución.Pltcmofrece un robusto cliente MQTT asíncrono que proporciona integridad de mensajes durante fallas de WiFi. Se describe un enlace serial asincrónico full-duplex simple entre un cliente inalámbrico y un servidor con cable con entrega garantizada de mensajes.7.7 Argumentos del constructor del bucle de eventosPuede producirse un pequeño error si necesita crear un bucle de eventos con valores que difieran de los valores predeterminados. Dicho ciclo debe declararse antes de ejecutar cualquier otro código utilizando asyncio porque estos valores pueden ser necesarios en este código. De lo contrario, el código se inicializará con los valores predeterminados: import uasyncio as asyncio import some_module bar = some_module.Bar()
Dado que la importación de un módulo puede ejecutar código, la forma más segura es crear una instancia de un bucle de eventos inmediatamente después de importar uasyncio . import uasyncio as asyncio loop = asyncio.get_event_loop(runq_len=40, waitq_len=40) import some_module bar = some_module.Bar()
Al escribir módulos para su uso por otros programas, prefiero evitar ejecutar el código uasyncio al importar. Escriba funciones y métodos para esperar un bucle de evento como argumento. Luego, asegúrese de que solo las aplicaciones de nivel superior llamen a get_event_loop : import uasyncio as asyncio import my_module
Este tema se discute aquí .8 notas para principiantesEstas notas están destinadas a principiantes en código asíncrono y comienzan con una descripción de los problemas que los planificadores intentan resolver, así como una descripción general del enfoque de uasyncio para las soluciones.La Sección 8.5 discute los méritos relativos de los módulos de hilo uasyncio y _ , así como también por qué preferiría usar las rutinas de uasyncio con programación proactiva (_thread). 8.1 Problema 1: bucles de eventosUna aplicación de firmware típica funciona continuamente y, al mismo tiempo, debe responder a eventos externos, que pueden incluir un cambio de voltaje en el ADC, la aparición de una interrupción de hardware o un símbolo recibido en el UART o datos disponibles en el zócalo. Estos eventos ocurren de forma asíncrona, y el código debería poder responder independientemente del orden en que ocurran. Además, pueden requerirse tareas que dependen del tiempo, como el parpadeo de los LED.La forma obvia de hacerlo es con el bucle de eventos uasycio . Este ejemplo no es un código práctico, pero sirve para ilustrar la forma general del bucle 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()
Tal ciclo funciona para ejemplos simples, pero a medida que aumenta el número de eventos, el código rápidamente se vuelve engorroso. También violan los principios de la programación orientada a objetos al combinar la mayor parte de la lógica del programa en un solo lugar, en lugar de vincular el código a un objeto controlado. Queremos desarrollar una clase para un LED parpadeante que se pueda insertar en un módulo e importar. El enfoque OOP para el parpadeo del LED puede verse así: 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()
El planificador en uasyncio le permite crear tales clases.8.2 Problema 2: Métodos de bloqueoSuponga que necesita leer un cierto número de bytes de un socket. Si llama a socket.read (n) con un socket de bloqueo de forma predeterminada, se "bloqueará" (es decir, no podrá terminar) hasta que se reciban n bytes. Durante este período, la aplicación no responderá a otros eventos.Usando el zócalo uasyncio sin bloqueo , puede escribir un método de lectura asíncrono. Una tarea que requiera datos se bloqueará (necesariamente) hasta que se reciba, pero se realizarán otras tareas durante este período, lo que permitirá que la aplicación permanezca receptiva.8.3. Uasyncio se acercaLa próxima clase tiene un LED que se puede encender y apagar, y también puede parpadear a cualquier velocidad. La instancia de LED_async usa el método de ejecución , que puede usarse para la operación continua. El comportamiento de los LED se puede controlar utilizando los métodos on (), off () y 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
Cabe señalar que on (), off () y flash () son métodos sincrónicos comunes. Cambian el comportamiento del LED, pero regresan de inmediato. El parpadeo ocurre "en segundo plano". Esto se explica en detalle en la siguiente sección.La clase cumple con el principio OOP, que consiste en almacenar la lógica asociada con el dispositivo en la clase. Al mismo tiempo, el uso de uasyncio asegura que la aplicación pueda responder a otros eventos mientras el LED parpadea. El siguiente programa parpadea con cuatro LED de Pyboard a diferentes frecuencias y también responde al botón USR, que lo completa. import pyb import uasyncio as asyncio from led_async import LED_async
A diferencia del primer ejemplo de un bucle de eventos, la lógica asociada con el interruptor está en una función separada de la funcionalidad del LED. Presta atención al código utilizado para iniciar el planificador: loop = asyncio.get_event_loop() loop.run_until_complete(killer())
8.4 La planificación en uasyncioPython 3.5 y MicroPython admiten el concepto de una función asincrónica, también conocida como una rutina o tarea. Una corutina debe incluir al menos una declaración de espera . async def hello(): for _ in range(10): print('Hello world.') await asyncio.sleep(1)
Esta función imprime un mensaje diez veces a intervalos de un segundo. Mientras la función está en pausa en previsión de un retraso, el programador asíncio realizará otras tareas, creando la ilusión de ejecutarlas simultáneamente.Cuando un problema de rutina aguarda asyncio.sleep_ms () o aguarda asyncio.sleep (), la tarea actual se detiene y se coloca en una cola ordenada por tiempo y la ejecución continúa a la tarea en la parte superior de la cola. La cola está diseñada de tal manera que, incluso si el modo de suspensión especificado es cero, se realizarán otras tareas relevantes hasta que se reanude la corriente. Esta es la planificación "honesta circular". Es una práctica común correr esperar bucles asyncio.sleep (0) .para que la tarea no retrase la ejecución. El siguiente es un bucle de espera ocupada que espera otra tarea para establecer la variable de marca global . Por desgracia, monopoliza el procesador e impide el lanzamiento de otras corutinas: async def bad_code(): global flag while not flag: pass
El problema aquí es que hasta que el flagis False loop pase el control al planificador, no se iniciará ninguna otra tarea. El enfoque correcto: async def good_code(): global flag while not flag: await asyncio.sleep(0)
Por la misma razón, es una mala práctica establecer retrasos, por ejemplo, utime.sleep (1) porque bloquea otras tareas durante 1 s; es más correcto usar await asyncio.sleep (1) .Tenga en cuenta que los retrasos generados por los métodos uasyncio sleep y sleep_ms en realidad pueden exceder el tiempo especificado. Esto se debe al hecho de que se realizarán otras tareas durante el retraso. Una vez transcurrido el período de retraso, la ejecución no se reanudará hasta que los problemas de la tarea en ejecución aguarden o finalicen. Una corutina bien portada siempre declarará esperara intervalos regulares. Cuando se requiere un retraso exacto, especialmente si uno es inferior a unos pocos ms, puede ser necesario usar utime.sleep_us (us) .8.5 ¿Por qué la programación colaborativa, no basada en subprocesos ( _thread )?La reacción inicial de los principiantes a la idea de co-planificar corutinas es a menudo decepcionante. ¿Seguramente la planificación de transmisión es mejor? ¿Por qué debería ceder explícitamente el control si la máquina virtual Python puede hacer esto por mí?Cuando se trata de sistemas embebidos, el modelo de colaboración tiene dos ventajas.El primero es ligero. Es posible tener una gran cantidad de corutinas, porque a diferencia de los hilos programados, las corutinas suspendidas ocupan menos espacio.En segundo lugar, esto evita algunos de los problemas sutiles asociados con la programación de transmisión.En la práctica, la multitarea colaborativa se usa ampliamente, especialmente en aplicaciones de interfaz de usuario.En defensa del modelo de planificación de transmisión, mostraré una ventaja: si alguien escribe for x in range ( 1000000 ):
No bloqueará otras tareas. El modelo de colaboración asume que el ciclo debería dar explícitamente a cada tarea un cierto número de iteraciones, por ejemplo, poner el código en una rutina y emitir periódicamente asyncio.sleep (0) .Por desgracia, esta ventaja palidece en comparación con las desventajas. Algunos de estos se describen en la documentación para escribir manejadores de interrupciones.. En un modelo de programación de transmisión, cada subproceso puede interrumpir cualquier otro subproceso, cambiando los datos que se pueden utilizar en otros subprocesos. Como regla general, es mucho más fácil encontrar y corregir un bloqueo que ocurre debido a un error que no da un resultado que la detección de errores a veces muy sutiles y raramente encontrados que pueden ocurrir en el código escrito en el marco de un modelo con planificación de transmisión.En pocas palabras, si escribe una corutina MicroPython , puede estar seguro de que las variables no serán cambiadas repentinamente por otra corutina: su corutina tiene control total hasta que regrese aguarde asyncio.sleep (0) .Tenga en cuenta que los manejadores de interrupciones son preventivos. Esto se aplica tanto a las interrupciones de hardware como de software que pueden ocurrir en cualquier parte de su código.Una discusión elocuente de los problemas de planificación de transmisión se puede encontrar aquí .8.6 InteracciónEn aplicaciones no triviales, las corutinas deben interactuar. Se pueden usar los métodos convencionales de Python . Estos incluyen el uso de variables globales o la declaración de corutinas como métodos de objeto: pueden compartir variables de instancia. Alternativamente, un objeto mutable puede pasarse como argumento a una rutina.El modelo de planificación de transmisión requiere especialistas para garantizar que las clases proporcionen una conexión segura; en un modelo de colaboración, esto rara vez se requiere.8.7. Poll ( Polling )Algunos dispositivo de hardware tal como un acelerómetro Pyboard , no admiten interrupciones, y por lo tanto debe ser sondea (es decir, verificar periódicamente). El sondeo también se puede usar junto con los manejadores de interrupciones: el manejador de interrupciones mantiene el equipo y establece una bandera. La rutina sondea la bandera; si está configurada, los datos se procesan y la bandera se restablece. El mejor enfoque es usar la clase Evento .