Haskellの計算負荷の高いスレッドが他のすべてのスレッドをブロックする


8

メインスレッドが計算のために新しいスレッドをフォークし、一定の期間終了するのを待つプログラムを作成したいと思います。子スレッドが所定の時間内に完了しない場合、タイムアウトして強制終了されます。これには次のコードがあります。

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' 

上記のプログラムをghc -O2 Test.hsandでコンパイルしてghc -O2 -threaded Test.hs実行しましたが、どちらの場合も、プログラムは何も出力したり終了したりせずにハングします。ブロックのthreadDelay (2 * 1000 * 1000)前に計算スレッドにaを追加するとif、タイマースレッドがを埋めることができるため、プログラムは期待どおりに動作し、1秒後に終了しmvarます。

スレッドが期待どおりに機能しないのはなぜですか?


MVar競合状態の影響を受けやすいという状態に関するメモ。私はそのことを真剣に受け止めます。
Bob Dalgleish

@BobDalgleish、私はそれを非常に疑っています。MVar規律は、ここで私には罰金を探します。
dfeuer

1
でプログラムを実行しました +RTS -Nか?詳細については、wiki.haskell.org / Concurrencyを確認してください
lsmor

これは、この動作の同様の例と、ghcのほとんどの同時実行性の主な責任者である伝説的なSimonからの応答です:) github.com/simonmar/async/issues/93
lehins

回答:


10

GHCは、並行性の実装で、協調型マルチタスクと先制マルチタスクのハイブリッドの一種を使用します。

Haskellレベルでは、スレッドは明示的に制御を譲る必要がなく、いつでもランタイムによって中断されているように見えるため、プリエンプティブのように見えます。ただし、ランタイムレベルでは、スレッドはメモリを割り当てるたびに「譲歩」します。ほとんどすべてのHaskellスレッドは常に割り当てを行っているので、これは通常かなりうまく機能します。

ただし、特定の計算を非割り当てコードに最適化できる場合、ランタイムレベルで非協調的になり、Haskellレベルでプリエンプティブにならない可能性があります。@Carlが指摘したように-fomit-yields、これは実際にはフラグであり、これにより、これが-O2可能になります。

-fomit-yields

割り当てが実行されていないときにヒープチェックを省略するようにGHCに指示します。これによりバイナリサイズが約5%向上しますが、スレッドが割り当てられていないタイトなループで実行されるスレッドがタイムリーにプリエンプトされないことも意味します。このようなスレッドを常に中断できることが重要な場合は、この最適化をオフにする必要があります。割り込み性を保証する必要がある場合は、この最適化をオフにしてすべてのライブラリを再コンパイルすることも検討してください。

明らかに、シングルスレッドランタイムでは( -threadedフラグ)では、1つのスレッドが他のすべてのスレッドを完全に枯渇させる可能性があります。それほど明白ではありませんが、コンパイルし-threaded+RTS -Nオプションを使用しても、同じことが起こります。問題は、非協調的なスレッドがランタイムスケジューラを使い果たす可能性があることです。自体をことです。ある時点で非協調スレッドが現在実行がスケジュールされている唯一のスレッドである場合、そのスレッドは中断されなくなり、他のO / Sスレッドで実行できたとしても、追加のスレッドのスケジューリングを検討するためにスケジューラが再実行されることはありません。

何かをテストするだけの場合は、シグネチャを変更します fibfib :: Integer -> IntegerInteger割り当てが発生するため、すべてが再び機能し始めます(の有無にかかわらず-threaded)。

実際のコードでこの問題が発生した場合、最も簡単な解決策は、@ Carlによって提案されたものです。スレッドの割り込み可能性を保証する必要がある場合は、コードをでコンパイルする必要があり-fno-omit-yieldsます。 。ドキュメントによると、これによりバイナリサイズが増加します。パフォーマンスのペナルティも少しあると思います。

または、計算がすでにで行われている場合は、最適化されたループでIO明示的に実行yieldすることをお勧めします。純粋な計算の場合は、IOとに変換できますがyield、通常、割り当てを再度導入する簡単な方法を見つけることができます。最も現実的な状況では、「少数」yieldまたは割り当てのみを導入する方法があります-スレッドを再び応答させるには十分ですが、パフォーマンスに深刻な影響を与えるほどではありません。(たとえば、ネストされた再帰ループがある場合、yieldまたは最も外側のループで割り当てを強制する場合)。


4
を意味する-fno-omit-yieldsので、でのコンパイルも検討してください。downloads.haskell.org/~ghc/latest/docs/html/users_guide/…-O2-fomit-yields
カール・

はい!関連する旗があると思いましたが、名前を覚えていなかったり、マニュアルで見つけることができませんでした。これは通常、最も簡単で信頼できる修正であり、それに応じて回答を更新しました。
KA Buhr
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.