Um mundo sem corrotinas. Iteradores do gerador

1. Introdução



Para confundir o problema tanto quanto possível, confie a solução aos programadores;). Mas falando sério, na minha opinião algo semelhante acontece com as co-rotinas, porque, voluntariamente ou não, elas são usadas para borrar a situação. Este último é caracterizado pelo fato de que ainda existem problemas de programação paralela que não vão a lugar nenhum e, o mais importante, as corrotinas não contribuem para sua solução fundamental.



Vamos começar com a terminologia. "Quantas vezes eles disseram ao mundo", mas até agora "o mundo" ainda está perguntando sobre a diferença entre a programação assíncrona e a programação paralela (veja a discussão sobre o tema assincronia em [1] ). O ponto crucial do problema de entender assincronia versus paralelismo começa com a definição do próprio paralelismo. Isso simplesmente não existe. Existe algum tipo de compreensão intuitiva, que muitas vezes é interpretada de maneiras diferentes, mas não existe uma definição científica que remova todas as questões de forma tão construtiva quanto uma discussão sobre o resultado da operação "dois e dois".



E como, mais uma vez, tudo isso não existe, então, confuso em termos e conceitos, ainda distinguimos entre programação paralela e concorrente, assíncrona, reativa e alguma outra, etc. etc. Eu acho que seria improvável que houvesse um problema em perceber que uma calculadora mecânica como o Felix funciona de maneira diferente de uma calculadora de software. Mas de um ponto de vista formal, ou seja, um conjunto de operações e o resultado final, não há diferença entre eles. Este princípio deve ser levado em consideração na definição da programação paralela.



Devemos ter uma definição estrita e meios transparentes de descrever o paralelismo, levando a resultados consistentes como o "desajeitado" Felix e qualquer calculadora de software. É impossível que o conceito de "paralelismo" seja associado aos meios de sua implementação (com o número dos mesmos núcleos). E o que há “por baixo do capô” - isso deve ser de interesse principalmente apenas para aqueles que estão envolvidos na implementação de “máquinas”, mas não para aqueles que usam uma “calculadora paralela” convencional.



Mas temos o que temos. E temos, se não uma mania, então uma discussão ativa sobre corrotinas e programação assíncrona. E o que mais fazer se parecermos estar fartos de multithreading e algo mais não for oferecido? Eles até falam sobre algum tipo de magia;) Mas tudo se torna óbvio se você entender os motivos. E eles estão exatamente lá - no plano de paralelismo. Sua definição e sua implementação.



Mas vamos descer das alturas globais e, até certo ponto, filosóficas da ciência da programação (desde computador) até nossa "terra pecaminosa". Aqui, sem diminuir os méritos da linguagem Kotlin atualmente popular, eu gostaria de admitir minha paixão pela linguagem Python. Talvez algum dia e em alguma outra situação, minhas preferências mudem, mas na verdade, até agora tudo está assim.



Há várias razões para isso. Entre eles está o acesso gratuito ao Python. Este não é o argumento mais forte, uma vez que um exemplo com o mesmo Qt diz que a situação pode mudar a qualquer momento. Mas embora Python, ao contrário de Kotlin, seja gratuito, pelo menos na forma do mesmo ambiente PyCharm da JetBrains (pelo qual agradecimentos especiais a eles), minhas simpatias estão do lado dele. Também é atraente que haja uma massa de literatura em russo, exemplos em Python na Internet, tanto educacionais quanto reais. Em Kotlin, eles não estão nesse número e sua variedade não é tão grande.



Talvez um pouco à frente da curva, decidi apresentar os resultados de dominar Python no contexto de questões de definição e implementação de paralelismo e assincronia de software. Isso foi iniciado pelo artigo [2]... Hoje vamos considerar o tópico geradores-corrotinas. Meu interesse por eles é alimentado pela necessidade de estar ciente das possibilidades específicas, interessantes, mas não muito familiares para mim no momento, das linguagens / linguagens de programação modernas.



