Eine Welt ohne Coroutinen. Generator-Iteratoren

1. Einleitung



Um das Problem so weit wie möglich zu verwirren, vertrauen Sie die Lösung den Programmierern an;). Aber im Ernst, meiner Meinung nach passiert etwas Ähnliches mit Coroutinen, weil sie bereitwillig oder nicht dazu verwendet werden, die Situation zu verwischen. Letzteres zeichnet sich dadurch aus, dass es immer noch Probleme der parallelen Programmierung gibt, die nirgendwo hingehen, und vor allem Coroutinen nicht zu ihrer grundlegenden Lösung beitragen.



Beginnen wir mit der Terminologie. "Wie oft haben sie es der Welt erzählt", aber bis jetzt stellt "die Welt" immer noch Fragen zum Unterschied zwischen asynchroner und paralleler Programmierung (siehe die Diskussion zum Thema Asynchronität in [1] ). Der Kern des Problems des Verständnisses von Asynchronität und Parallelität beginnt mit der Definition der Parallelität selbst. Es existiert einfach nicht. Es gibt eine Art intuitives Verständnis, das oft unterschiedlich interpretiert wird, aber es gibt keine wissenschaftliche Definition, die alle Fragen so konstruktiv wie eine Diskussion über das Ergebnis der Operation "zwei und zwei" entfernen würde.



Und da dies alles wiederum nicht existiert, unterscheiden wir immer noch verwirrt in Begriffen und Konzepten zwischen paralleler und gleichzeitiger Programmierung, asynchroner, reaktiver und einer anderen usw. usw. Ich denke, es ist unwahrscheinlich, dass es ein Problem gibt, zu erkennen, dass ein mechanischer Taschenrechner wie Felix anders funktioniert als ein Software-Taschenrechner. Aber aus formaler Sicht, d.h. Bei einer Reihe von Operationen und dem Endergebnis gibt es keinen Unterschied zwischen ihnen. Dieses Prinzip sollte bei der Definition der parallelen Programmierung berücksichtigt werden.



Wir müssen eine strenge Definition und transparente Mittel zur Beschreibung der Parallelität haben, die zu konsistenten Ergebnissen wie "ungeschicktem" Felix und jedem Software-Rechner führen. Es ist unmöglich, das Konzept der "Parallelität" mit den Mitteln seiner Implementierung (mit der Anzahl der gleichen Kerne) zu verknüpfen. Und was gibt es „unter der Haube“ - dies sollte in erster Linie nur für diejenigen von Interesse sein, die mit der Implementierung von „Maschinen“ befasst sind, nicht jedoch für diejenigen, die einen solchen bedingten „Parallelrechner“ verwenden.



Aber wir haben was wir haben. Und wir haben, wenn nicht sogar verrückt, eine aktive Diskussion über Coroutinen und asynchrone Programmierung. Und was tun, wenn wir das Multithreading satt zu haben scheinen, aber etwas anderes nicht angeboten wird? Sie reden sogar über irgendeine Art von Magie;) Aber alles wird offensichtlich, wenn man die Gründe versteht. Und sie liegen genau dort - in der Ebene der Parallelität. Seine Definition und seine Umsetzung.



Aber lassen Sie uns von den globalen und bis zu einem gewissen Grad philosophischen Höhen der Programmierwissenschaft (Computer seitdem) auf unsere "sündige Erde" hinabsteigen. Hier möchte ich meine Leidenschaft für Python zugeben, ohne die Vorzüge der derzeit beliebten Kotlin-Sprache zu beeinträchtigen. Vielleicht werden sich eines Tages und in einer anderen Situation meine Vorlieben ändern, aber tatsächlich ist bisher alles so.



Dafür gibt es mehrere Gründe. Darunter ist der freie Zugang zu Python. Dies ist seitdem nicht das stärkste Argument Ein Beispiel mit demselben Qt besagt, dass sich die Situation jederzeit ändern kann. Aber während Python im Gegensatz zu Kotlin kostenlos ist, zumindest in Form derselben PyCharm-Umgebung von JetBrains (wofür ich mich besonders danke), sind meine Sympathien auf seiner Seite. Es ist auch attraktiv, dass es eine Menge russischsprachiger Literatur gibt, Beispiele in Python im Internet, sowohl lehrreich als auch ziemlich real. Auf Kotlin sind sie nicht in dieser Anzahl und ihre Vielfalt ist nicht so groß.



