Un mundo sin corrutinas. Iteradores del generador

1. Introducción



Para confundir el problema tanto como sea posible, confíe la solución a los programadores;). Pero en serio, en mi opinión les pasa algo parecido a las corutinas, porque, de buena gana o no, se utilizan para difuminar la situación. Este último se caracteriza por el hecho de que todavía existen problemas de programación paralela que no van a ningún lado y, lo más importante, las corrutinas no contribuyen a su solución cardinal.



Comencemos con la terminología. "Cuántas veces se lo han dicho al mundo", pero hasta ahora "el mundo" todavía se está preguntando la diferencia entre la programación asincrónica y la programación paralela (ver la discusión sobre el tema de la asincronía en [1] ). El quid del problema de comprender la asincronía frente al paralelismo comienza con la definición del paralelismo en sí. Simplemente no existe. Existe algún tipo de comprensión intuitiva, que a menudo se interpreta de diferentes maneras, pero no existe una definición científica que elimine todas las preguntas de manera tan constructiva como una discusión sobre el resultado de la operación "dos y dos".



Y como, una vez más, todo esto no está ahí, entonces, confundidos en términos y conceptos, seguimos distinguiendo entre programación paralela y concurrente, asincrónica, reactiva y alguna otra, etc. etc. Creo que sería poco probable que hubiera un problema al darse cuenta de que una calculadora mecánica como Felix funciona de manera diferente a una calculadora de software. Pero desde un punto de vista formal, es decir, un conjunto de operaciones y el resultado final, no hay diferencia entre ellas. Este principio debe tenerse en cuenta en la definición de programación paralela.



Debemos tener una definición estricta y medios transparentes para describir el paralelismo, lo que lleva a resultados consistentes como el "torpe" Felix y cualquier calculadora de software. Es imposible que el concepto de "paralelismo" se asocie con los medios de su implementación (con el número de los mismos núcleos). Y lo que hay "debajo del capó" - esto debería ser de interés principalmente solo para aquellos que están involucrados en la implementación de la "maquinaria", pero no para aquellos que usan tal "calculadora paralela" condicional.



Pero tenemos lo que tenemos. Y tenemos, si no una locura, entonces una discusión activa de corrutinas y programación asincrónica. ¿Y qué más podemos hacer si parece que estamos hartos del multiproceso, pero no se ofrece algo más? Incluso hablan de algún tipo de magia;) Pero todo se vuelve obvio si entiendes las razones. Y se encuentran exactamente allí, en el plano del paralelismo. Su definición y su implementación.



Pero bajemos de las alturas globales y, hasta cierto punto, filosóficas de la ciencia de la programación (desde la computadora) a nuestra "tierra pecaminosa". Aquí, sin restar valor a los méritos del lenguaje Kotlin actualmente popular, me gustaría admitir mi pasión por Python. Quizás algún día y en alguna otra situación mis preferencias cambiarán, pero de hecho, hasta ahora todo es así.



Hay varias razones para esto. Entre ellos se encuentra el acceso gratuito a Python. Este no es el argumento más fuerte, ya que un ejemplo con el mismo Qt dice que la situación puede cambiar en cualquier momento. Pero si bien Python, a diferencia de Kotlin, es gratuito, al menos en la forma del mismo entorno PyCharm de JetBrains (por lo que se les agradece especialmente), entonces mis simpatías están de su lado. También es atractivo que haya una gran cantidad de literatura en ruso, ejemplos en Python en Internet, tanto educativos como bastante reales. En Kotlin, no están en ese número y su variedad no es tan grande.



Quizás un poco por delante de la curva, decidí presentar los resultados de dominar Python en el contexto de los problemas de definición e implementación de paralelismo y asincronía de software. Esto fue iniciado por el artículo [2]... Hoy consideraremos el tema de los generadores-corrutinas. Mi interés en ellos está alimentado por la necesidad de ser consciente de lo específico, interesante, pero no muy familiar para mí en este momento, las posibilidades de los lenguajes / lenguajes de programación modernos.



Como en realidad soy un programador puro de C ++, esto explica muchas cosas. Por ejemplo, si en Python las corrutinas y los generadores han estado presentes durante mucho tiempo, entonces en C ++ todavía tienen que ganar su lugar. Pero, ¿C ++ realmente lo necesita? En mi opinión, el lenguaje de programación debe ampliarse razonablemente. Parece que C ++ tiró lo más lejos posible, y ahora está tratando de ponerse al día apresuradamente. Pero se pueden implementar problemas de concurrencia similares utilizando otros conceptos y modelos que son más fundamentales que las corrutinas / corrutinas. Y se demostrará aún más el hecho de que detrás de esta declaración no hay solo palabras.



