Unity Coroutine gegen Threads

13

Was ist der Unterschied zwischen einer Koroutine und einem Faden? Gibt es irgendwelche Vorteile, wenn man eins gegenüber dem anderen verwendet?

StrengthPlusPlus
quelle
3
Coroutines, die an einem Hauptthread arbeiten.
Woltus
2
Sie können in einem Thread keine Unity-Funktionen verwenden. Coroutinen können.
Hellium
1
@Hellium Correction, Sie können keine Unity-Funktionen in einem anderen Thread verwenden, dh Unity-Funktionen können im Haupt-Thread verwendet werden .
Pharap

Antworten:

24

Während Coroutines auf den ersten Blick wie Threads zu funktionieren scheinen, verwenden sie eigentlich kein Multithreading. Sie werden nacheinander ausgeführt, bis sie yield. Die Engine prüft alle erhaltenen Coroutinen als Teil ihrer eigenen Hauptschleife (an welcher Stelle genau vom Typ abhängt yield, weitere Informationen finden Sie in diesem Diagramm ), setzt sie nacheinander bis zur nächsten fort yieldund fährt dann mit der Hauptschleife fort.

Diese Technik hat den Vorteil, dass Sie Coroutinen ohne die Kopfschmerzen verwenden können, die durch Probleme mit echtem Multithreading verursacht werden. Sie werden keine Deadlocks, Racebedingungen oder Leistungsprobleme durch Kontextwechsel bekommen, Sie werden in der Lage sein, richtig zu debuggen und Sie müssen keine thread-sicheren Datencontainer verwenden. Dies liegt daran, dass sich die Unity-Engine während der Ausführung einer Coroutine in einem kontrollierten Zustand befindet. Es ist sicher, die meisten Unity-Funktionen zu verwenden.

Bei Threads hingegen wissen Sie überhaupt nicht, in welchem ​​Zustand sich die Unity-Hauptschleife gerade befindet (möglicherweise wird sie überhaupt nicht mehr ausgeführt). Ihr Thread kann also ziemlich viel Chaos anrichten, wenn Sie etwas zu einem Zeitpunkt tun, an dem dies nicht der Fall sein soll. Berühren Sie keine nativen Unity-Funktionen in einem Sub-Thread . Wenn Sie zwischen einem Sub-Thread und Ihrem Haupt-Thread kommunizieren müssen, lassen Sie den Thread in ein threadsicheres (!) Container-Objekt schreiben und lassen Sie ein MonoBehaviour diese Informationen während der üblichen Unity-Ereignisfunktionen lesen.

Der Nachteil, kein "echtes" Multithreading durchzuführen, besteht darin, dass Sie keine Coroutinen verwenden können, um CPU-intensive Berechnungen über mehrere CPU-Kerne zu parallelisieren. Sie können sie jedoch verwenden, um eine Berechnung auf mehrere Aktualisierungen aufzuteilen. Anstatt dein Spiel für eine Sekunde einzufrieren, erhältst du nur eine niedrigere durchschnittliche Framerate über mehrere Sekunden. In diesem Fall sind Sie jedoch gegenüber yieldIhrer Coroutine verantwortlich, wenn Sie Unity erlauben möchten, ein Update auszuführen.

Fazit:

  • Wenn Sie asynchrone Ausführung verwenden möchten, um die Spielelogik auszudrücken, verwenden Sie Coroutinen.
  • Wenn Sie die asynchrone Ausführung verwenden möchten, um mehrere CPU-Kerne zu verwenden, verwenden Sie Threads.
Philipp
quelle
Kommentare sind nicht für längere Diskussionen gedacht. Diese Unterhaltung wurde in den Chat verschoben .
MichaelHouse
10

Coroutinen nennen wir in der Informatik "kooperatives Multitasking". Sie sind eine Möglichkeit für mehrere verschiedene Ausführungsströme, sich kooperativ miteinander zu verschachteln. Beim kooperativen Multitasking hat ein Ausführungsstrom das unbestrittene alleinige Eigentum an der CPU, bis er a erreicht yield. Zu diesem Zeitpunkt hat Unity (oder das von Ihnen verwendete Framework) die Möglichkeit, zu einem anderen Ausführungsstream zu wechseln. Es wird dann auch alleiniges unbestrittenes Eigentum an der CPU, bis es ist yield.