Vielleicht ein wenig vor der Kurve, entschied ich mich, die Ergebnisse der Beherrschung von Python im Kontext von Fragen der Definition und Implementierung von Software-Parallelität und Asynchronität zu präsentieren. Dies wurde durch Artikel [2] initiiert.... Heute werden wir uns mit dem Thema Generatoren-Coroutinen befassen. Mein Interesse an ihnen wird durch die Notwendigkeit geweckt, mir der spezifischen, interessanten, aber mir derzeit nicht sehr vertrauten Möglichkeiten moderner Sprachen / Programmiersprachen bewusst zu sein.



Da ich eigentlich ein reiner C ++ - Programmierer bin, erklärt dies viel. Wenn beispielsweise in Python Coroutinen und Generatoren schon lange vorhanden sind, müssen sie in C ++ noch ihren Platz gewinnen. Aber braucht C ++ das wirklich? Meiner Meinung nach muss die Programmiersprache angemessen erweitert werden. Es scheint, dass C ++ so weit wie möglich gezogen hat, und jetzt versucht es hastig aufzuholen. Ähnliche Parallelitätsprobleme können jedoch mit anderen Konzepten und Modellen implementiert werden, die grundlegender sind als Coroutinen / Coroutinen. Und die Tatsache, dass hinter dieser Aussage nicht nur Worte stehen, wird weiter demonstriert.



Wenn wir alles zugeben wollen, dann gebe ich auch zu, dass ich in Bezug auf C ++ eher konservativ bin. Natürlich sind seine Objekte und OOP-Fähigkeiten für uns "unser Alles", aber ich, sagen wir, kritisiere Vorlagen. Nun, ich habe nie wirklich auf ihre eigentümliche "Vogelsprache" geschaut, was anscheinend die Wahrnehmung des Codes und das Verständnis des Algorithmus erheblich erschwert. Obwohl ich gelegentlich sogar auf ihre Hilfe zurückgegriffen habe, reichen die Finger einer Hand für all dies. Ich respektiere die STL-Bibliothek und kann nicht darauf verzichten :) Daher habe ich trotz dieser Tatsache manchmal Zweifel an Vorlagen. Also vermeide ich sie immer noch so weit ich kann. Und jetzt warte ich mit einem Schauder auf "Template Coroutines" in C ++;)



Python ist eine andere Sache. Ich habe noch keine Muster darin bemerkt und es beruhigt mich. Auf der anderen Seite ist dies seltsamerweise alarmierend. Wenn ich mir jedoch den Kotlin-Code und insbesondere den Motorraumcode anschaue, geht die Angst schnell vorbei;) Ich denke jedoch, dass dies immer noch eine Frage der Gewohnheit und meiner Vorurteile ist. Ich hoffe, dass ich mich im Laufe der Zeit darin üben werde, sie angemessen wahrzunehmen (Vorlagen).



Aber ... zurück zu den Coroutinen. Es stellt sich heraus, dass sie jetzt unter dem Namen Corutin sind. Was ist neu bei der Namensänderung? Ja eigentlich nichts. Wie zuvor wird der Satz betrachtet wiederumFunktionen ausgeführt. Nach wie vor, vor dem Verlassen der Funktion, aber vor Abschluss der Arbeit, wird der Rückgabepunkt festgelegt, von dem aus die Arbeit anschließend wieder aufgenommen wird. Da die Schaltsequenz nicht festgelegt ist, steuert der Programmierer diesen Prozess selbst, indem er seinen eigenen Scheduler erstellt. Oft ist dies nur ein Durchlaufen von Funktionen. Wie zum Beispiel der Round Robin-Ereigniszyklus im Video von Oleg Molchanov [3] .



So sieht eine moderne Einführung in Coroutine Coroutines und asynchrone Programmierung normalerweise "an den Fingern" aus. Es ist klar, dass beim Eintauchen in dieses Thema neue Begriffe und Konzepte entstehen. Generatoren sind einer von ihnen. Ferner wird ihr Beispiel die Grundlage für die Demonstration "paralleler Präferenzen" sein, aber bereits in meiner automatischen Interpretation.



2. Generatoren von Datenlisten



Also - Generatoren. Mit ihnen sind häufig asynchrone Programmierung und Coroutinen verbunden. Eine Reihe von Videos von Oleg Molchanov erzählt davon. Daher bezeichnet er das Hauptmerkmal von Generatoren als ihre „Fähigkeit, die Ausführung einer Funktion anzuhalten, um ihre Ausführung an derselben Stelle fortzusetzen, an der sie zuletzt gestoppt wurde“ (weitere Einzelheiten siehe [3] ). Und in Anbetracht dessen, was oben über die bereits recht alte Definition von Coroutinen gesagt wurde, gibt es nichts Neues.