Si vamos a admitirlo todo, también admito que soy bastante conservador con respecto a C ++. Por supuesto, sus objetos y capacidades de programación orientada a objetos son "nuestro todo" para mí, pero yo, digamos, soy crítico con las plantillas. Bueno, nunca me fijé en su peculiar "lenguaje de pájaro", que, al parecer, complica enormemente la percepción del código y la comprensión del algoritmo. Aunque de vez en cuando incluso recurrí a su ayuda, los dedos de una mano son suficientes para todo esto. Respeto la biblioteca STL y no puedo prescindir de ella :) Por lo tanto, incluso a partir de este hecho, a veces tengo dudas sobre las plantillas. Así que todavía los evito tanto como puedo. Y ahora estoy esperando con un escalofrío las "corrutinas de plantilla" en C ++;)



Python es otro asunto. No he notado ninguna plantilla todavía y me calma. Pero, por otro lado, esto es, curiosamente, alarmante. Sin embargo, cuando miro el código de Kotlin y, especialmente, el código del compartimiento del motor, la ansiedad pasa rápidamente;) Es cierto, creo que esto sigue siendo una cuestión de costumbre y mis prejuicios. Espero que con el tiempo me capacite para percibirlos adecuadamente (plantillas).



Pero ... volvamos a las corrutinas. Resulta que ahora están bajo el nombre de corutin. ¿Qué hay de nuevo con el cambio de nombre? Sí, en realidad nada. Como antes, el conjunto se considera a su vezfunciones realizadas. De la misma forma que antes, antes de salir de la función, pero antes de la finalización de su trabajo, se fija el punto de retorno, a partir del cual se retoma el trabajo posteriormente. Dado que la secuencia de conmutación no está estipulada, el programador mismo controla este proceso creando su propio planificador. A menudo, esto es solo un recorrido de funciones. Como, por ejemplo, el ciclo de eventos Round Robin en el video de Oleg Molchanov [3] .



Así es como suele verse una introducción moderna a las corrutinas y la programación asincrónica "en los dedos". Está claro que con la inmersión en este tema surgen nuevos términos y conceptos. Los generadores son uno de ellos. Además, su ejemplo será la base para demostrar "preferencias paralelas", pero ya en mi interpretación automática.



2. Generadores de listas de datos



Entonces, generadores. La programación asincrónica y las corrutinas a menudo se asocian con ellos. Una serie de videos de Oleg Molchanov habla de todo esto. Por lo tanto, se refiere a la característica clave de los generadores como su “capacidad de pausar la ejecución de una función para continuar su ejecución desde el mismo lugar en el que se detuvo la última vez” (para más detalles, ver [3] ). Y en esto, dado lo dicho anteriormente sobre la ya bastante antigua definición de corrutinas, no hay nada nuevo.



Pero resulta que los generadores han encontrado un uso bastante específico para crear listas de datos. Ya se ha dedicado una introducción a este tema a un video de Egorov Artem [4]... Pero, como parece, por su aplicación, mezclamos conceptos cualitativamente diferentes: operaciones y procesos. Al expandir las capacidades descriptivas del lenguaje, enmascaramos en gran medida los problemas que puedan surgir. Aquí, como dicen, para no jugar demasiado. El uso de generadores-corrutinas para describir datos contribuye exactamente a esto, me parece. Tenga en cuenta que Oleg Molchanov también advierte contra la asociación de generadores con estructuras de datos, enfatizando que “los generadores son funciones” [3] .



Pero volvamos al uso de generadores para definir datos. Es difícil ocultar que hemos creado un proceso que calcula los elementos de la lista. Por lo tanto, surgen inmediatamente preguntas sobre dicha lista como proceso. Por ejemplo, ¿cómo reutilizarlo si las corrutinas, por definición, solo funcionan "de una manera"? ¿Cómo calcular un elemento arbitrario del mismo si el proceso no se puede indexar? Etc. etc. Artyom no da respuestas a estas preguntas, solo advierte que, dicen, el acceso repetido a los elementos de la lista no se puede organizar y la indexación es inadmisible. Una búsqueda en Internet nos convence de que no solo yo tengo preguntas similares, sino que las soluciones que se proponen no son tan triviales y obvias.