Themen sind das, was wir "präventives Multitasking" nennen. Wenn Sie Threads verwenden, behält sich das Framework das Recht vor , Ihren Thread jederzeit zu unterbrechen und zu einem anderen Thread zu wechseln. Es ist egal, wo du bist. In manchen Fällen können Sie sogar vorübergehend angehalten werden, indem Sie eine Variable in den Speicher schreiben!

Für jeden gibt es Vor- und Nachteile. Die Nachteile von Koroutinen sind wahrscheinlich am einfachsten zu verstehen. Zunächst einmal werden alle Coroutinen auf einem einzigen Core ausgeführt. Wenn Sie eine Quad-Core-CPU haben, verwenden Coroutinen nur einen der vier Kerne. Dies vereinfacht die Dinge, kann jedoch in einigen Fällen ein Leistungsproblem darstellen. Der zweite Nachteil ist, dass Sie sich darüber im Klaren sein müssen, dass jede Coroutine Ihr gesamtes Programm stoppen kann, indem Sie dies ablehnen yield. Dies war vor vielen Jahren ein Problem unter Mac OS9. OS9 unterstützte nur kooperatives Multitasking auf dem gesamten Computer. Wenn eines Ihrer Programme hängen bleibt, kann der Computer so heftig angehalten werden, dass das Betriebssystem nicht einmal den Text der Fehlermeldung rendern kann, um Sie darüber zu informieren, was passiert ist!

Die Vorteile von Coroutinen sind, dass sie relativ einfach zu verstehen sind. Die Fehler, die Sie haben, sind weitaus vorhersehbarer. Sie erfordern normalerweise auch weniger Ressourcen, was hilfreich sein kann, wenn Sie in die Zehntausende von Coroutinen oder Threads aufsteigen. Candid Moon erwähnte in den Kommentaren, dass man sich, wenn man die Themen nicht richtig studiert hat, einfach an Koroutinen hält und sie stimmen. Mit Coroutinen lässt sich viel einfacher arbeiten.

Threads sind ein ganz anderes Biest. Sie müssen immer auf der Hut sein vor der Möglichkeit, dass Sie jederzeit von einem anderen Thread unterbrochen werdenund verwirren Sie mit Ihren Daten. Threading-Bibliotheken bieten eine ganze Reihe leistungsstarker Tools, die Sie dabei unterstützen, z. B. Mutexe und Bedingungsvariablen, mit denen Sie dem Betriebssystem mitteilen können, ob es sicher ist, einen Ihrer anderen Threads auszuführen, und ob es unsicher ist. Es gibt ganze Kurse, die sich mit dem richtigen Umgang mit diesen Tools befassen. Eines der bekanntesten Probleme ist ein "Deadlock", bei dem zwei Threads "hängenbleiben" und darauf warten, dass der andere Ressourcen freigibt. Ein weiteres für Unity sehr wichtiges Problem ist, dass viele Bibliotheken (wie Unity) nicht für die Unterstützung von Aufrufen von mehreren Threads ausgelegt sind. Sie können sehr leicht Ihren Rahmen sprengen, wenn Sie nicht darauf achten, welche Anrufe zulässig und welche verboten sind.

Der Grund für diese zusätzliche Komplexität ist eigentlich sehr einfach. Das präemptive Multitasking-Modell ähnelt dem Multithreading- Modell, mit dem Sie nicht nur andere Threads unterbrechen, sondern Threads tatsächlich nebeneinander auf verschiedenen Kernen ausführen können. Dies ist unglaublich leistungsfähig und die einzige Möglichkeit, diese neuen Quad-Core- und Hex-Code-CPUs, die auf den Markt kommen, wirklich zu nutzen, öffnet aber die Pandoras-Box. Die Synchronisationsregeln für die Verwaltung dieser Daten in einer Multithreading-Welt sind äußerst brutal. In der C ++ - Welt gibt es ganze Artikel, MEMORY_ORDER_CONSUMEdie sich mit einer einzigen Ecke der Multithread-Synchronisation befassen.