Wie sich jedoch herausstellt, haben Generatoren eine ganz bestimmte Verwendung für die Erstellung von Datenlisten gefunden. Eine Einführung in dieses Thema ist bereits in einem Video von Egorov Artem [4] enthalten.... Es scheint jedoch, dass wir durch ihre Anwendung qualitativ unterschiedliche Konzepte mischen - Operationen und Prozesse. Durch die Erweiterung der Beschreibungsmöglichkeiten der Sprache maskieren wir weitgehend die möglicherweise auftretenden Probleme. Hier, wie sie sagen, nicht zu viel zu spielen. Die Verwendung von Generatoren-Coroutinen zur Beschreibung von Daten trägt meiner Meinung nach genau dazu bei. Beachten Sie, dass Oleg Molchanov auch davor warnt, Generatoren mit Datenstrukturen zu verknüpfen, und betont, dass „Generatoren Funktionen sind“ [3] .



Zurück zur Verwendung von Generatoren zum Definieren von Daten. Es ist schwer zu verbergen, dass wir einen Prozess erstellt haben, der die Listenelemente berechnet. Daher stellen sich sofort Fragen zu einer solchen Liste als Prozess. Wie kann man es beispielsweise wiederverwenden, wenn Coroutinen per Definition nur "in eine Richtung" funktionieren? Wie berechnet man ein beliebiges Element davon, wenn der Prozess nicht indiziert werden kann? Usw. usw. Artyom gibt keine Antworten auf diese Fragen, sondern warnt nur davor, dass ein wiederholter Zugriff auf die Elemente der Liste nicht organisiert werden kann und eine Indizierung unzulässig ist. Eine Suche im Internet überzeugt mich davon, dass nicht nur ich ähnliche Fragen habe, sondern die vorgeschlagenen Lösungen auch nicht so trivial und offensichtlich sind.



Ein weiteres Problem ist die Geschwindigkeit der Listenerstellung. Jetzt bilden wir auf jedem Coroutine-Switch ein einzelnes Element der Liste, was die Datengenerierungszeit verlängert. Der Prozess kann erheblich beschleunigt werden, indem Elemente in „Chargen“ generiert werden. Aber höchstwahrscheinlich wird es Probleme damit geben. Wie stoppe ich einen bereits laufenden Prozess? Oder etwas anderes. Die Liste kann sehr lang sein und nur ausgewählte Elemente verwenden. In einer solchen Situation wird das Speichern von Daten häufig für einen effizienten Zugriff verwendet. Übrigens habe ich fast sofort einen Artikel zu diesem Thema für Python gefunden, siehe [5] (weitere Informationen zur Memoisierung in Bezug auf Automaten finden Sie in Artikel [6] ). Aber was ist in diesem Fall?



Die Zuverlässigkeit einer solchen Syntax zur Definition von Listen kann ebenfalls fraglich sein, da Es ist leicht genug, fälschlicherweise eckige Klammern anstelle von Klammern zu verwenden und umgekehrt. Es stellt sich heraus, dass eine scheinbar schöne und elegante Lösung in der Praxis zu bestimmten Problemen führen kann. Eine Programmiersprache sollte technologisch fortschrittlich, flexibel und gegen unfreiwillige Fehler versichert sein.



Übrigens können Sie zum Thema Listen und Generatoren über ihre Vor- und Nachteile, die sich mit den obigen Ausführungen überschneiden, ein weiteres Video von Oleg Molchanov [7] ansehen .



3. Generatoren-Coroutinen



Das nächste Video von Oleg Molchanov [8] beschreibt die Verwendung von Generatoren zur Koordinierung der Arbeit von Coroutinen. Eigentlich sind sie dafür gedacht. Es wird auf die Wahl der Momente für den Wechsel der Coroutinen hingewiesen. Ihre Anordnung folgt einer einfachen Regel: Stellen Sie die Yield-Anweisung vor die Blockierungsfunktionen. Letztere werden als Funktionen verstanden, deren Rückgabezeit im Vergleich zu anderen Operationen so lang ist, dass Berechnungen mit dem Stoppen verbunden sind. Aus diesem Grund wurden sie Blocker genannt.



