Ich möchte ein Programm schreiben, dessen Hauptthread einen neuen Thread zur Berechnung gibt und darauf wartet, dass er eine Zeit lang beendet wird. Wenn der untergeordnete Thread nicht in einer bestimmten Zeit beendet wird, wird eine Zeitüberschreitung festgestellt und beendet. Ich habe den folgenden Code dafür.
import Control.Concurrent
fibs :: Int -> Int
fibs 0 = 0
fibs 1 = 1
fibs n = fibs (n-1) + fibs (n-2)
main = do
mvar <- newEmptyMVar
tid <- forkIO $ do
threadDelay (1 * 1000 * 1000)
putMVar mvar Nothing
tid' <- forkIO $ do
if fibs 1234 == 100
then putStrLn "Incorrect answer" >> putMVar mvar (Just False)
else putStrLn "Maybe correct answer" >> putMVar mvar (Just True)
putStrLn "Waiting for result or timeout"
result <- takeMVar mvar
killThread tid
killThread tid'
Ich habe das obige Programm mit ghc -O2 Test.hs
und kompiliert ghc -O2 -threaded Test.hs
und ausgeführt, aber in beiden Fällen hängt das Programm nur, ohne etwas zu drucken oder zu beenden. Wenn ich threadDelay (2 * 1000 * 1000)
dem Berechnungsthread vor dem if
Block ein hinzufüge , funktioniert das Programm wie erwartet und endet nach einer Sekunde, da der Timer-Thread den füllen kann mvar
.
Warum funktioniert das Threading nicht wie erwartet?
quelle
MVar
besagen, dass es anfällig für Rennbedingungen ist. Ich würde diese Notiz ernst nehmen.MVar
Disziplin sieht für mich hier gut aus.+RTS -N
? überprüfen wiki.haskell.org/Concurrency für weitere InformationenAntworten:
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: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
infib :: Integer -> Integer
. DaInteger
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, undyield
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 habenyield
oder eine Zuweisung in der äußersten Schleife erzwingen.)quelle
-fno-omit-yields
, da dies-O2
impliziert-fomit-yields
. downloads.haskell.org/~ghc/latest/docs/html/users_guide/…