Otro problema es la velocidad de generación de la lista. Ahora formamos un solo elemento de la lista en cada interruptor de corrutina, y esto aumenta el tiempo de generación de datos. El proceso se puede acelerar enormemente generando elementos en "lotes". Pero, muy probablemente, habrá problemas con esto. ¿Cómo detener un proceso que ya se está ejecutando? O algo mas. La lista puede ser bastante larga, utilizando solo elementos seleccionados. En tal situación, la memorización de datos se usa a menudo para un acceso eficiente. Por cierto, casi de inmediato encontré un artículo sobre este tema para Python, consulte [5] (para obtener más información sobre la memorización en términos de autómatas, consulte el artículo [6] ). Pero, ¿y en este caso?



La confiabilidad de tal sintaxis para definir listas también puede ser cuestionable, ya que es bastante fácil utilizar por error corchetes en lugar de paréntesis y viceversa. Resulta que una solución aparentemente hermosa y elegante en la práctica puede conducir a ciertos problemas. Un lenguaje de programación debe ser tecnológico, flexible y seguro contra errores involuntarios.



Por cierto, sobre el tema de las listas y los generadores sobre sus ventajas y desventajas, que se cruzan con los comentarios anteriores, puede ver otro video de Oleg Molchanov [7] .



3. Generadores-corrutinas



El siguiente video de Oleg Molchanov [8] analiza el uso de generadores para coordinar el trabajo de las corrutinas. En realidad, están destinados a esto. Se llama la atención sobre la elección de momentos para cambiar de rutina. Su disposición sigue una regla simple: colocamos la declaración de rendimiento delante de las funciones de bloqueo. Estas últimas se entienden como funciones, cuyo tiempo de retorno es tan largo en comparación con otras operaciones que se asocian cálculos a detenerlas. Por eso, se les llamó bloqueadores.



El cambio es efectivo cuando el proceso suspendido continúa su trabajo exactamente cuando la llamada de bloqueo no esperará, pero completará rápidamente su trabajo. Y, al parecer, por el bien de esto, todo este "alboroto" se inició en torno al modelo de corrutina / corrutina y, en consecuencia, se dio un impulso al desarrollo de la programación asincrónica. Aunque, observamos, la idea original de las corrutinas todavía era diferente: crear un modelo virtual de computación paralela.



En el video en consideración, como en el caso general de las corrutinas, la continuación de la operación de corrutina está determinada por el entorno externo, que es un programador de eventos. En este caso, está representado por una función llamada event_loop. Y, al parecer, todo es lógico: el planificador realizará el análisis y continuará el trabajo de la corrutina llamando al operador next (), exactamente cuando sea necesario. El problema está en la espera donde no se esperaba: el planificador puede ser bastante complejo. En el video anterior de Molchanov ( ver [3] ), todo era simple, ya que Se realizó una simple transferencia alterna de control, en la que no hubo bloqueos, ya que no hubo llamadas correspondientes. No obstante, destacamos que en cualquier caso es necesario al menos un planificador simple.



Problema 1. , next() (. event_loop). , , yield. - , , next(), .



2. , select, — . .



Pero la cuestión no es ni siquiera la necesidad de un planificador, sino el hecho de que asume funciones inusuales para él. La situación se complica aún más por el hecho de que es necesario implementar un algoritmo para el funcionamiento conjunto de muchas corrutinas. La comparación de los programadores discutidos en los dos videos mencionados por Oleg Molchanov refleja un problema similar con bastante claridad: el algoritmo de programación de sockets en [8] es notablemente más complicado que el algoritmo "carrusel" en [3] .



3. A un mundo sin corrutinas



Como estamos seguros de que un mundo sin corrutinas es posible, oponiéndolos con autómatas, entonces es necesario mostrar cómo tareas similares ya están resueltas por ellos. Demostremos esto usando el mismo ejemplo de trabajo con sockets. Tenga en cuenta que su implementación inicial resultó no ser tan trivial como para que pudiera entenderse de inmediato. Esto es enfatizado repetidamente por el propio autor del video. Otros enfrentan problemas similares en el contexto de las corrutinas. Entonces, las desventajas de las corrutinas asociadas con la complejidad de su percepción, comprensión, depuración, etc. discutido en el video [10] .



Primero, algunas palabras sobre la complejidad del algoritmo en consideración. Esto se debe a la naturaleza dinámica y plural de los procesos de atención al cliente. Para ello, se crea un servidor que escucha un puerto determinado y, a medida que aparecen las solicitudes, genera muchas funciones de servicio al cliente que llegan hasta él. Dado que puede haber muchos clientes, aparecen de forma impredecible, se crea una lista dinámica a partir de los procesos de mantenimiento de sockets e intercambio de información con ellos. El código para la solución generadora de Python discutida en el video [8] se muestra en el Listado 1.