Como eu sou um programador C ++ puro, isso explica muita coisa. Por exemplo, se em Python co-rotinas e geradores estão presentes há muito tempo, em C ++ eles ainda não conquistaram seu lugar. Mas o C ++ realmente precisa disso? Na minha opinião, a linguagem de programação precisa ser estendida razoavelmente. Parece que o C ++ puxou o máximo possível e agora está tentando rapidamente alcançá-lo. Mas problemas de simultaneidade semelhantes podem ser implementados usando outros conceitos e modelos que são mais fundamentais do que corrotinas / corrotinas. E o fato de que por trás dessa afirmação não existem apenas palavras será demonstrado mais adiante.



Se devemos admitir tudo, também admito que sou bastante conservador em relação ao C ++. Claro, seus objetos e recursos OOP são "nosso tudo" para mim, mas eu, digamos, sou crítico em relação aos modelos. Bem, eu nunca realmente olhei para sua peculiar "linguagem de pássaros", que, ao que parecia, complica muito a percepção do código e a compreensão do algoritmo. Embora ocasionalmente eu até recorresse à ajuda deles, os dedos de uma mão bastam para tudo isso. Respeito a biblioteca STL e não posso ficar sem ela :) Portanto, mesmo por isso, às vezes tenho dúvidas sobre os templates. Então, eu ainda os evito tanto quanto posso. E agora estou esperando com um estremecimento por "co-rotinas de modelo" em C ++;)



Python é outra questão. Eu não notei nenhum padrão nele ainda e isso me acalma. Mas, por outro lado, isso é, estranhamente, alarmante. Porém, quando olho para o código do Kotlin e, principalmente, para o código do compartimento do motor, a ansiedade passa rapidamente;) No entanto, acho que isso ainda é uma questão de hábito e meus preconceitos. Espero que com o tempo eu me treine para percebê-los adequadamente (modelos).



Mas ... de volta às corrotinas. Acontece que agora eles estão sob o nome de corutin. O que há de novo com a mudança de nome? Sim, na verdade nada. Como antes, o conjunto é considerado por sua vezfunções executadas. Da mesma forma que antes, antes de sair da função, mas antes da conclusão do seu trabalho, é fixado o ponto de retorno, a partir do qual o trabalho é retomado posteriormente. Uma vez que a sequência de comutação não é estipulada, o próprio programador controla este processo criando seu próprio escalonador. Freqüentemente, isso é apenas um loop de funções. Como, por exemplo, o ciclo de eventos Round Robin no vídeo de Oleg Molchanov [3] .



É assim que uma introdução moderna às co-rotinas de co-rotina e programação assíncrona geralmente se parece com "nos dedos". É claro que com a imersão neste tópico, novos termos e conceitos surgem. Os geradores são um deles. Além disso, o exemplo deles será a base para demonstrar "preferências paralelas", mas já na minha interpretação automática.



2. Geradores de listas de dados



Então - geradores. A programação assíncrona e as corrotinas são frequentemente associadas a eles. Uma série de vídeos de Oleg Molchanov fala sobre tudo isso. Portanto, ele se refere à característica chave dos geradores como sua “capacidade de pausar a execução de uma função para continuar sua execução do mesmo lugar em que parou da última vez” (para mais detalhes, ver [3] ). E nisso, dado o que foi dito acima sobre a já bastante antiga definição de co-rotinas, não há nada de novo.



Mas acontece que os geradores encontraram um uso bastante específico para a criação de listas de dados. Uma introdução a este tópico já foi abordada em um vídeo de Egorov Artem [4]... Mas, ao que parece, com sua aplicação, misturamos conceitos qualitativamente diferentes - operações e processos. Ao expandir as capacidades descritivas da linguagem, ignoramos amplamente os problemas que podem surgir. Aqui, como se costuma dizer, para não jogar muito. Usar geradores-co-rotinas para descrever dados contribui exatamente para isso, parece-me. Observe que Oleg Molchanov também alerta contra a associação de geradores com estruturas de dados, enfatizando que “geradores são funções” [3] .