Das Umschalten ist wirksam, wenn der angehaltene Prozess seine Arbeit genau dann fortsetzt, wenn der blockierende Anruf nicht wartet, sondern seine Arbeit schnell abschließt. Aus diesem Grund wurde all diese "Aufregung" um das Coroutine / Coroutine-Modell herum begonnen, und dementsprechend wurde der Entwicklung der asynchronen Programmierung ein Impuls gegeben. Wir stellen jedoch fest, dass die ursprüngliche Idee von Coroutinen immer noch anders war - ein virtuelles Modell für paralleles Rechnen zu erstellen.



In dem betrachteten Video wird, wie im allgemeinen Fall für Coroutinen, die Fortsetzung der Coroutinenoperation durch die externe Umgebung bestimmt, die ein Ereignisplaner ist. In diesem Fall wird es durch eine Funktion namens event_loop dargestellt. Und anscheinend ist alles logisch: Der Scheduler führt die Analyse durch und setzt die Arbeit der Coroutine fort, indem er den next () -Operator genau bei Bedarf aufruft. Das Problem liegt in der Wartezeit, wo es nicht erwartet wurde: Der Scheduler kann sehr komplex sein. In Molchanovs vorherigem Video ( siehe [3] ) war seitdem alles einfach Es wurde eine einfache abwechselnde Übertragung der Kontrolle durchgeführt, bei der es seitdem keine Sperren gab Es gab keine entsprechenden Anrufe. Wir betonen jedoch, dass in jedem Fall zumindest ein einfacher Scheduler erforderlich ist.



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



2. , select, — . .



Aber es geht nicht einmal um die Notwendigkeit eines Planers, sondern darum, dass er für ihn ungewöhnliche Funktionen übernimmt. Die Situation wird durch die Tatsache weiter kompliziert, dass es notwendig ist, einen Algorithmus für den gemeinsamen Betrieb vieler Coroutinen zu implementieren. Der Vergleich der in den beiden genannten Videos von Oleg Molchanov diskutierten Scheduler spiegelt ein ähnliches Problem deutlich wider: Der Socket-Scheduling-Algorithmus in [8] ist deutlich komplizierter als der "Karussell" -Algorithmus in [3] .



3. Zu einer Welt ohne Coroutinen



Da wir sicher sind, dass eine Welt ohne Coroutinen möglich ist, indem wir ihnen Automaten entgegensetzen, muss gezeigt werden, wie ähnliche Aufgaben von ihnen bereits gelöst werden. Lassen Sie uns dies am selben Beispiel für die Arbeit mit Sockets demonstrieren. Beachten Sie, dass sich die anfängliche Implementierung als nicht so trivial herausstellte, dass sie sofort verstanden werden konnte. Dies wird vom Autor des Videos selbst wiederholt betont. Andere stehen im Zusammenhang mit Coroutinen vor ähnlichen Problemen. Die Nachteile von Coroutinen sind also mit der Komplexität ihrer Wahrnehmung, ihres Verständnisses, ihres Debuggens usw. verbunden. diskutiert in Video [10] .



Zunächst einige Worte zur Komplexität des betrachteten Algorithmus. Dies ist auf die Dynamik und Pluralität der Kundendienstprozesse zurückzuführen. Zu diesem Zweck wird ein Server erstellt, der einen bestimmten Port überwacht und bei Auftreten von Anforderungen viele Client-Service-Funktionen generiert, die ihn erreichen. Da es viele Clients geben kann, die unvorhersehbar erscheinen, wird eine dynamische Liste aus den Prozessen der Wartung von Sockets und des Informationsaustauschs mit ihnen erstellt. Der in Video [8] beschriebene Code für die Python-Generatorlösung ist in Listing 1 dargestellt.



Listing 1. Steckdosen an Generatoren
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()




Server- und Client-Algorithmen sind ziemlich einfach. Es sollte jedoch alarmierend sein, dass der Server die Client-Funktion in die Aufgabenliste aufnimmt. Weiter - mehr: Es ist schwierig, den Algorithmus der Ereignisschleife event_loop zu verstehen. Bis die Aufgabenliste leer sein kann, wenn mindestens der Serverprozess immer darin vorhanden sein soll?



Als nächstes werden die Wörterbücher to_read und to_write eingeführt. Die Arbeit mit Wörterbüchern erfordert eine gesonderte Erklärung, da Es ist schwieriger als mit regulären Listen zu arbeiten. Aus diesem Grund sind die von den Yield-Anweisungen zurückgegebenen Informationen auf sie zugeschnitten. Dann beginnt das "Tanzen mit einem Tamburin" um die Wörterbücher herum und alles wird zu einer Art "Brodeln": Etwas scheint in den Wörterbüchern platziert zu sein, von wo aus es in die Aufgabenliste usw. aufgenommen wird. usw. Sie können "Ihren Kopf brechen" und das alles klären.



