こんにちは、pxfncです。今日はHackageが502を返してくる不機嫌な日でした(早く機嫌なおして)
asyncパッケージを使って非同期で遊んでみたので紹介しようと思います。
逐次処理の普通のプログラム
import Control.Concurrent ( threadDelay )
sleep :: Int -> IO ()
sleep n = threadDelay (1000000 * n)
main :: IO ()
main = do
putStrLn "start"
sleep 1
putStrLn "1 second later" -- startから1秒後に表示
sleep 2
putStrLn "2 second later" -- startから3秒後に表示
sleep 3
putStrLn "3 second later" -- startから6秒後に表示
putStrLn "done" -- startから6秒後に表示
このプログラムは合計6秒かけて実行されます。これをasyncパッケージを使って非同期にしていきます。
async関数で非同期にする
n秒待ってから"n second later"
と表示する部分をsleepAndPut
関数にくくり出してみます。そして、sleepAndPut 1
だけ非同期に実行してみましょう。やり方は簡単で、async
関数を、非同期にしたいIO a
の値に適用するだけです。つまり、async (sleepAndPut 1)
とするだけです
import Control.Concurrent ( threadDelay )
import Control.Concurrent.Async ( async )
sleep :: Int -> IO ()
sleep n = threadDelay (1000000 * n)
sleepAndPut :: Int -> IO ()
sleepAndPut n = do
sleep n
putStrLn $ show n <> " second later"
main :: IO ()
main = do
putStrLn "start"
async $ sleepAndPut 1 -- startから1秒後に表示
sleepAndPut 2 -- startから2秒後に表示
sleepAndPut 3 -- startから5秒後に表示
putStrLn "done" -- startから5秒後に表示
実行結果からわかることはasync $ sleepAndPut 1
の呼び出しが瞬時に終了して次のアクションが実行されていることです。今度は1秒から3秒全て非同期にしてみましょう。先ほどのmain関数を以下のように書き換えて実行すると
main :: IO ()
main = do
putStrLn "start"
async $ sleepAndPut 1 -- startから1秒後に表示
async $ sleepAndPut 2 -- startから2秒後に表示
async $ sleepAndPut 3 -- startから3秒後に表示
putStrLn "done" -- start直後に表示!!
最初にstartが表示された後に即座にdoneが表示され、その後sleepのログが出てしまっています。このような表示になるのは、sleepAndPutの全ての呼び出しが即座に終了するからです。
waitを使って待機をする
実はasync :: IO a -> IO (Async a)
の戻り値であるAsync a
を使うことで非同期の終了を待機することが可能です。非同期の実行を待つ関数が、このwait :: Async a -> IO a
です。
先ほどのプログラムを非同期処理が完了してから終了するように書き換えてみます
import Control.Concurrent ( threadDelay )
import Control.Concurrent.Async ( async, wait )
sleep :: Int -> IO ()
sleep n = threadDelay (1000000 * n)
sleepAndPut :: Int -> IO ()
sleepAndPut n = do
sleep n
putStrLn $ show n <> " second later"
main :: IO ()
main = do
putStrLn "start"
a1 <- async $ sleepAndPut 1 -- startから1秒後に表示
a2 <- async $ sleepAndPut 2 -- startから2秒後に表示
a3 <- async $ sleepAndPut 3 -- startから3秒後に表示
wait a1
wait a2
wait a3
putStrLn "done" -- startから3秒後に表示
手っ取り早く非同期がしたいんだが?
mapM
の代わりにmapConcurrently
、forM
の代わりにforConcurrently
、replicateM
の代わりにreplicateConcurrently
があり、それぞれアクションを捨てる~~~Concurrently_
関数がそれぞれ用意されています。先ほど処理はこのように書くことができます。
main :: IO ()
main = do
putStrLn "start"
forConcurrently_ [1 .. 3] sleepAndPut
putStrLn "done"
とりあえずここまで
想像より簡単に非同期処理ができることがわかったかと思います。非同期処理のキャンセルや変数については別記事にて紹介する予定です。