Mas voltando a usar geradores para definir dados. É difícil esconder que criamos um processo que calcula os itens da lista. Portanto, questões surgem imediatamente para essa lista como para um processo. Por exemplo, como reutilizá-lo se as corrotinas, por definição, funcionam apenas "de uma maneira"? Como calcular um elemento arbitrário dele se a indexação do processo é impossível? Etc. etc. Artem não responde a essas perguntas, apenas avisa que, dizem, o acesso repetido aos elementos da lista não pode ser organizado e a indexação é inaceitável. Uma busca na Internet nos convence de que não só eu tenho dúvidas semelhantes, mas as soluções que são propostas não são tão triviais e óbvias.



Outro problema é a velocidade de geração da lista. Agora formamos um único elemento da lista em cada switch de co-rotina, e isso aumenta o tempo de geração de dados. O processo pode ser bastante acelerado gerando elementos em “lotes”. Mas, provavelmente, haverá problemas com isso. Como parar um processo já em execução? Ou alguma outra coisa. A lista pode ser bem longa, usando apenas itens selecionados. Em tal situação, a memorização de dados é freqüentemente usada para acesso eficiente. A propósito, quase imediatamente encontrei um artigo sobre este tópico para Python, consulte [5] (para mais informações sobre memoização em termos de autômatos, consulte o artigo [6] ). Mas e neste caso?



A confiabilidade de tal sintaxe para definir listas também pode ser questionável, uma vez que é fácil usar equivocadamente colchetes em vez de parênteses e vice-versa. Acontece que uma solução aparentemente bonita e elegante na prática pode levar a certos problemas. Uma linguagem de programação deve ser tecnológica, flexível e segura contra erros involuntários.



A propósito, no tópico de listas e geradores sobre suas vantagens e desvantagens, cruzando com as observações acima, você pode assistir a outro vídeo de Oleg Molchanov [7] .



3. Geradores-corrotinas



O próximo vídeo de Oleg Molchanov [8] discute o uso de geradores para coordenar o trabalho de corrotinas. Na verdade, eles são destinados a isso. Chama-se a atenção para a escolha de momentos para alternar as corrotinas. Sua disposição segue uma regra simples - colocamos a declaração de rendimento antes das funções de bloqueio. Estas últimas são entendidas como funções, cujo tempo de retorno é tão longo em comparação com outras operações que os cálculos estão associados à sua detenção. Por causa disso, eles foram chamados de bloqueadores.



A comutação é eficaz quando o processo suspenso continua seu trabalho exatamente quando a chamada de bloqueio não espera, mas conclui rapidamente seu trabalho. E, ao que parece, por causa disso, todo esse "rebuliço" foi iniciado em torno do modelo de co-rotina / co-rotina e, consequentemente, um impulso foi dado ao desenvolvimento da programação assíncrona. Embora, notamos, a ideia original de co-rotinas ainda fosse diferente - criar um modelo virtual de computação paralela.



No vídeo em consideração, como no caso geral das corrotinas, a continuação da operação da corrotina é determinada pelo ambiente externo, que é um planejador de eventos. Nesse caso, ele é representado por uma função chamada event_loop. E, ao que parece, tudo é lógico: o escalonador fará a análise e continuará o trabalho da co-rotina chamando o operador next () exatamente quando necessário. O problema está na espera onde não era esperado: o escalonador pode ser bastante complexo. No vídeo anterior de Molchanov ( ver [3] ), tudo era simples, já que foi realizada uma simples transferência alternada de controle, na qual não houve travas, uma vez que não houve chamadas correspondentes. No entanto, ressaltamos que em qualquer caso, pelo menos um simples escalonador é necessário.



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



2. , select, — . .



Mas a questão nem mesmo é a necessidade de um planejador, mas o fato de ele assumir funções incomuns para ele. A situação é ainda mais complicada pelo fato de que é necessário implementar um algoritmo para a operação conjunta de muitas co-rotinas. A comparação dos escalonadores considerados nos dois vídeos mencionados por Oleg Molchanov reflete um problema semelhante de forma bastante clara: o algoritmo de escalonamento de soquete em [8] é notavelmente mais complicado do que o algoritmo de carrossel em [3] .