Und wie sieht die Lösung der vorliegenden Aufgabe aus? Für Automaten ist es logisch, Modelle zu erstellen, die den bereits im Video beschriebenen Prozessen der Arbeit mit Sockets entsprechen. Im Servermodell muss anscheinend nichts geändert werden. Dies ist ein Automat, der wie die Funktion server () funktioniert. Sein Diagramm ist in Abb. 1 dargestellt. 1a. Die Automatenaktion y1 () erstellt einen Server-Socket und verbindet ihn mit dem angegebenen Port. Das Prädikat x1 () definiert die Clientverbindung, und falls vorhanden, erstellt die Aktion y2 () einen Client-Socket-Serviceprozess, der in die Klassenprozessliste aufgenommen wird, die die aktiven Objektklassen enthält.



In Abb. 1b zeigt eine graphische Darstellung des Modells für einen einzelnen Kunden. Im Zustand "0" bestimmt der Automat die Bereitschaft des Clients, Informationen zu übertragen (das Prädikat x1 () ist wahr) und empfängt beim Übergang in den Zustand "1" eine Antwort innerhalb der Aktion y1 (). Wenn der Client bereit ist, Informationen zu empfangen (bereits x2 () muss wahr sein), implementiert die Aktion y2 () die Operation des Sendens einer Nachricht an den Client beim Übergang in den Anfangszustand "0". Wenn der Client die Verbindung zum Server unterbricht (in diesem Fall ist x3 () falsch), wechselt der Automat in den Status "4" und schließt den Client-Socket in der Aktion y3 (). Der Prozess bleibt im Status "4", bis er aus der Liste der aktiven Klassen ausgeschlossen wird (Informationen zur Erstellung der Liste finden Sie in der obigen Beschreibung des Servermodells).



In Abb. 1c zeigt einen Automaten, der den Start von Prozessen implementiert, die der Funktion event_loop () in Listing 1 ähnlich sind. Nur in diesem Fall ist sein Operationsalgorithmus viel einfacher. Es kommt alles darauf an, dass die Maschine die Elemente der Liste der aktiven Klassen durchläuft und für jede von ihnen die loop () -Methode aufruft. Diese Aktion wird von y2 () implementiert. Die Aktion y4 () schließt Klassen aus, die sich im Status "4" befinden. Die restlichen Aktionen arbeiten mit dem Index der Objektliste: Die Aktion y3 () erhöht den Index, die Aktion y1 () setzt ihn zurück.



Die Objektprogrammierungsfunktionen in Python unterscheiden sich von der Objektprogrammierung in C ++. Daher wird die einfachste Implementierung des Automatenmodells als Grundlage genommen (um genau zu sein, es ist eine Nachahmung eines Automaten). Es basiert auf dem Objektprinzip der Darstellung von Prozessen, bei denen jeder Prozess einer separaten aktiven Klasse entspricht (sie werden oft auch als Agenten bezeichnet). Die Klasse enthält die erforderlichen Eigenschaften und Methoden (weitere Einzelheiten zu bestimmten Automatenmethoden - Prädikate und Aktionen in [9] ), und die Logik des Automaten (seine Übergangs- und Ausgangsfunktionen) konzentriert sich auf die Methode loop (). Um die Logik des Verhaltens des Automaten zu implementieren, verwenden wir die if-elif-else-Konstruktion.



Bei diesem Ansatz hat die "Ereignisschleife" nichts mit der Analyse der Verfügbarkeit von Sockets zu tun. Sie werden von den Prozessen selbst überprüft, die dieselbe select-Anweisung innerhalb der Prädikate verwenden. In dieser Situation arbeiten sie mit einem einzelnen Socket und nicht mit einer Liste von ihnen und prüfen sie auf die Operation, die für diesen bestimmten Socket erwartet wird, und genau in der Situation, die durch den Operationsalgorithmus bestimmt wird. Übrigens erschien beim Debuggen einer solchen Implementierung eine unerwartet blockierende Essenz der select-Anweisung.



Zahl: 1. Diagramme von Automatenprozessen für die Arbeit mit Sockets
image