Listado 1. Enchufes en generadores
import socket
from select import select
tasks = []
to_read = {}
to_write = {}

def server():

    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind(('localhost', 5001))
    server_socket.listen()

    while True:
        yield ('read', server_socket)
        client_socket, addr = server_socket.accept()    
        print('Connection from', addr)
        tasks.append(client(client_socket, addr))       
    print('exit server')

def client(client_socket, addr):

    while True:
        yield ('read', client_socket)
        request = client_socket.recv(4096)              

        if not request:
            break
        else:
            response = 'Hello World\n'.encode()

            yield ('write', client_socket)

            client_socket.send(response)                
    client_socket.close()                               
    print('Stop client', addr)

def event_loop():
    while any([tasks, to_read, to_write]):

        while not tasks:

            ready_to_read, ready_to_write, _ = select(to_read, to_write, [])

            for sock in ready_to_read:
                tasks.append(to_read.pop(sock))

            for sock in ready_to_write:
                tasks.append(to_write.pop(sock))
        try:
            task = tasks.pop(0)

            reason, sock = next(task)   

            if reason == 'read':
                to_read[sock] = task
            if reason == 'write':
                to_write[sock] = task
        except StopIteration:
            print('Done!')
tasks.append(server())
event_loop()




Los algoritmos de servidor y cliente son bastante básicos. Pero debería ser alarmante que el servidor ponga la función del cliente en la lista de tareas. Además, más: es difícil comprender el algoritmo del bucle de eventos event_loop. ¿Hasta que la lista de tareas pueda estar vacía si al menos el proceso del servidor debe estar siempre presente en ella? ..



A continuación, se introducen los diccionarios to_read y to_write. El trabajo mismo con los diccionarios requiere una explicación separada, ya que es más difícil que trabajar con listas regulares. Debido a esto, la información devuelta por las declaraciones de rendimiento se adapta a ellos. Entonces comienza el "baile con pandereta" alrededor de los diccionarios y todo se vuelve como una especie de "hirviendo": algo parece estar colocado en los diccionarios, desde donde entra en la lista de tareas, etc. etc. Puedes "romperte la cabeza", resolviendo todo esto.



¿Y cómo será la solución de la tarea en cuestión? Será lógico que los autómatas creen modelos equivalentes a los procesos de trabajo con sockets ya discutidos en el video. En el modelo de servidor, parece que no es necesario cambiar nada. Este será un autómata que funciona como la función server (). Su gráfico se muestra en la Fig. 1a. La acción de autómata y1 () crea un socket de servidor y lo conecta al puerto especificado. El predicado x1 () define la conexión del cliente y, si está presente, la acción y2 () crea un proceso de servicio de socket de cliente, colocando este último en la lista de procesos de clases, que incluye las clases de objeto activas.



En la Fig. 1b muestra un gráfico del modelo para un cliente individual. Estando en el estado "0", el autómata determina la disposición del cliente para transmitir información (predicado x1 () - verdadero) y recibe una respuesta dentro de la acción y1 () en la transición al estado "1". Además, cuando el cliente está listo para recibir información (ya que x2 () debe ser verdadero), la acción y2 () implementa la operación de enviar un mensaje al cliente en la transición al estado inicial "0". Si el cliente rompe la conexión con el servidor (en este caso, x3 () es falso), entonces el autómata cambia al estado "4", cerrando el socket del cliente en la acción y3 (). El proceso permanece en el estado "4" hasta que se excluye de la lista de clases activas (consulte la descripción anterior del modelo de servidor para la formación de la lista).



En la Fig. 1c muestra un autómata que implementa el lanzamiento de procesos similar a la función event_loop () del Listado 1. Solo en este caso, su algoritmo de operación es mucho más simple. Todo se reduce al hecho de que la máquina revisa los elementos de la lista de clases activas y llama al método loop () para cada una de ellas. Esta acción es implementada por y2 (). La acción y4 () excluye de la lista las clases que están en el estado "4". El resto de acciones funcionan con el índice de la lista de objetos: la acción y3 () aumenta el índice, la acción y1 () lo restablece.



Las capacidades de programación de objetos en Python son diferentes de la programación de objetos en C ++. Por tanto, se tomará como base la implementación más simple del modelo de autómata (para ser precisos, es una imitación de un autómata). Se basa en el principio de objeto de representar procesos, dentro del cual cada proceso corresponde a una clase activa separada (a menudo también se les llama agentes). La clase contiene las propiedades y métodos necesarios (ver más detalles sobre métodos de autómatas específicos - predicados y acciones en [9] ), y la lógica del autómata (sus funciones de transición y salida) se concentra dentro del método llamado loop (). Para implementar la lógica del comportamiento del autómata, usaremos la construcción if-elif-else.