3. Para um mundo sem corrotinas



Como temos certeza de que um mundo sem co-rotinas é possível, opondo-as a autômatos, é necessário mostrar como tarefas semelhantes já são resolvidas por eles. Vamos demonstrar isso usando o mesmo exemplo de trabalho com soquetes. Observe que sua implementação inicial acabou por não ser tão trivial que pudesse ser entendida imediatamente. Isso é repetidamente enfatizado pelo próprio autor do vídeo. Outros enfrentam problemas semelhantes no contexto de corrotinas. Portanto, as desvantagens das corrotinas associadas à complexidade de sua percepção, compreensão, depuração, etc. discutido no vídeo [10] .



Em primeiro lugar, algumas palavras sobre a complexidade do algoritmo em consideração. Isso se deve à natureza dinâmica e plural dos processos de atendimento ao cliente. Para isso, é criado um servidor que escuta em uma determinada porta e, conforme aparecem as solicitações, gera várias funções de atendimento ao cliente que a alcançam. Como pode haver muitos clientes, eles aparecem de forma imprevisível, uma lista dinâmica é criada a partir dos processos de manutenção de soquetes e troca de informações com eles. O código para a solução do gerador Python discutido no vídeo [8] é mostrado na Listagem 1.



Listagem 1. Sockets em geradores
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()




Os algoritmos de servidor e cliente são bastante básicos. Mas deve ser alarmante que o servidor coloque a função do cliente na lista de tarefas. Além disso - mais: é difícil entender o algoritmo do loop de eventos event_loop. Até que a lista de tarefas possa estar vazia, se pelo menos o processo do servidor sempre estiver presente nela? ..



Em seguida, os dicionários to_read e to_write são introduzidos. O próprio trabalho com dicionários requer uma explicação separada. é mais difícil do que trabalhar com listas regulares. Por isso, as informações retornadas pelas declarações de rendimento são personalizadas para eles. Então a "dança de pandeiro" começa em torno dos dicionários e tudo se torna uma espécie de "fervilhar": algo parece estar colocado nos dicionários, de onde entra na lista de tarefas, etc. etc. Você pode "quebrar sua cabeça", resolvendo tudo isso.



E como será a solução da tarefa em questão? Seria lógico que os autômatos criassem modelos equivalentes aos processos de trabalho com soquetes já discutidos no vídeo. No modelo de servidor, parece que nada precisa ser alterado. Este será um autômato que funcionará como a função server (). Seu gráfico é mostrado na Fig. 1a. A ação de autômato y1 () cria um soquete de servidor e o conecta à porta especificada. O predicado x1 () define a conexão do cliente e, se presente, a ação y2 () cria um processo de serviço de soquete do cliente, colocando este último na lista de processos de classes, que inclui as classes de objetos ativos.



Na fig. 1b mostra um gráfico do modelo para um cliente individual. Estando no estado "0", o autômato determina a prontidão do cliente para transmitir informações (predicado x1 () - verdadeiro) e recebe uma resposta dentro da ação y1 () na transição para o estado "1". Além disso, quando o cliente está pronto para receber informações (já x2 () deve ser verdadeiro), a ação y2 () implementa a operação de envio de uma mensagem ao cliente na transição para o estado inicial "0". Se o cliente interromper a conexão com o servidor (neste caso, x3 () é falso), o autômato passa para o estado "4", fechando o soquete do cliente na ação y3 (). O processo permanece no estado "4" até que seja excluído da lista de classes ativas (veja a descrição acima do modelo de servidor para a formação da lista).



Na fig. 1c mostra um autômato que implementa o lançamento de processos semelhantes à função event_loop () na Listagem 1. Somente neste caso, seu algoritmo de operação é muito mais simples. Tudo se resume ao fato de que a máquina percorre os elementos da lista de classes ativas e chama o método loop () para cada uma delas. Esta ação é implementada por y2 (). A ação y4 () exclui da lista as classes que estão no estado "4". O resto das ações funcionam com o índice da lista de objetos: a ação y3 () aumenta o índice, a ação y1 () o redefine.