Listing 2 zeigt einen automatischen Objektcode in Python für die Arbeit mit Sockets. Dies ist unsere Art von "Welt ohne Coroutinen". Es ist eine "Welt" mit unterschiedlichen Prinzipien für das Entwerfen von Softwareprozessen. Es ist durch das Vorhandensein eines algorithmischen Modells paralleler Berechnungen gekennzeichnet (für weitere Einzelheiten siehe [9] , der den Haupt- und qualitativen Unterschied zwischen der Automatprogrammierungstechnologie (AP) und der "Coroutine-Technologie" darstellt.



Die Programmierung von Automaten implementiert leicht asynchrone Prinzipien des Programmdesigns, der Prozessparallelität und gleichzeitig alles, was ein Programmierer sich vorstellen kann. Meine vorherigen Artikel beschreiben dies ausführlicher, beginnend mit der Beschreibung des Strukturmodells der automatischen Berechnung und seiner formalen Definition bis hin zu Beispielen seiner Anwendung. Der obige Code in Python demonstriert die automatische Implementierung der Coroutine-Prinzipien der Coroutinen, wobei sie vollständig überlappt und durch das Zustandsmaschinenmodell ergänzt und erweitert werden.



Listing 2. Steckdosen an Maschinen
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()




Der Code in Listing 2 ist technologisch viel weiter fortgeschritten als der Code in Listing 1. Dies ist der Vorteil des automatischen Berechnungsmodells. Dies wird durch die Integration des Automatenverhaltens in das Programmierobjektmodell erleichtert. Infolgedessen konzentriert sich die Verhaltenslogik von Automatenprozessen genau dort, wo sie erzeugt wird, und wird nicht, wie es bei Coroutinen üblich ist, in die Ereignisschleife der Prozesssteuerung delegiert. Die neue Lösung provoziert die Schaffung einer universellen "Ereignisschleife", deren Prototyp als Code der EventLoop-Klasse betrachtet werden kann.



4. Über SRP- und DRY-Prinzipien



Die Prinzipien von "Single Responsibility" - SRP (The Single Responsibility Principle) und "Don't Repeat Yourself" - DRY (Don't Repeat Yourself) werden im Kontext eines anderen Videos von Oleg Molchanov [11] geäußert . Demnach sollte die Funktion nur den Zielcode enthalten, um das SRY-Prinzip nicht zu verletzen und die Wiederholung von "zusätzlichem Code" nicht zu fördern, um das DRY-Prinzip nicht zu verletzen. Zu diesem Zweck wird vorgeschlagen, Dekorateure zu verwenden. Es gibt aber noch eine andere Lösung - eine automatische.



Im vorherigen Artikel [2]Da sie sich der Existenz solcher Prinzipien nicht bewusst waren, wurde ein Beispiel mit Dekorateuren gegeben. Wird als Zähler betrachtet, der übrigens auf Wunsch Listen erstellen kann. Das Stoppuhrobjekt, das die Laufzeit des Zählers misst, wird erwähnt. Wenn Objekte den Prinzipien von SRP und DRY entsprechen, ist ihre Funktionalität nicht so wichtig wie das Kommunikationsprotokoll. In der Implementierung hat der Zählercode nichts mit dem Stoppuhrcode zu tun, und das Ändern eines der Objekte wirkt sich nicht auf das andere aus. Sie sind nur an das Protokoll gebunden, über das sich die Objekte „am Ufer“ einigen und sich dann strikt daran halten.



Somit überschreibt ein Parallelautomatenmodell im Wesentlichen die Fähigkeiten von Dekorateuren. Es ist flexibler und einfacher, ihre Fähigkeiten zu implementieren, weil "umgibt" den Funktionscode nicht (verziert ihn nicht). Zum Zwecke einer objektiven Bewertung und eines Vergleichs des Automaten und der herkömmlichen Technologie zeigt Listing 3 ein Objektanalogon des im vorherigen Artikel [2] erörterten Zählers, in dem vereinfachte Versionen mit den Ausführungszeiten und die Originalversion des Zählers nach Kommentaren dargestellt werden.



Listing 3. Automatische Zählerimplementierung
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)




Lassen Sie uns die Betriebszeiten verschiedener Optionen in einer Tabelle zusammenfassen und die Ergebnisse der Arbeit kommentieren.



  1. Klassische Automatenimplementierung - 110,66 Sek
  2. Automatenimplementierung ohne Automatenmethoden - 73,38 Sek
  3. Ohne automatische Stoppuhr - 35.14
  4. Zähler im Formular mit Ausgabe bei jeder Iteration - 30.53
  5. Zähler mit Sperrzyklus - 18.27
  6. Original Theke mit Dekorateur - 6.96


Die erste Option, die das automatische Zählermodell vollständig darstellt, d.h. Der Zähler selbst und die Stoppuhr haben die längste Laufzeit. Die Betriebszeit kann reduziert werden, indem sozusagen die Prinzipien der automatischen Technologie aufgegeben werden. Dementsprechend werden in Option 2 Aufrufe von Prädikaten und Aktionen durch ihren Code ersetzt. Auf diese Weise haben wir Zeit für Methodenaufrufoperatoren gespart, und dies ist ziemlich auffällig, d. H. um mehr als 30 Sekunden, verkürzte die Betriebszeit.



Bei der dritten Option haben wir etwas mehr gespart, wodurch eine einfachere Implementierung des Zählers erstellt wurde, die jedoch bei jeder Iteration des Zählerzyklus beendet wurde (Nachahmung der Coroutine-Operation). Durch den Wegfall der Aufhängung des Zählers (siehe Option 5) konnten wir die Arbeit des Zählers am stärksten reduzieren. Gleichzeitig haben wir die Vorteile der Coroutine-Arbeit verloren. Option 6 - Dies ist die Original-Theke mit einem bereits wiederholten Dekorateur und der kürzesten Laufzeit. Wie bei Option 5 handelt es sich jedoch um eine blockierende Implementierung, die im Zusammenhang mit der Erörterung der Coroutine-Funktionsweise von Funktionen nicht für uns geeignet ist.



5. Schlussfolgerungen



Ob Sie die Automatentechnologie verwenden oder Coroutinen vertrauen - die Entscheidung liegt ganz beim Programmierer. Für uns hier ist es wichtig, dass er weiß, dass es einen anderen Ansatz / eine andere Technologie als Coroutinen für das Programmdesign gibt. Sie können sich sogar die folgende exotische Option vorstellen. Zunächst wird in der Modellentwurfsphase ein Automatenlösungsmodell erstellt. Es ist streng wissenschaftlich, evidenzbasiert und gut dokumentiert. Um beispielsweise die Leistung zu verbessern, wird es beispielsweise zu einer "normalen" Version des Codes "entstellt", wie Listing 3 zeigt. Sie können sich sogar ein "umgekehrtes Refactoring" des Codes vorstellen, d. H. der Übergang von der 7. Option zur 1., aber dies ist zwar möglich, aber der am wenigsten wahrscheinliche Verlauf der Ereignisse :)