Con este enfoque, el "bucle de eventos" no tiene nada que ver con analizar la disponibilidad de sockets. Son comprobados por los propios procesos, que utilizan la misma instrucción select dentro de los predicados. En esta situación, operan con un solo socket, y no una lista de ellos, verificándolo para la operación que se espera para este socket en particular y precisamente en la situación que está determinada por el algoritmo de operación. Por cierto, en el curso de la depuración de dicha implementación, apareció una esencia de bloqueo inesperado de la instrucción select.



Figura: 1. Gráficos de procesos de autómatas para trabajar con sockets
image



El Listado 2 muestra un código de objeto de autómata en Python para trabajar con sockets Este es nuestro tipo de "mundo sin corrutinas". Es un "mundo" con diferentes principios para diseñar procesos de software. Se caracteriza por la presencia de un modelo algorítmico de cómputos paralelos (para más detalles ver [9] , que es la principal y cualitativa diferencia entre la tecnología de programación de autómatas (AP) y la "tecnología de corrutinas".



La programación de autómatas implementa fácilmente principios asincrónicos de diseño de programas, paralelismo de procesos y, al mismo tiempo, todo lo que la mente de un programador puede pensar. Mis artículos anteriores hablan de esto con más detalle, comenzando con la descripción del modelo estructural de computación automática y su definición formal a ejemplos de su aplicación. El código anterior en Python demuestra la implementación automática de los principios de corrutina de las corrutinas, superponiéndolas completamente, completándolas y extendiéndolas con el modelo de máquina de estados.



Listado 2. Enchufes en máquinas
import socket
from select import select

timeout = 0.0; classes = []

class Server:
    def __init__(self): self.nState = 0;

    def x1(self):
        self.ready_client, _, _ = select([self.server_socket], [self.server_socket], [], timeout)
        return self.ready_client

    def y1(self):
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.server_socket.bind(('localhost', 5001))
        self.server_socket.listen()
    def y2(self):
        self.client_socket, self.addr = self.server_socket.accept()
        print('Connection from', self.addr)
        classes.append(Client(self.client_socket, self.addr))

    def loop(self):
        if (self.nState == 0):      self.y1();      self.nState = 1
        elif (self.nState == 1):
            if (self.x1()):         self.y2();      self.nState = 0

class Client:
    def __init__(self, soc, adr): self.client_socket = soc; self.addr = adr; self.nState = 0

    def x1(self):
        self.ready_client, _, _ = select([self.client_socket], [], [], timeout)
        return self.ready_client
    def x2(self):
        _, self.write_client, _ = select([], [self.client_socket], [], timeout)
        return self.write_client
    def x3(self): return self.request

    def y1(self): self.request = self.client_socket.recv(4096);
    def y2(self): self.response = 'Hello World\n'.encode(); self.client_socket.send(self.response)
    def y3(self): self.client_socket.close(); print('close Client', self.addr)

    def loop(self):
        if (self.nState == 0):
            if (self.x1()):                     self.y1(); self.nState = 1
        elif (self.nState == 1):
            if (not self.x3()):                 self.y3(); self.nState = 4
            elif (self.x2() and self.x3()):     self.y2(); self.nState = 0

class EventLoop:
    def __init__(self): self.nState = 0; self.i = 0

    def x1(self): return self.i < len(classes)

    def y1(self): self.i = 0
    def y2(self): classes[self.i].loop()
    def y3(self): self.i += 1
    def y4(self):
        if (classes[self.i].nState == 4):
            classes.pop(self.i)
            self.i -= self.i

    def loop(self):
        if (self.nState == 0):
            if (not self.x1()): self.y1();
            if (self.x1()):     self.y2(); self.y4(); self.y3();

namSrv = Server(); namEv = EventLoop()
while True:
    namSrv.loop(); namEv.loop()




El código del Listado 2 es mucho más avanzado tecnológicamente que el código del Listado 1. Y este es el mérito del modelo automático de cálculos. Esto se ve facilitado por la integración del comportamiento del autómata en el modelo de objetos de programación. Como resultado, la lógica del comportamiento de los procesos de los autómatas se concentra exactamente donde se genera y no se delega, como se practica en las corrutinas, en el ciclo de eventos del control del proceso. La nueva solución provoca la creación de un "bucle de eventos" universal, cuyo prototipo puede considerarse el código de la clase EventLoop.



4. Acerca de los principios SRP y DRY



Los principios de “responsabilidad única” - SRP (El principio de responsabilidad única) y “no te repitas” - DRY (no te repitas) se expresan en el contexto de otro video de Oleg Molchanov [11] . Según ellos, la función debe contener solo el código de destino para no violar el principio SRY, y no promover la repetición de "código extra" para no violar el principio DRY. Para ello, se propone utilizar decoradores. Pero hay otra solución, una automática.



En el artículo anterior [2]inconscientes de la existencia de tales principios, se dio un ejemplo utilizando decoradores. Considerado un contador, que, por cierto, podría generar listas si se desea. Se menciona el objeto cronómetro que mide el tiempo de ejecución del contador. Si los objetos se ajustan a los principios SRP y DRY, entonces su funcionalidad no es tan importante como el protocolo de comunicación. En la implementación, el código del contador no tiene nada que ver con el código del cronómetro y cambiar cualquiera de los objetos no afectará al otro. Están obligados únicamente por el protocolo, sobre el cual los objetos se ponen de acuerdo "en la orilla" y luego lo siguen estrictamente.



Por lo tanto, un modelo de autómata paralelo esencialmente anula las capacidades de los decoradores. Es más flexible y más fácil implementar sus capacidades, porque no "rodea" (no decora) el código de función. Con el propósito de una evaluación objetiva y comparación del autómata y la tecnología convencional, el Listado 3 muestra un objeto análogo del contador discutido en el artículo anterior [2] , donde se presentan versiones simplificadas con los tiempos de su ejecución y la versión original del contador después de los comentarios.



Listado 3. Implementación de contador automático
import time
# 1) 110.66 sec
class PCount:
    def __init__(self, cnt ): self.n = cnt; self.nState = 0
    def x1(self): return self.n > 0
    def y1(self): self.n -=1
    def loop(self):
        if (self.nState == 0 and self.x1()):
            self.y1();
        elif (self.nState == 0 and not self.x1()):  self.nState = 4;

class PTimer:
    def __init__(self, p_count):
        self.st_time = time.time(); self.nState = 0; self.p_count = p_count
#    def x1(self): return self.p_count.nStat == 4 or self.p_count.nState == 4
    def x1(self): return self.p_count.nState == 4
    def y1(self):
        t = time.time() - self.st_time
        print ("speed CPU------%s---" % t)
    def loop(self):
       if (self.nState == 0 and self.x1()): self.y1(); self.nState = 1
       elif (self.nState == 1): pass

cnt1 = PCount(1000000)
cnt2 = PCount(10000)
tmr1 = PTimer(cnt1)
tmr2 = PTimer(cnt2)
# event loop
while True:
    cnt1.loop(); tmr1.loop()
    cnt2.loop(); tmr2.loop()

# # 2) 73.38 sec
# class PCount:
#     def __init__(self, cnt ): self.n = cnt; self.nState = 0
#     def loop(self):
#         if (self.nState == 0 and self.n > 0): self.n -= 1;
#         elif (self.nState == 0 and not self.n > 0):  self.nState = 4;
# 
# class PTimer:
#     def __init__(self): self.st_time = time.time(); self.nState = 0
#     def loop(self):
#        if (self.nState == 0 and cnt.nState == 4):
#            t = time.time() - self.st_time
#            print("speed CPU------%s---" % t)
#            self.nState = 1
#        elif (self.nState == 1): exit()
# 
# cnt = PCount(100000000)
# tmr = PTimer()
# while True:
#     cnt.loop();
#     tmr.loop()

# # 3) 35.14 sec
# class PCount:
#     def __init__(self, cnt ): self.n = cnt; self.nState = 0
#     def loop(self):
#         if (self.nState == 0 and self.n > 0):
#             self.n -= 1;
#             return True
#         elif (self.nState == 0 and not self.n > 0):  return False;
#
# cnt = PCount(100000000)
# st_time = time.time()
# while cnt.loop():
#     pass
# t = time.time() - st_time
# print("speed CPU------%s---" % t)

# # 4) 30.53 sec
# class PCount:
#     def __init__(self, cnt ): self.n = cnt; self.nState = 0
#     def loop(self):
#         while self.n > 0:
#             self.n -= 1;
#             return True
#         return False
#
# cnt = PCount(100000000)
# st_time = time.time()
# while cnt.loop():
#     pass
# t = time.time() - st_time
# print("speed CPU------%s---" % t)

# # 5) 18.27 sec
# class PCount:
#     def __init__(self, cnt ): self.n = cnt; self.nState = 0
#     def loop(self):
#         while self.n > 0:
#             self.n -= 1;
#         return False
# 
# cnt = PCount(100000000)
# st_time = time.time()
# while cnt.loop():
#     pass
# t = time.time() - st_time
# print("speed CPU------%s---" % t)

# # 6) 6.96 sec
# def count(n):
#   st_time = time.time()
#   while n > 0:
#     n -= 1
#   t = time.time() - st_time
#   print("speed CPU------%s---" % t)
#   return t
#
# def TestTime(fn, n):
#   def wrapper(*args):
#     tsum=0
#     st = time.time()
#     i=1
#     while (i<=n):
#       t = fn(*args)
#       tsum +=t
#       i +=1
#     return tsum
#   return wrapper
#
# test1 = TestTime(count, 2)
# tt = test1(100000000)
# print("Total ---%s seconds ---" % tt)




Resumamos los tiempos de funcionamiento de varias opciones en una tabla y comentemos los resultados del trabajo.



  1. Implementación de autómata clásico - 110,66 segundos
  2. Implementación de autómatas sin métodos de autómatas: 73,38 segundos
  3. Sin cronómetro automático - 35,14
  4. Contador en el formulario mientras con salida en cada iteración - 30.53
  5. Contador con ciclo de bloqueo - 18,27
  6. Mostrador original con decorador - 6,96


La primera opción, que representa el modelo de contador automático en su totalidad, es decir el contador y el cronómetro tienen el tiempo de funcionamiento más largo. El tiempo de funcionamiento se puede reducir renunciando, por así decirlo, a los principios de la tecnología automática. De acuerdo con esto, en la opción 2, las llamadas a predicados y acciones se reemplazan por su código. De esta forma ahorramos tiempo en los operadores de llamadas a métodos y esto es bastante notable, es decir en más de 30 segundos, redujo el tiempo de operación.



Guardamos un poco más en la tercera opción, creando una implementación de contador más simple, pero con una salida de ella en cada iteración del ciclo de contador (imitación de la operación de corrutina). Al eliminar la suspensión del mostrador (ver opción 5), logramos la mayor reducción en el trabajo del mostrador. Pero al mismo tiempo perdimos las ventajas del trabajo de rutina. Opción 6: este es el contador original con un decorador ya repetido y tiene el tiempo de ejecución más corto. Pero, al igual que la opción 5, esta es una implementación de bloqueo, que no nos conviene en el contexto de la discusión de la operación de rutina de las funciones.



5. Conclusiones



Ya sea para utilizar la tecnología de autómatas o confiar en las corrutinas, la decisión recae enteramente en el programador. Es importante para nosotros aquí que él sepa que existe un enfoque / tecnología diferente a las corrutinas para el diseño de programas. Incluso puedes imaginar la siguiente opción exótica. Primero, en la etapa de diseño del modelo, se crea un modelo de solución de autómata. Es rigurosamente científico, basado en evidencia y bien documentado. Entonces, por ejemplo, para mejorar el rendimiento, se "desfigura" a una versión "normal" del código, como lo demuestra el Listado 3. Incluso puede imaginar una "refactorización inversa" del código, es decir la transición de la 7ª opción a la 1ª, pero esto, aunque posible, pero el curso de los acontecimientos menos probable :)