Os recursos de programação de objetos em Python são diferentes da programação de objetos em C ++. Portanto, a implementação mais simples do modelo de autômato será tomada como base (para ser mais preciso, é uma imitação de um autômato). Baseia-se no princípio de objeto de representar processos, dentro do qual cada processo corresponde a uma classe ativa separada (muitas vezes também são chamados de agentes). A classe contém as propriedades e métodos necessários (veja mais detalhes sobre métodos específicos de autômato - predicados e ações em [9] ), e a lógica de operação do autômato (suas funções de transição e saída) é concentrada dentro da estrutura do método chamado loop (). Para implementar a lógica de comportamento do autômato, usaremos a construção if-elif-else.



Com essa abordagem, o "loop de eventos" não tem nada a ver com a análise da disponibilidade de soquetes. Eles são verificados pelos próprios processos, que usam a mesma instrução select dentro dos predicados. Nesta situação, eles operam com um único socket, e não com uma lista deles, verificando-se a operação que se espera para este socket em particular e precisamente na situação que é determinada pelo algoritmo de operação. A propósito, durante a depuração de tal implementação, uma essência bloqueadora inesperada da instrução select apareceu.



Figura: 1. Gráficos de processos automáticos para trabalhar com soquetes
image



A Listagem 2 mostra um código de objeto de autômato em Python para trabalhar com soquetes. Este é o nosso tipo de "mundo sem corrotinas". É um "mundo" com diferentes princípios para projetar processos de software. É caracterizada pela presença de um modelo algorítmico de cálculos paralelos (para mais detalhes ver [9] , que é a principal e qualitativa diferença entre a tecnologia de programação de autômato (AP) e a "tecnologia de co-rotina".



A programação de autômatos implementa facilmente princípios assíncronos de design de programa, paralelismo de processo e, ao mesmo tempo, tudo que a mente de um programador pode imaginar. Meus artigos anteriores falam sobre isso com mais detalhes, começando com a descrição do modelo estrutural de computação automática e sua definição formal para exemplos de sua aplicação. O código acima em Python demonstra a implementação automática dos princípios da co-rotina das co-rotinas, sobrepondo-as completamente, complementando e estendendo-as com o modelo da máquina de estado.



Listagem 2. Sockets em 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()




O código da Listagem 2 é muito mais avançado tecnologicamente do que o código da Listagem 1. E esse é o mérito do modelo automático de cálculos. Isso é facilitado pela integração do comportamento do autômato no modelo de objeto de programação. Como resultado, a lógica de comportamento dos processos de autômato concentra-se exatamente onde é gerada, e não é delegada, como é praticado nas co-rotinas, no loop de eventos de controle do processo. A nova solução provoca a criação de um "loop de eventos" universal, cujo protótipo pode ser considerado o código da classe EventLoop.



4. Sobre os princípios SRP e DRY



Os princípios de "responsabilidade única" - SRP (o princípio da responsabilidade única) e "não se repita" - DRY (não se repita) são expressos no contexto de outro vídeo de Oleg Molchanov [11] . Segundo eles, a função deve conter apenas o código-alvo para não violar o princípio SRY, e não promover a repetição de “código extra” para não violar o princípio DRY. Para tal, propõe-se a utilização de decoradores. Mas existe outra solução - automática.



No artigo anterior [2]desconhecendo a existência de tais princípios, foi dado um exemplo com decoradores. Considerado um contador, que, aliás, poderia gerar listas se desejado. O objeto cronômetro que mede o tempo de execução do contador é mencionado. Se os objetos estiverem em conformidade com os princípios SRP e DRY, sua funcionalidade não será tão importante quanto o protocolo de comunicação. Na implementação, o código do contador não tem nada a ver com o código do cronômetro, e a alteração de qualquer um dos objetos não afetará o outro. Eles são limitados apenas pelo protocolo, sobre o qual os objetos concordam “na costa” e então o seguem estritamente.



Assim, um modelo de autômato paralelo essencialmente substitui as capacidades dos decoradores. É mais flexível e fácil de implementar seus recursos, porque não "envolve" (não decora) o código da função. Para fins de avaliação e comparação objetiva do autômato e da tecnologia convencional, a Listagem 3 mostra um objeto análogo ao contador discutido no artigo anterior [2] , onde versões simplificadas com os tempos de sua execução e a versão original do contador são apresentadas após os comentários.



Listagem 3. Implementação 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)