In Abb. 2 zeigt Folien aus dem Video zum Thema "asynchron" [10]... Und das "Schlechte" scheint das "Gute" zu überwiegen. Und wenn Automaten meiner Meinung nach immer gut sind, dann wählen Sie bei asynchroner Programmierung, wie sie sagen, nach Ihrem Geschmack. Aber es sieht so aus, als ob die "schlechte" Option am wahrscheinlichsten ist. Und der Programmierer sollte dies beim Entwerfen eines Programms im Voraus wissen.



Zahl: 2. Eigenschaften der asynchronen Programmierung
image



Sicher ist der Automatencode etwas "nicht ohne Sünde". Es wird eine etwas größere Menge an Code haben. Aber zuerst ist es besser strukturiert und daher leichter zu verstehen und leichter zu warten. Und zweitens wird es nicht immer größer sein, weil Mit zunehmender Komplexität wird es höchstwahrscheinlich sogar einen Gewinn geben (zum Beispiel aufgrund der Wiederverwendung von Automatenmethoden). Das Debuggen ist einfacher und klarer. Ja, am Ende des Tages ist es komplett SRP und DRY. Und das überwiegt manchmal sehr.



Es wäre wünschenswert und vielleicht sogar notwendig, beispielsweise den Standard für die Gestaltung von Funktionen zu beachten. Der Programmierer sollte es nach Möglichkeit vermeiden, Blockierungsfunktionen zu entwerfen. Dazu muss entweder nur der Berechnungsprozess gestartet werden, der dann auf Vollständigkeit überprüft wird, oder es müssen Mittel zur Überprüfung der Startbereitschaft vorhanden sein, wie in den in den Beispielen berücksichtigten Auswahlfunktionen. Der in Listing 4 gezeigte Code, der Funktionen aus DOS-Zeiten verwendet, weist darauf hin, dass solche Probleme eine lange "Vorroutine" -Historie haben.



Listing 4. Zeichen von der Tastatur lesen
/*
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();
}
//*/