En la fig. 2 muestra diapositivas del video sobre el tema "asincrónico" [10]... Y lo "malo" parece pesar más que lo "bueno". Y si en mi opinión los autómatas siempre son buenos, entonces en el caso de la programación asincrónica, elige, como dicen, a tu gusto. Pero parece que la opción "mala" será la más probable. Y el programador debe saber esto de antemano al diseñar un programa.



Figura: 2. Características de la programación asincrónica
image



Ciertamente, el código de autómata es algo "no exento de pecado". Tendrá una cantidad de código un poco mayor. Pero, en primer lugar, está mejor estructurado y, por tanto, es más fácil de entender y de mantener. Y, en segundo lugar, no siempre será más grande, porque con una complejidad creciente, lo más probable es que incluso haya una recompensa (debido, por ejemplo, a la reutilización de métodos de autómatas) Es más fácil y claro de depurar. Sí, al final del día es completamente SRP y DRY. Y esto, a veces, pesa mucho.



Sería deseable, y tal vez incluso necesario, prestar atención a, digamos, el estándar para el diseño de funciones. El programador debería, en la medida de lo posible, evitar diseñar funciones de bloqueo. Para hacer esto, solo debe iniciar el proceso de cálculo, que luego se verifica que esté completo, o tener medios para verificar la preparación para comenzar, como la función de selección considerada en los ejemplos. El código que utiliza funciones que se remontan a la época de DOS, que se muestra en el Listado 4, indica que tales problemas tienen una larga historia de "prerutina".



