Posted at

asyncでお手軽非同期プログラミング with Haskell その2

こんにちは、pxfncです。

前回に引き続き、今回は非同期処理のキャンセルをやります。

import           Control.Concurrent             ( threadDelay )

sleep :: Int -> IO ()
sleep n = threadDelay (n * 1000000)

-- 1秒待った後にstrを出力
waitAndPut :: String -> IO ()
waitAndPut str = do
sleep 1
putStrLn str

-- 無限に"loop"を出力するアクション、どう頑張っても終わらない
loop :: IO ()
loop = do
waitAndPut "loop"
loop

main :: IO ()
main = do
loop
putStrLn "done" -- ここに到達しない


キャンセルをする

では、このプログラムをエンターキーが押されるまで"loop"を出力し続けるプログラムに変えてみます。

これも実装は簡単で、asyncパッケージの中に非同期で実行されているアクションをキャンセルする関数があり、ズバリその名前もcancel :: Async a -> IO ()関数です。そして、エンターキーを入力されるまで実行が終わらないアクションとしてwaitEnter :: IO ()を用意すればあとはそれをいい感じに合成するだけで書くことができます。

import           Control.Concurrent             ( threadDelay )

import Control.Concurrent.Async ( async
, cancel
)

-- n秒待つ
sleep :: Int -> IO ()
sleep n = threadDelay (n * 1000000)

-- 1秒待った後にstrを出力
waitAndPut :: String -> IO ()
waitAndPut str = do
sleep 1
putStrLn str

-- 無限に"loop"を出力するアクション
loop :: IO ()
loop = do
waitAndPut "loop"
loop

-- 改行の入力を待つアクション
waitEnter :: IO ()
waitEnter = do
c <- getChar
if c == '\n' then pure () else waitEnter

main :: IO ()
main = do
action <- async loop -- loopを非同期で実行
waitEnter -- 改行が入力されるまで次に進まない
cancel action -- 非同期で実行してたactionを止める
putStrLn "done" -- ここに到達する!


知っておかなきゃいけないcancelの例外

実はcancelによって非同期の実行を止めるメカニズムとして非同期例外というものが使われています。

Haskellではスレッドの中で例外が発生すると全てをやるべきことをすっぽかして突然プログラムが終了するのですが、cancel actionがしているのは、actionが内部で保持している別のスレッドに向かってAsyncCancelledという例外を外側から投げつけるということをしているのです。結果として、例外を突然投げつけられたそのスレッドは、例外を受け取ったタイミングで即座に何もかものやる気を無くして落ちる、といった流れで実行が止まるのです。


まとめ



  • cancel :: Async a -> IO ()で非同期で実行してたアクションを止めることができる。

  • 非同期で実行されてる処理が終了できるのは、例外を投げつけて無理やり終了させているから。

モチベが続けば、次は1番大事な非同期例外されても安全に終了できるbracketやwith系関数について書ければなと思ってます(書け