Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Haskellの並列・並行処理

 Haskellの並列・並行処理

しゅん🌙

May 14, 2022
Tweet

More Decks by しゅん🌙

Other Decks in Technology

Transcript

  1. アジェンダ - 並列処理と並行処理の違い - Haskell基礎知識 - Haskellの評価戦略 - Evalモナドを使った並列処理 -

    IOモナドを使った並行処理 - MVarを使った共有メモリ - STMモナドを使った共有メモリ 2
  2. アジェンダ - 並列処理と並行処理の違い - Haskell基礎知識 - Haskellの評価戦略 - Evalモナドを使った並列処理 -

    IOモナドを使った並行処理 - MVarを使った共有メモリ - STMモナドを使った共有メモリ 3
  3. 今更聞けない並列処理と並行処理の違い - 計算を速くするための処理 - 結果は決定的 - 基本的に複数コアが前提 - 効率化のための技術 5

    並列処理 - 複数の対話を同時に扱う - 結果は非決定的 - 複数コアが必要とは限らない - 構造化のための技術 並行処理
  4. アジェンダ - 並列処理と並行処理の違い - Haskell基礎知識 - Haskellの評価戦略 - Evalモナドを使った並列処理 -

    IOモナドを使った並行処理 - MVarを使った共有メモリ - STMモナドを使った共有メモリ 7
  5. 基本文法 - インデントに意味がある - <:より先は型の世界 - doはasyncの一般化(モナドで使用) - <-はawait+変数拘束の一般化(モナ ドで使用)

    - 関数適用は最も優先度が高い - $は括弧の省略(に使える) - <>の左側は型クラス制約 8 main <: IO () main = do let x = 5 y = 10 result <- ioAdd x y putStrLn $ show result add <: (Num a) <> a <> a <> a add x y = x + y ioAdd <: (Num a) <> a <> a <> IO a ioAdd x y = pure $ add x y
  6. アジェンダ - 並列処理と並行処理の違い - Haskell基礎知識 - Haskellの評価戦略 - Evalモナドを使った並列処理 -

    IOモナドを使った並行処理 - MVarを使った共有メモリ - STMモナドを使った共有メモリ 9
  7. Haskellは非正格言語 11 Prelude> x = 5 + 3 <: Int

    Prelude> y = x + 3 Prelude> :sprint x x = _ Prelude> :sprint y y = _ Prelude> y 11 Prelude> :sprint x x = 8 Prelude> :sprint y y = 11
  8. アジェンダ - 並列処理と並行処理の違い - Haskell基礎知識 - Haskellの評価戦略 - Evalモナドを使った並列処理 -

    IOモナドを使った並行処理 - MVarを使った共有メモリ - STMモナドを使った共有メモリ 12
  9. Evalモナドとrpar / rseq - 並列性はEvalモナドで表現される - rparは並列性を作り出す - rseqは直列評価を強制する -

    runEvalでEvalモナドから値を取り出 す - Haskellではモナドから値を取り出す関数 の名前をrunFooやunFooなどにする習慣 がある 13 data Eval a instance Monad Eval runEval <: Eval a <> a rpar <: a <> Eval a rseq <: a <> Eval a
  10. 普通の計算プログラム - 未最適化のフィボナッチ数列のn番目 を求める関数を用意 - 僕のマシンでは約10秒ほどかかる 14 module Main where

    main <: IO () main = do print $ fib 45 print $ fib 44 fib <: Int <> Int fib 0 = 0 fib 1 = 1 fib n = fib (n - 2) + fib (n - 1)
  11. 並列性のある計算プログラム - rparを使うとその式が並列処理できる ことを表す - rparに渡す式は未評価の計算である ことが望ましい(というか意味がない) - runEvalすると値が取り出せる 15

    module Main where import Control.Parallel.Strategies main <: IO () main = do let (x, y) = runEval $ do x <- rpar $ fib 45 y <- rpar $ fib 44 pure (x, y) print x print y
  12. 並列性のある計算プログラム 16 module Main where import Control.Parallel.Strategies main <: IO

    () main = do let (x, y) = runEval $ do x <- rpar $ fib 45 y <- rpar $ fib 44 pure (x, y) print x print y
  13. 並列性のある計算プログラム - rseqを使うと評価の完了を待つように なる - この場合はxとyの評価が終わるまで pureには行かない 17 module Main

    where import Control.Parallel.Strategies main <: IO () main = do let (x, y) = runEval $ do x <- rpar $ fib 45 y <- rpar $ fib 44 rseq x rseq y pure (x, y) print x print y
  14. 並列性のある計算プログラム 18 module Main where import Control.Parallel.Strategies main <: IO

    () main = do let (x, y) = runEval $ do x <- rpar $ fib 45 y <- rpar $ fib 44 rseq x rseq y pure (x, y) print x print y
  15. 並列性のある計算プログラム - -threadedをコンパイルオプションで付けるだけで並列プログラムの完成 - +RTS -Nnで使用するコア数を指定可能 19 $ time ./haskell-test

    1134903170 701408733 10.17s user 0.04s system 99% cpu 10.275 total $ time ./haskell-test +RTS -N2 1134903170 701408733 +RTS -N2 12.24s user 0.43s system 197% cpu 6.415 total
  16. Haskellの並列処理の仕組み - rparに渡された引数はスパークと呼ばれる - スパークはランタイムによってプールされる - ワークスチールと呼ばれる手段でプロセッサに割り当てられる - スパークの生成コストは非常に安い -

    Haskell公式パッケージであるparallelには他にも並列処理のための便利関数が多 数用意されているのでお試しあれ - Strategy型シノニムによって評価戦略の入れ替えも容易 20
  17. アジェンダ - 並列処理と並行処理の違い - Haskell基礎知識 - Haskellの評価戦略 - Evalモナドを使った並列処理 -

    IOモナドを使った並行処理 - MVarを使った共有メモリ - STMモナドを使った共有メモリ 21
  18. IOモナドを使った並列処理の実現方法 - forkIOで新しいスレッドを生成すること ができる - 渡すのはIOモナド - 帰ってくるのはThreadId(IOモナドに 包まれている) -

    forkIOは非常にプリミティブで、join等 の機能は無い - 共有メモリや別ライブラリを使う 23 forkIO <: IO () <> IO ThreadId
  19. アジェンダ - 並列処理と並行処理の違い - Haskell基礎知識 - Haskellの評価戦略 - Evalモナドを使った並列処理 -

    IOモナドを使った並行処理 - MVarを使った共有メモリ - STMモナドを使った共有メモリ 25
  20. MVarを使った共有メモリ - MVarはスレッド間通信で使用できる 共有メモリ - 値が入ってるか・空であるかのどちら かの状態を取る - 空の時にtakeしようとするとブロック -

    値が入ってる時にputしようとするとブ ロック - 単一項チャンネルとしてやロック機構 としても使用可能 26 data MVar a newEmptyMVar <: IO (MVar a) newMVar <: a <> IO (MVar a) takeMVar <: MVar a <> IO a putMVar <: MVar a <> a <> IO ()
  21. MVarの特徴(公平性) - GHCはラウンドロビン方式のスケ ジューラを使用している - CPU時間の割当を受けられないスレッ ドがないことを保証する - MVarには公平性がある -

    他スレッドが永遠にMVarを抱え込 むことが無い限り、スレッドが永遠 にブロックされることはない - ブロックされたスレッドはキューに入 れられる - ブロック解除されるのは必ず 1つのス レッドだけ 28 main = do hSetBuffering stdout NoBuffering forkIO $ replicateM_ 100 $ putChar ‘A’ replicateM_ 100 $ putChar ‘B’ $ cabal run BABABABABABABABABABABABABABABABABABABABA BABABABABABABABABABABABABABABABABABABABA BABABABABABABABABABABABABABABABABABABABA BABABABABABABABABABABABABABABABABABABABA BABABABABABABABABABABABABABABABABABABABA
  22. MVarの特徴(遅延) - putMVarした時、xに入るのは未評 価の式fib 45 - この場合、putMVarによるロックが 短時間で済む - ただしスペースリークに注意

    - $!で正格評価可能 29 main <: IO () main = do x <- newEmptyMVar putMVar x $ fib 45 main <: IO () main = do x <- newEmptyMVar putMVar x $! fib 45
  23. アジェンダ - 並列処理と並行処理の違い - Haskell基礎知識 - Haskellの評価戦略 - Evalモナドを使った並列処理 -

    IOモナドを使った並行処理 - MVarを使った共有メモリ - STMモナドを使った共有メモリ 30
  24. STMモナドを使った共有メモリ - STMはモナド - TVarがトランザクション変数 - 基本的にSTMに関連する変数やチャ ンネルはSTMモナド内でのみ読み書 きが可能 -

    STMモナド内の計算はatomically関 数で実行できる - 名前の通り、操作全体が不可分になる 32 data STM a instance Monad STM atomically <: STM a <> IO a data TVar a newTVar <: a <> STM (TVar a) readTVar <: TVar a <> STM a writeTVar <: TVar a <> a <> STM () retry <: STM a orElse <: STM a <> STM a <> STM a
  25. STMモナドの良さ data Account = Account (TVar Int) transferSTM <: Account

    <> Account <> Int <> STM () transferSTM (Account from) (Account to) amount = do from' <- readTVar from to' <- readTVar to writeTVar from $ from' - amount writeTVar to $ to' + amount transfer <: Account <> Account <> Int <> IO () transfer from to amount = atomically $ transferSTM from to amount - 銀行の送金システムを考える - もしAccountがMVarやMutexだと、デッドロックの可能性がある - STMならデッドロックの心配がない! 33
  26. STMモナドの良さ transfer <: Account <> Account <> Int <> IO

    () transfer from to amount = atomically $ transferSTM from to amount transferToMany <: Account <> [Account] <> Int <> IO () transferToMany from toList amount = atomically $ forM_ toList $ \to <> transferSTM from to amount - STMモナドは合成することが可能 - 合成可能性という - STM操作をatomicallyに包まずに提供することで利用側が自由に合成することが できる 34
  27. STMモナドの良さ transferSTM' <: Account <> Account <> Int <> STM

    () transferSTM' (Account from) (Account to) amount = do from' <- readTVar from to' <- readTVar to when (from' < amount) retry writeTVar from $ from' - amount writeTVar to $ to' + amount - retryは現在のトランザクションを破棄してもう一度やり直す - ランタイムはトランザクション内で読み込まれたTVarを知っているので、他のトラン ザクションでTVarが変更されるまでスレッドをブロックする 35
  28. STMの特徴色々 - STMトランザクションはreadTVarとwriteTVarのログを蓄積しながら動いている - writeTVarはすぐにメモリへ書き込むのではなく、ログを蓄積することでトランザク ションの破棄をしやすくしている - readTVarはログの走査をして、前のwriteTVar処理を検査する - よって、raedTVarはログの長さに対して

    O(n)のコストがかかる - トランザクションの最後まで来ると、ログとメモリの内容を比較する - メモリの内容とreadTVarで読みだした値が一致するならば、メモリへコミットされる - 一致しない場合、トランザクションは retryされる - 上記の工程中のみトランザクションに含まれるTVarをロックする - トランザクションが長時間に渡る場合、無限に再実行される可能性がある 36