Listado 4. Lectura de caracteres desde el teclado
/*
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    int C=0;
    while (C != 'e')
    {
        C = getch();
        putchar (C);
    }
    return a.exec();
}
*/
//*
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    int C=0;
    while (C != 'e')
    {
        if (kbhit()) {
            C = getch();
            putch(C);
        }
    }

    return a.exec();
}
//*/




Aquí hay dos opciones para leer caracteres desde el teclado. La primera opción es bloquear. Bloqueará el cálculo y no ejecutará la instrucción para generar el carácter hasta que la función getch () lo reciba del teclado. En la segunda variante, la misma función se lanzará solo en el momento adecuado, cuando la función emparejada kbhit () confirme que el carácter está en el búfer de entrada. Por tanto, no habrá bloqueo de cálculos.



Si la función es "pesada" en sí misma, es decir requiere una cantidad significativa de tiempo para trabajar, y la salida periódica de ella por el tipo de trabajo de las corrutinas (esto se puede hacer sin usar el mecanismo de las mismas corrutinas, para no vincularse a ella) es difícil de hacer o no tiene mucho sentido, luego queda colocar tales funciones en un hilo separado y luego controlar la finalización de su trabajo (ver la implementación de la clase QCount en [2]).



Siempre puede encontrar una salida para excluir el bloqueo de cálculos. Arriba, mostramos cómo se puede crear código asíncrono dentro del marco de los medios habituales del lenguaje, sin utilizar el mecanismo de corrutina / corrutina e incluso cualquier entorno especializado como el entorno de programación automática VKP (a). Y qué y cómo usar depende del programador para decidir.