Also die Nachteile des Einfädelns? Einfach: Sie sind hart. Sie können auf ganze Klassen von Bugs stoßen, die Sie noch nie gesehen haben. Viele sind sogenannte "Heisenbugs", die manchmal auftreten und dann verschwinden, wenn Sie sie debuggen. Die Werkzeuge, die Ihnen zur Verfügung stehen, sind sehr leistungsfähig, aber auch sehr niedrig. Sie wurden entwickelt, um die Architektur moderner Chips effizienter zu gestalten, anstatt einfach zu bedienen zu sein.

Wenn Sie jedoch die gesamte CPU-Leistung nutzen möchten, benötigen Sie diese Tools. Außerdem gibt es tatsächlich Algorithmen, die beim Multithreading einfacher zu verstehen sind als bei Coroutinen, nur weil Sie das Betriebssystem alle Fragen behandeln lassen, wo Unterbrechungen auftreten können.

Der Kommentar von Candid Moon, sich an Koroutinen zu halten, ist auch meine Empfehlung. Wenn Sie die Leistungsfähigkeit von Threads nutzen möchten, verpflichten Sie sich dazu. Geh raus und lerne wirklich Fäden, formal. Wir hatten mehrere Jahrzehnte Zeit, um herauszufinden, wie Sie die besten Überlegungen zu Threads anstellen können, damit Sie frühzeitig sichere, zuverlässige und wiederholbare Ergebnisse erhalten und die Leistung nach und nach steigern können. Beispielsweise werden in allen vernünftigen Kursen Mutexe vor dem Unterrichten von Bedingungsvariablen unterrichtet. In allen vernünftigen Kursen, die sich mit Atomik befassen, werden Mutexe und Bedingungsvariablen vollständig unterrichtet, bevor überhaupt erwähnt wird, dass es Atomik gibt. (Hinweis: Es gibt kein vernünftiges Tutorial zur Atomik.) Versuchen Sie, das Stückchenweise Einfädeln zu lernen, und Sie betteln um eine Migräne.

Cort Ammon - Setzen Sie Monica wieder ein
quelle
Threads erhöhen die Komplexität hauptsächlich in Fällen, in denen Vorgänge und Abhängigkeiten verschachtelt sind. Wenn die Hauptspielschleife drei Funktionen enthält, x (), y () und z (), und keine von ihnen etwas beeinflusst, was von einer anderen benötigt wird, starten Sie alle drei gleichzeitig, und warten Sie dann, bis alle abgeschlossen sind, bevor Sie fortfahren Ermöglichen Sie es einem, von einer Multi-Core-Maschine zu profitieren, ohne zu viel Komplexität hinzufügen zu müssen. Während Bedingungsvariablen im Allgemeinen in Verbindung mit Mutexen verwendet werden, benötigt das Independent-Parallel-
Step
... aber einfach eine Möglichkeit für die Threads, die mit y () und z () umgehen, zu warten, bis sie ausgelöst werden, und dann eine Möglichkeit für den Haupt-Thread, nach dem Ausführen von x () zu warten, bis y () und z () Ich habe beendet.
Supercat
@supercat Sie müssen immer eine gewisse Synchronisation haben, um zwischen unabhängigen parallelen Phasen und sequentiellen Phasen zu wechseln . Manchmal ist das nichts weiter als ein join(), aber du brauchst etwas. Wenn Sie keinen Architekten haben, der ein System für die Synchronisation entworfen hat, müssen Sie das selbst schreiben. Als jemand, der Multithread-Sachen macht, finde ich, dass die mentalen Modelle der Leute, wie Computer funktionieren, angepasst werden müssen, bevor sie sicher synchronisieren (was gute Kurse lehren werden)
Cort Ammon - Reinstate Monica
Natürlich muss es eine gewisse Synchronisation geben, und um höchste Effizienz zu erzielen, ist im Allgemeinen etwas mehr als erforderlich join. Mein Punkt war, dass es manchmal besser sein kann, einen moderaten Bruchteil der möglichen Leistungsvorteile von Threading zu verfolgen, als einen komplizierteren Threading-Ansatz zu verwenden, um einen größeren Bruchteil zu ernten.
Supercat
Über neue Arten von Fehlern: Beachten Sie, dass Sie nicht wissen können, wie fehlerhaft der Code im Freien sein wird, wenn Sie ihn nicht logisch als sicher beweisen können. Obwohl die Ausführungsreihenfolge undefiniert sein kann, werden bei jeder Ausführung auf einem Computer häufig ähnliche Pfade verwendet. Sie werden oft feststellen, dass es schwer und oft auf einem Bruchteil der Maschinen ausfällt, und Sie können möglicherweise nicht auf allen möglichen Maschinen testen. Bei der Arbeit hatten wir einen Fehler, der nur auf einem unserer Computer auftrat, wenn die Protokollierung deaktiviert war. Es dauerte lange, bis mehrere Personen es ausfindig machten. Ich will das nicht in freier Wildbahn. Testen Sie nicht nur die Synchronisation, sondern beweisen Sie dies logisch.
Aaron
2