Vamos resumir os tempos de operação das várias opções em uma tabela e comentar os resultados do trabalho.



  1. Implementação de autômato clássico - 110,66 seg
  2. Implementação de autômatos sem métodos de autômatos - 73,38 seg
  3. Sem cronômetro automático - 35,14
  4. Contador na forma enquanto com saída em cada iteração - 30,53
  5. Contador com ciclo de bloqueio - 18,27
  6. Balcão original com decorador - 6,96


A primeira opção, que representa o modelo de contador automático na íntegra, ou seja, o contador e o cronômetro têm o tempo de execução mais longo. O tempo de operação pode ser reduzido abandonando, por assim dizer, os princípios da tecnologia automática. Consequentemente, na opção 2, as chamadas para predicados e ações são substituídas por seu código. Dessa forma, economizamos tempo nos operadores de chamada de método e isso é bastante perceptível, ou seja, por mais de 30 segundos, reduziu o tempo de operação.



Economizamos um pouco mais na 3ª opção, criando uma implementação de contador mais simples, mas com uma saída dela a cada iteração do ciclo do contador (imitação da operação de co-rotina). Ao eliminar a suspensão do balcão (ver opção 5), conseguimos a redução mais forte no trabalho do balcão. Mas, ao mesmo tempo, perdemos as vantagens do trabalho de co-rotina. Opção 6 - este é o balcão original com um decorador já repetido e com menor tempo de execução. Mas, como a opção 5, esta é uma implementação de bloqueio, o que não pode nos servir no contexto de discutir a operação de co-rotina de funções.



5. Conclusões



Se usar tecnologia de autômato ou co-rotinas de confiança - a decisão é inteiramente do programador. É importante para nós aqui que ele saiba que existe uma abordagem / tecnologia diferente para projetar programas do que as corrotinas. Você pode até imaginar a seguinte opção exótica. Primeiro, no estágio de projeto do modelo, um modelo de solução de autômato é criado. É rigorosamente científico, baseado em evidências e bem documentado. Então, por exemplo, para melhorar o desempenho, ele é “desfigurado” para uma versão “normal” do código, como demonstra a Listagem 3. Você pode até imaginar uma “refatoração reversa” do código, ou seja, a transição da 7ª opção para a 1ª, mas esta, embora possível, mas o curso menos provável dos eventos :)



Na fig. 2 mostra slides do vídeo sobre o tema "assíncrono" [10]... E o “mau” parece prevalecer sobre o “bom”. E se na minha opinião os autômatos são sempre bons, então no caso da programação assíncrona, escolha, como dizem, ao seu gosto. Mas parece que a opção "ruim" será a mais provável. E o programador deve saber sobre isso com antecedência ao projetar um programa.



Figura: 2. Características da programação assíncrona
image



Certamente, o código do autômato é um tanto "não sem pecado". Ele terá uma quantidade de código um pouco maior. Mas, primeiro, é mais bem estruturado e, portanto, mais fácil de entender e manter. E, em segundo lugar, nem sempre será maior, porque com o aumento da complexidade, muito provavelmente haverá até mesmo uma recompensa (devido, por exemplo, à reutilização de métodos de autômatos). É mais fácil e claro depurar. Sim, no final do dia é totalmente SRP e SECO. E isso, às vezes, supera muito.



Seria desejável, e talvez até necessário, prestar atenção, digamos, ao padrão de design de funções. O programador deve, tanto quanto possível, evitar projetar funções de bloqueio. Para fazer isso, ele deve apenas iniciar o processo de cálculo, que é verificado para integridade, ou ter os meios para verificar a prontidão para o lançamento, como a função de seleção considerada nos exemplos. A Listagem 4 mostra o código que usa funções que datam da época do DOS para mostrar que esses problemas têm um longo histórico pré-rotina.