Literatura



1. Podcast de Python Junior. Acerca de la asincronía en Python. [Recurso electrónico], modo de acceso: www.youtube.com/watch?v=Q2r76grtNeg , gratis. Yaz. ruso (fecha de tratamiento 13/07/2020).

2. Simultaneidad y eficiencia: Python vs FSM. [Recurso electrónico], modo de acceso: habr.com/ru/post/506604 , gratis. Idioma. ruso (fecha de tratamiento 13/07/2020).

3. Molchanov O. Fundamentos de asincronía en Python # 4: Generadores y el ciclo de eventos Round Robin. [Recurso electrónico], modo de acceso: www.youtube.com/watch?v=PjZUSSkGLE8 ], gratis. Idioma. ruso (fecha de tratamiento 13/07/2020).

4. 48 Generadores e iteradores. Expresiones generadoras en Python. [Recurso electrónico], modo de acceso: www.youtube.com/watch?v=vn6bV6BYm7w, gratis. Idioma. ruso (fecha de tratamiento 13/07/2020).

5. Memorización y curry (Python). [Recurso electrónico], modo de acceso: habr.com/ru/post/335866 , gratis. Yaz. ruso (fecha de tratamiento 13/07/2020).

6. Lyubchenko V.S. Sobre lidiar con la recursividad. "PC World", nº 11/02. www.osp.ru/pcworld/2002/11/164417

7. Molchanov O. Lecciones de Python reparto # 10 - What is yield. [Recurso electrónico], modo de acceso: www.youtube.com/watch?v=ZjaVrzOkpZk , gratis. Idioma. ruso (fecha de tratamiento 18/07/2020).

8. Molchanov O. Fundamentos de async en Python # 5: Async en generadores. [Recurso electrónico], modo de acceso: www.youtube.com/watch?v=hOP9bKeDOHs , gratis. Idioma. ruso (fecha de tratamiento 13/07/2020).

9. Modelo de computación paralela. [Recurso electrónico], modo de acceso: habr.com/ru/post/486622 , gratis. Idioma. ruso (fecha de tratamiento 20/07/2020).

10. Polishchuk A. Asynchronous en Python. [Recurso electrónico], modo de acceso: www.youtube.com/watch?v=lIkA0TDX8tE , gratis. Yaz. ruso (fecha de tratamiento 13/07/2020).

11. Molchanov O. Lecciones Reparto de Python n. ° 6 - Decoradores. [Recurso electrónico], modo de acceso: www.youtube.com/watch?v=Ss1M32pp5Ew , gratis. Idioma. ruso (fecha de tratamiento 13/07/2020).



All Articles