Edited at

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

こんにちは、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の代わりにmapConcurrentlyforMの代わりにforConcurrentlyreplicateMの代わりにreplicateConcurrentlyがあり、それぞれアクションを捨てる~~~Concurrently_関数がそれぞれ用意されています。先ほど処理はこのように書くことができます。

main :: IO ()

main = do
putStrLn "start"
forConcurrently_ [1 .. 3] sleepAndPut
putStrLn "done"


とりあえずここまで

想像より簡単に非同期処理ができることがわかったかと思います。非同期処理のキャンセルや変数については別記事にて紹介する予定です。