Lua-Koroutine
Was ist eine Koroutine?
Lua-Koroutinen sind Threads recht ähnlich: Sie besitzen unabhängige Stacks, unabhängige lokale Variablen und unabhängige Instruktionszeiger und teilen gleichzeitig globale Variablen und die meisten anderen Dinge mit anderen Koroutinen.
Eine Koroutine kann als eine spezielle Art von Thread verstanden werden, der seine Ausführung anhalten und fortsetzen kann, wodurch nicht-präemptives Multitasking ermöglicht wird.
Koroutinen sind ein sehr mächtiges Feature, aber auch komplex in der Anwendung.
Grundlegende Syntax
Coroutinen werdencoroutinevom Modul unterstützt.
Wenn Sie Coroutinen verwenden, können Siecoroutine.createverwenden, um ein neues Coroutine-Objekt zu erstellen, undcoroutine.resumeverwenden, um ihre Ausführung zu starten. Eine Coroutine kanncoroutine.yieldfreiwillig ihre eigene Ausführung unterbrechen und die Kontrolle an den Aufrufer zurückgeben.
| Methode | Beschreibung |
|---|---|
| coroutine.create() | Erstellt eine Coroutine, gibt eine Coroutine zurück, der Parameter ist eine Funktion. In Verbindung mit resume weckt es den Funktionsaufruf. |
| coroutine.resume() | Startet eine Coroutine neu, wird mit create verwendet. |
| coroutine.yield() | Unterbricht eine Coroutine, versetzt die Coroutine in den Zustand suspended. In Verbindung mit resume kann dies viele nützliche Effekte haben. |
| coroutine.status() | Prüft den Status einer Coroutine. Hinweis: coroutine hat drei Zustände: dead, suspended, running. Im folgenden Programm sehen Sie, wann diese Zustände auftreten. |
| coroutine.wrap() | Erstellt eine Coroutine und gibt eine Funktion zurück. Sobald Sie diese Funktion aufrufen, betreten Sie die Coroutine. Dies dupliziert die Funktionalität von create. |
| coroutine.running() | Gibt die laufende Coroutine zurück. Eine Coroutine ist ein Thread. Bei Verwendung von running wird die Thread-ID einer Coroutine zurückgegeben. |
Das folgende Beispiel zeigt, wie man Lua-Coroutinen verwendet:
Beispiel
print("Coroutine foo hat mit der Ausführung begonnen")
local value = coroutine.yield("Pausiere die Ausführung von foo")
print("Coroutine foo hat die Ausführung fortgesetzt, der übergebene Wert ist: " .. tostring(value))
print("Coroutine foo hat die Ausführung beendet")
end
-- Eine Coroutine erstellen
local co = coroutine.create(foo)
-- Die Coroutine starten
local status, result = coroutine.resume(co)
print(result) -- Ausgabe: Pausieren der Ausführung von foo
-- Setze die Ausführung der Coroutine fort und übergib einen Wert
status, result = coroutine.resume(co, 42)
print(result) -- Ausgabe: Coroutine foo hat die Ausführung fortgesetzt, der übergebene Wert ist: 42
Im obigen Beispiel haben wir eine Funktion namens foo als Coroutine definiert. Innerhalb der Funktion haben wir coroutine.yield verwendet, um die Ausführung der Coroutine zu pausieren und einen Wert zurückzugeben.
Im Hauptprogramm haben wir coroutine.create verwendet, um ein Coroutine-Objekt zu erstellen, und coroutine.resume, um seine Ausführung zu starten.
Nach dem ersten Aufruf von coroutine.resume pausiert die Coroutine bei coroutine.yield und gibt den Wert an das Hauptprogramm zurück. Dann rufen wir coroutine.resume erneut auf und übergeben einen Wert als Parameter, damit die Coroutine die Ausführung fortsetzt.
Das Ausführen des obigen Codes erzeugt die folgende Ausgabe:
协同程序 foo 开始执行 暂停 foo 的执行 协同程序 foo 恢复执行,传入的值为: 42 协同程序 foo 结束执行 nil
Es ist zu beachten, dass der Zustand einer Coroutine über die Funktion coroutine.status abgerufen werden kann. Durch Überprüfen des Zustands können Sie den Ausführungsstatus der Coroutine bestimmen (wie z. B. running, suspended, ended usw.).
Das folgende Beispiel demonstriert die Verwendung der oben genannten Methoden:
coroutine_test.lua-Datei
-- Erstelle ein neues Coroutine-Objekt co, wobei die Coroutine-Funktion den übergebenen Parameter i ausgibt
co = coroutine.create(
Funktion(i)
print(i);
end
)
-- Verwende coroutine.resume, um die Ausführung der Coroutine co zu starten, und übergib Parameter 1. Die Coroutine beginnt mit der Ausführung und gibt Ausgabe 1 aus.
coroutine.resume(co, 1) -- 1
-- Überprüfe den Status der Coroutine co mit coroutine.status; die Ausgabe ist dead, was anzeigt, dass die Coroutine die Ausführung beendet hat.
print(coroutine.status(co)) -- dead
print("----------")
-- Verwende coroutine.wrap, um einen Coroutine-Wrapper zu erstellen, der die Coroutine-Funktion in ein direkt aufrufbares Funktionsobjekt umwandelt.
co = coroutine.wrap(
Funktion(i)
print(i);
end
)
co(1)
print("----------")
-- Erstelle ein weiteres Coroutine-Objekt co2, wobei die Coroutine-Funktion in einer Schleife die Zahlen 1 bis 10 ausgibt und, wenn die Schleife 3 erreicht, den aktuellen Coroutine-Status und den laufenden Thread ausgibt.
co2 = coroutine.create(
Funktion()
for i=1,10 do
print(i)
if i == 3 then
print(coroutine.status(co2)) --running
print(coroutine.running()) --thread:XXXXXX
end
coroutine.yield()
end
end
)
-- Rufen Sie coroutine.resume mehrmals auf, um die Ausführung von Coroutine co2 zu starten.
coroutine.resume(co2) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3
-- Prüfen Sie den Status von Coroutine co2 über coroutine.status; die Ausgabe ist suspended, was anzeigt, dass die Coroutine pausiert ist.
print(coroutine.status(co2)) -- suspended
print(coroutine.running())
print("----------")
Das obige Beispiel erzeugt die folgende Ausgabe:
1 dead ---------- 1 ---------- 1 2 3 running thread: 0x7fb801c05868 false suspended thread: 0x7fb801c04c88 true ----------
Wie aus coroutine.running ersichtlich ist, wird eine Coroutine auf der zugrunde liegenden Ebene als Thread implementiert.
Wenn Sie eine Coroutine erstellen, registriert sie ein Ereignis in einem neuen Thread.
Wenn resume verwendet wird, um das Ereignis auszulösen, wird die von create erstellte Coroutine-Funktion ausgeführt. Wenn yield auftritt, bedeutet dies, den aktuellen Thread anzuhalten und auf ein weiteres resume zu warten, um das Ereignis auszulösen.
Als Nächstes analysieren wir ein detaillierteres Beispiel:
Beispiel
print("Ausgabe der foo-Funktion", a)
return coroutine.yield(2 * a) -- Gibt den Wert von 2*a zurück
end
co = coroutine.create(function (a , b)
print("Ausgabe der ersten Coroutine-Ausführung", a, b) -- co-body 1 10
local r = foo(a + 1)
print("Ausgabe der zweiten Coroutine-Ausführung", r)
local r, s = coroutine.yield(a + b, a - b) -- Die Werte von a und b werden übergeben, wenn die Coroutine zum ersten Mal aufgerufen wird
print("Ausgabe der dritten Coroutine-Ausführung", r, s)
return b, "Ende der Coroutine" -- Der Wert von b wird übergeben, wenn die Coroutine zum zweiten Mal aufgerufen wird
end)
print("main", coroutine.resume(co, 1, 10)) -- true, 4
print("--Trennzeichen----")
print("main", coroutine.resume(co, "r")) -- true 11 -9
print("---Separator---")
print("main", coroutine.resume(co, "x", "y")) -- true 10 end
print("---Separator---")
print("main", coroutine.resume(co, "x", "y")) -- cannot resume dead coroutine
print("---Separator---")
Das obige Beispiel erzeugt die folgende Ausgabe:
第一次协同程序执行输出 1 10 foo 函数输出 2 main true 4 --分割线---- 第二次协同程序执行输出 r main true 11 -9 ---分割线--- 第三次协同程序执行输出 x y main true 10 结束协同程序 ---分割线--- main false cannot resume dead coroutine ---分割线---
Das obige Beispiel verläuft wie folgt:
- Rufen Sie resume auf, um die Coroutine aufzuwecken. Die resume-Operation gibt bei Erfolg true zurück, andernfalls false.
- Die Coroutine läuft.
- Läuft bis zur yield-Anweisung.
- yield hält die Coroutine an, und der erste resume-Aufruf kehrt zurück. (Hinweis: Hier kehrt yield zurück, und die Parameter sind die Argumente von resume).
- Der zweite resume-Aufruf weckt die Coroutine erneut auf. (Hinweis: Bei den Argumenten von resume hier dienen außer dem ersten Argument die übrigen Argumente als Parameter von yield).
- yield kehrt zurück.
- Die Coroutine läuft weiter.
- Wenn Sie nach Beendigung der Coroutine weiterhin die Methode resume aufrufen, wird Folgendes ausgegeben: cannot resume dead coroutine.
Die Stärke der Kombination von resume und yield besteht darin, dass resume im Hauptprogramm liegt und externen Zustand (Daten) in die Coroutine übergibt, während yield internen Zustand (Daten) an das Hauptprogramm zurückgibt.
Erzeuger-Verbraucher-Problem
Jetzt werde ich Lua-Coroutinen verwenden, um das klassische Erzeuger-Verbraucher-Problem zu lösen.
Beispiel
function productor()
local i = 0
while true do
i = i + 1
send(i) -- Sende das erzeugte Element an den Verbraucher
end
end
function consumer()
while true do
local i = receive() -- Hole ein Element vom Erzeuger
print(i)
end
end
function receive()
local status, value = coroutine.resume(newProductor)
return value
end
function send(x)
coroutine.yield(x) -- x repräsentiert den Wert, der gesendet werden soll. Nachdem der Wert zurückgegeben wurde, wird die Coroutine angehalten.
end
-- Start the
newProductor = coroutine.create(productor)
consumer()
Das obige Beispiel erzeugt die folgende Ausgabe:
1 2 3 4 5 6 7 8 9 10 11 12 13 ……
Unterschied zwischen Threads und Coroutinen
Der Hauptunterschied zwischen Threads und Coroutinen besteht darin, dass ein Programm mit mehreren Threads mehrere Threads gleichzeitig ausführen kann, während Coroutinen in Kooperation miteinander laufen müssen.
Zu jedem Zeitpunkt läuft nur eine Coroutine, und die laufende Coroutine wird nur unterbrochen, wenn sie ausdrücklich zum Anhalten aufgefordert wird.
Coroutinen sind gewissermaßen wie synchrones Multithreading. Mehrere Threads, die auf dieselbe Thread-Sperre warten, ähneln in gewisser Weise Coroutinen.
Die Hauptunterschiede sind wie folgt zusammengefasst:
Scheduling-Methode: Threads werden normalerweise präemptiv vom Scheduler des Betriebssystems geplant, der die Ausführungsrechte zwischen verschiedenen Threads umschaltet. Coroutinen hingegen werden nicht präemptiv geplant; Programmierer steuern die Übergabe der Ausführungsrechte explizit.
Nebenläufigkeit: Threads werden nebenläufig ausgeführt; mehrere Threads können gleichzeitig auf mehreren Prozessorkernen laufen oder ihre Ausführung auf einem einzelnen Kern per Zeitscheibenverfahren wechseln. Coroutinen sind kooperativ: Es läuft immer nur eine Coroutine, und andere Coroutinen müssen warten, bis die aktuell laufende Coroutine die Ausführung freiwillig abgibt.
Speichernutzung: Threads benötigen normalerweise separate Stacks und Kontextumgebungen, daher verursacht das Erstellen und Zerstören von Threads zusätzlichen Overhead. Coroutinen können denselben Stack und Kontext gemeinsam nutzen, daher ist der Overhead beim Erstellen und Zerstören von Coroutinen geringer.
Datenweitergabe: Threads können denselben Speicherbereich gemeinsam nutzen, aber Thread-Sicherheit und Synchronisationsprobleme müssen beachtet werden. Coroutinen teilen Daten normalerweise über Parameterübergabe und Rückgabewerte, und die Datenisolierung zwischen verschiedenen Coroutinen ist besser.
Debugging und Fehlerbehandlung: Threads sind beim Debugging und bei der Fehlerbehandlung normalerweise komplexer, weil Interaktionen und nebenläufige Ausführung zwischen mehreren Threads zu Problemen führen können, die schwer zu debuggen sind. Koroutinen sind beim Debugging und bei der Fehlerbehandlung relativ einfacher, weil Programmierer den Ausführungsfluss explizit steuern.
Insgesamt sind Threads für Szenarien geeignet, die nebenläufige Ausführung erfordern, wie z. B. die Nutzung von Parallelität auf Mehrkernprozessoren, um die Aufgabenausführung zu beschleunigen. Koroutinen sind für Szenarien geeignet, die Zusammenarbeit und Koordination erfordern, wie Zustandsautomaten, ereignisgesteuerte Programmierung oder kooperative Aufgabenverarbeitung. Die Wahl, ob Threads oder Koroutinen verwendet werden, hängt von den spezifischen Anwendungsanforderungen und dem Programmiermodell ab.
Weitere Erweiterungen