Hier sind zwei Optionen zum Lesen von Zeichen von der Tastatur. Die erste Option ist das Blockieren. Es blockiert die Berechnung und führt die Anweisung zur Ausgabe des Zeichens erst aus, wenn die Funktion getch () sie von der Tastatur empfängt. In der zweiten Variante wird dieselbe Funktion nur zum richtigen Zeitpunkt gestartet, wenn die gepaarte Funktion kbhit () bestätigt, dass sich das Zeichen im Eingabepuffer befindet. Somit wird es keine Blockierung von Berechnungen geben.



Wenn die Funktion an sich "schwer" ist, d.h. erfordert eine beträchtliche Zeit zum Arbeiten, und ein periodisches Verlassen durch die Art der Arbeit von Coroutinen (dies kann ohne Verwendung des Mechanismus derselben Coroutinen erfolgen, um sich nicht daran zu binden) ist schwierig oder macht wenig Sinn, dann bleibt es, solche Funktionen in einem separaten Thread zu platzieren und dann Kontrollieren Sie den Abschluss ihrer Arbeit (siehe die Implementierung der QCount-Klasse in [2]).).



Sie können immer einen Ausweg finden, um das Blockieren von Berechnungen auszuschließen. Oben haben wir gezeigt, wie Sie asynchronen Code im Rahmen der üblichen Sprachmittel erstellen können, ohne den Coroutine / Coroutine-Mechanismus und sogar eine spezielle Umgebung wie die automatische Programmierumgebung VKP (a) zu verwenden. Und was und wie zu verwenden ist, muss der Programmierer entscheiden.



Literatur



1. Python Junior Podcast. Informationen zur Asynchronität in Python. [Elektronische Ressource], Zugriffsmodus: www.youtube.com/watch?v=Q2r76grtNeg , kostenlos. Sprache. Russisch (Datum der Behandlung 13.07.2020).

2. Parallelität und Effizienz: Python vs FSM. [Elektronische Ressource], Zugriffsmodus: habr.com/ru/post/506604 , kostenlos. Sprache. Russisch (Datum der Behandlung 13.07.2020).

3. Molchanov O. Grundlagen der Asynchronität in Python # 4: Generatoren und die Round Robin-Ereignisschleife. [Elektronische Ressource], Zugriffsmodus: www.youtube.com/watch?v=PjZUSSkGLE8 ], kostenlos. Sprache. Russisch (Datum der Behandlung 13.07.2020).

4. 48 Generatoren und Iteratoren. Generatorausdrücke in Python. [Elektronische Ressource], Zugriffsmodus: www.youtube.com/watch?v=vn6bV6BYm7w, frei. Yaz. Russisch (Datum der Behandlung 13.07.2020).

5. Auswendiglernen und Currying (Python). [Elektronische Ressource], Zugriffsmodus: habr.com/ru/post/335866 , kostenlos. Yaz. Russisch (Datum der Behandlung 13.07.2020).

6. Lyubchenko V.S. Über den Umgang mit Rekursion. "PC World", Nr. 11/02. www.osp.ru/pcworld/2002/11/164417

7. Molchanov O. Python-Lektionen Besetzung Nr. 10 - Was ist Ertrag? [Elektronische Ressource], Zugriffsmodus: www.youtube.com/watch?v=ZjaVrzOkpZk , kostenlos. Yaz. Russisch (Datum der Behandlung 18.07.2020).

8. Molchanov O. Grundlagen von Async in Python # 5: Async auf Generatoren. [Elektronische Ressource], Zugriffsmodus: www.youtube.com/watch?v=hOP9bKeDOHs , kostenlos. Yaz. Russisch (Datum der Behandlung 13.07.2020).

9. Paralleles Rechenmodell. [Elektronische Ressource], Zugriffsmodus: habr.com/ru/post/486622 , kostenlos. Yaz. Russisch (Datum der Behandlung 20.07.2020).

10. Polishchuk A. Asynchronismus in Python. [Elektronische Ressource], Zugriffsmodus: www.youtube.com/watch?v=lIkA0TDX8tE , kostenlos. Yaz. Russisch (Datum der Behandlung 13.07.2020).

11. Molchanov O. Lektionen Python Besetzung # 6 - Dekorateure. [Elektronische Ressource], Zugriffsmodus: www.youtube.com/watch?v=Ss1M32pp5Ew , kostenlos. Yaz. Russisch (Datum der Behandlung 13.07.2020).



All Articles