GHC verwendet bei seiner Parallelitätsimplementierung eine Art Hybrid aus kooperativem und präventivem Multitasking.
Auf der Haskell-Ebene scheint dies präventiv zu sein, da Threads nicht explizit nachgeben müssen und jederzeit durch die Laufzeit unterbrochen werden können. Auf Laufzeitebene "geben" Threads jedoch immer dann nach, wenn sie Speicher zuweisen. Da fast alle Haskell-Threads ständig zugeordnet werden, funktioniert dies normalerweise recht gut.
Wenn eine bestimmte Berechnung jedoch in nicht zuweisenden Code optimiert werden kann, kann sie auf Laufzeitebene nicht mehr kooperativ und auf Haskell-Ebene nicht mehr vorab zulässig sein. Wie @Carl betonte, ist es tatsächlich die -fomit-yields
Flagge, die impliziert wird -O2
, dass dies möglich ist:
-fomit-yields
Weist GHC an, Heap-Prüfungen wegzulassen, wenn keine Zuordnung durchgeführt wird. Dies verbessert zwar die Binärgröße um etwa 5%, bedeutet jedoch auch, dass Threads, die in engen, nicht zuweisenden Schleifen ausgeführt werden, nicht rechtzeitig vorab geprüft werden. Wenn es wichtig ist, solche Threads immer unterbrechen zu können, sollten Sie diese Optimierung deaktivieren. Ziehen Sie auch in Betracht, alle Bibliotheken mit deaktivierter Optimierung neu zu kompilieren, wenn Sie die Unterbrechbarkeit gewährleisten möchten.
In der Single-Threaded-Laufzeit (kein -threaded
Flag) bedeutet dies natürlich, dass ein Thread alle anderen Threads vollständig aushungern kann. Weniger offensichtlich kann dasselbe passieren, selbst wenn Sie mit Optionen kompilieren -threaded
und diese verwenden +RTS -N
. Das Problem ist , dass ein unkooperativ Thread die Laufzeit aushungern kann Scheduler selbst. Wenn der nicht kooperative Thread irgendwann der einzige Thread ist, dessen Ausführung derzeit geplant ist, wird er nicht mehr unterbrochen, und der Scheduler wird niemals erneut ausgeführt, um das Planen zusätzlicher Threads in Betracht zu ziehen, selbst wenn sie auf anderen Betriebssystem-Threads ausgeführt werden könnten.
Wenn Sie nur versuchen, einige Dinge zu testen, ändern Sie die Signatur von fib
in fib :: Integer -> Integer
. Da Integer
die Zuordnung verursacht, funktioniert alles wieder (mit oder ohne -threaded
).
Wenn Sie in echtem Code auf dieses Problem stoßen, ist die mit Abstand einfachste Lösung die von @Carl vorgeschlagene: Wenn Sie die Unterbrechbarkeit von Threads gewährleisten müssen, sollte der Code mit kompiliert werden -fno-omit-yields
, wodurch Scheduler-Aufrufe in nicht zuweisendem Code erhalten bleiben . Gemäß der Dokumentation werden dadurch die Binärgrößen erhöht. Ich gehe davon aus, dass es auch eine kleine Leistungsstrafe gibt.
Wenn die Berechnung bereits ausgeführt wird IO
, yield
kann es auch sinnvoll sein , explizit in der optimierten Schleife zu arbeiten. Für eine reine Berechnung können Sie sie in E / A konvertieren, und yield
obwohl Sie normalerweise eine einfache Möglichkeit finden, eine Zuordnung erneut einzuführen. In den meisten realistischen Situationen gibt es eine Möglichkeit, nur "wenige" yield
oder Zuordnungen einzuführen - genug, um den Thread wieder ansprechbar zu machen, aber nicht genug, um die Leistung ernsthaft zu beeinträchtigen. (Wenn Sie beispielsweise verschachtelte rekursive Schleifen haben yield
oder eine Zuweisung in der äußersten Schleife erzwingen.)
MVar
besagen, dass es anfällig für Rennbedingungen ist. Ich würde diese Notiz ernst nehmen.