Im einfachsten Sinne möglich ...


Themen

Ein Thread entscheidet nicht, wann er liefert, das Betriebssystem (das 'Betriebssystem', zB Windows) entscheidet, wann ein Thread geliefert wird. Das Betriebssystem ist fast ausschließlich für die Planung der Threads verantwortlich. Es entscheidet, welche Threads wann und wie lange ausgeführt werden sollen.

Zusätzlich kann ein Thread synchron (ein Thread nach dem anderen) oder asynchron (verschiedene Threads, die auf verschiedenen CPU-Kernen ausgeführt werden) ausgeführt werden. Die Fähigkeit, asynchron zu laufen, bedeutet, dass Threads in der gleichen Zeit mehr Arbeit erledigen können (da die Threads buchstäblich zwei Dinge gleichzeitig erledigen). Sogar synchrone Threads erledigen viel Arbeit, wenn das Betriebssystem sie gut plant.

Diese zusätzliche Rechenleistung ist jedoch mit Nebenwirkungen verbunden. Wenn beispielsweise zwei Threads versuchen, auf dieselbe Ressource (z. B. eine Liste) zuzugreifen, und jeder Thread an einer beliebigen Stelle im Code zufällig gestoppt werden kann, können die Änderungen des zweiten Threads die vom ersten Thread vorgenommenen Änderungen beeinträchtigen. (Siehe auch: Race Conditions und Deadlock .)

Threads werden auch als "schwer" eingestuft, da sie einen hohen Overhead haben, was bedeutet, dass beim Wechseln von Threads ein erheblicher Zeitaufwand entsteht.


Coroutinen

Im Gegensatz zu Threads sind Coroutinen vollständig synchron, es kann zu jedem Zeitpunkt nur eine Coroutine ausgeführt werden. Zusätzlich können Coroutinen wählen, wann sie nachgeben möchten, und somit an einem Punkt im Code nachgeben, der von Vorteil ist (z. B. am Ende eines Schleifenzyklus). Dies hat den Vorteil, dass Probleme wie Rennbedingungen und Deadlocks viel einfacher zu vermeiden sind und Coroutinen leichter miteinander kooperieren können.

Dies ist jedoch auch eine Hauptverantwortung, wenn eine Coroutine nicht richtig liefert, kann dies viel Prozessorzeit in Anspruch nehmen und dennoch Fehler verursachen, wenn gemeinsam genutzte Ressourcen falsch geändert werden.

Coroutinen erfordern im Allgemeinen keine Kontextumschaltung und sind daher schnell ein- und auszuschalten und recht leicht.


In Summe:

Faden:

  • Synchron oder asynchron
  • Vom Betriebssystem erbracht
  • Zufällig vergeben
  • Schwer

Coroutine:

  • Synchron
  • Ergibt sich
  • Erträge nach Wahl
  • Leicht

Die Rollen von Threads und Coroutinen sind sehr ähnlich, unterscheiden sich jedoch in der Art und Weise, in der sie ihre Arbeit erledigen. Themen eignen sich am besten für Aufgaben, bei denen sie sich darauf konzentrieren können, etwas alleine zu tun, ohne unterbrochen zu werden, und dann zurückmelden, wenn sie fertig sind. Coroutinen eignen sich am besten für Aufgaben, die in vielen kleinen Schritten erledigt werden können, und für Aufgaben, die eine kooperative Datenverarbeitung erfordern.

Pharap
quelle