Listagem 4. Lendo caracteres do 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();
}
//*/




Aqui estão duas opções para ler caracteres do teclado. A primeira opção é o bloqueio. Ele bloqueará o cálculo e não executará a instrução para gerar o caractere até que a função getch () o receba do teclado. Na segunda variante, a mesma função será iniciada somente no momento certo, quando a função kbhit () emparelhada com ela confirmar que o caractere está no buffer de entrada. Assim, não haverá bloqueio de cálculos.



Se a função for "pesada" em si mesma, ou seja, requer uma quantidade significativa de tempo para funcionar, e a saída periódica dele pelo tipo de trabalho de co-rotinas (isso pode ser feito sem usar o mecanismo das mesmas co-rotinas, de modo a não se vincular a elas) é difícil de fazer ou não faz muito sentido, então resta colocar tais funções em um thread separado e então controlar a conclusão de seu trabalho (ver a implementação da classe QCount em [2])



Você sempre pode encontrar uma maneira de excluir o bloqueio de computação. Acima, mostramos como é possível criar código assíncrono dentro da estrutura dos meios usuais da linguagem, sem usar o mecanismo de co-rotina / co-rotina e até mesmo qualquer ambiente especializado como o ambiente de programação automática VKP (a). E o que e como usar é decisão do programador.



Literatura



1. Podcast Python Junior. Sobre assincronia em python. [Recurso eletrônico], modo de acesso: www.youtube.com/watch?v=Q2r76grtNeg , gratuito. Yaz. russo (data do tratamento 13/07/2020).

2. Concorrência e eficiência: Python vs FSM. [Recurso eletrônico], modo de acesso: habr.com/ru/post/506604 , gratuito. Língua. russo (data do tratamento 13/07/2020).

3. Molchanov O. Fundamentos de assincronia em Python # 4: Geradores e o loop de eventos Round Robin. [Recurso eletrônico], modo de acesso: www.youtube.com/watch?v=PjZUSSkGLE8 ], gratuito. Língua. russo (data do tratamento 13/07/2020).

4. 48 geradores e iteradores. Expressões geradoras em Python. [Recurso eletrônico], modo de acesso: www.youtube.com/watch?v=vn6bV6BYm7w, livre. Yaz. russo (data do tratamento 13/07/2020).

5. Memoização e currying (Python). [Recurso eletrônico], Modo de acesso: habr.com/ru/post/335866 , gratuito. Língua. russo (data do tratamento 13/07/2020).

6. Lyubchenko V.S. Sobre como lidar com a recursão. "PC World", nº 11/02. www.osp.ru/pcworld/2002/11/164417

7. Molchanov O. Python Cast # 10 - O que é rendimento. [Recurso eletrônico], modo de acesso: www.youtube.com/watch?v=ZjaVrzOkpZk , gratuito. Yaz. russo (data do tratamento 18/07/2020).

8. Molchanov O. Fundamentos de assíncrono em Python # 5: Assíncrono em geradores. [Recurso eletrônico], modo de acesso: www.youtube.com/watch?v=hOP9bKeDOHs , gratuito. Língua. russo (data do tratamento 13/07/2020).

9. Modelo de computação paralela. [Recurso eletrônico], Modo de acesso: habr.com/ru/post/486622 , gratuito. Yaz. russo (data do tratamento 20/07/2020).

10. Polishchuk A. Assincronismo em Python. [Recurso eletrônico], modo de acesso: www.youtube.com/watch?v=lIkA0TDX8tE , gratuito. Língua. russo (data do tratamento 13/07/2020).

11. Molchanov O. Lessons Python cast # 6 - Decorators. [Recurso eletrônico], modo de acesso: www.youtube.com/watch?v=Ss1M32pp5Ew , gratuito. Yaz. russo (data do tratamento 13/07/2020).



All Articles