nimは非常に書きやすくお気に入りの言語です。
ついに1.0になりました🎉
そんなNimの非同期に触れてみたいと思います。
※nim version 1.0.2で動かしています。
非同期IO
asyncdispatchモジュールを使います。
import asyncdispatch, httpclient
proc asyncRequest(url: string): Future[AsyncResponse] {.async.} =
let client = newAsyncHttpClient()
let response = await client.get(url)
return response
proc main() {.async.} =
try:
let response = await asyncRequest("https://forum.nim-lang.org/")
except:
echo "Operation Failed"
waitFor main()
非同期IOはasync pragmaと awaitをつけるだけで簡単に行うことができます。
This allows you to write asynchronous code in a synchronous style and works similar to C#'s await
とあるとおりC#ライクです。
戻り値はFuture型になっていて、こちらもC#のTaskにそっくりです。
Python風で非同期はC#風って嬉しい。
Future作り方
newFutureで作れます。
成功でcomplete 失敗でfailを返せばいいようです。
import os, asyncdispatch
proc createFuture1(): Future[string] =
var retFuture = newFuture[string]("createFuture1")
echo "1: Start"
sleep(2000)
echo "1: End"
retFuture.complete("Future1完了")
return retFuture
proc createFuture2(): Future[string] =
var retFuture = newFuture[string]("createFuture2")
echo "2: Start"
sleep(5000)
echo "2: End"
retFuture.complete("Future2完了")
return retFuture
proc main() {.async.} =
try:
var fut1 = await createFuture1()
var fut2 = await createFuture2()
echo fut1
echo fut2
except:
echo "Operation Failed"
waitFor main()
1: Start
1: EndWait
2: Start
2: End
Future1完了
Future2完了
複数のFuture完了待ち
まとめて処理したい場合は await allですべて完了するまで待つことができるようです。すばらしい
proc main() {.async.} =
try:
var fut1 = createFuture1()
var fut2 = createFuture2()
var results = await all(@[fut1, fut2])
for result in results:
echo result
except:
echo "Operation Failed"
waitFor main()
動かしてみます。
1: Start
1: End
2: Start
2: End
Future1完了
Future2完了
となり、sleepで待って同期的にうごいてしまいます。C#のTask.Run 感覚ではだめでした。
sleepはsleepAsyncを使い、下のように変えるとうまく動きました。
import asyncdispatch
proc createFuture1(): Future[string] {.async.} =
echo "1: Start"
await sleepAsync(2000)
echo "1: End"
return "Future1完了"
proc createFuture2(): Future[string] {.async.} =
echo "2: Start"
await sleepAsync(5000)
echo "2: End"
return "Future2完了"
proc main() {.async.} =
try:
var fut1 = createFuture1()
var fut2 = createFuture2()
var results = await all(@[fut1, fut2])
for result in results:
echo result
except:
echo "Operation Failed"
waitFor main()
1: Start
2: Start
1: End
2: End
Future1完了
Future2完了
イベントループですね。
シングルスレッドで動作するのでsleepで普通に止まります。
マルチスレッド
普通にスレッド作ることもできますが、threadpoolモジュール を使うのがよさそうです。
スレッドを管理してもらえます。
Spawnすると新しいスレッドで動作し、結果はFlowVar型に包まれます。
Sync()でspawnした処理がすべて完了するまで待ちます。
結果は^で取り出せます。
import os, threadpool
proc heavyMethod1(): int =
echo "1: Start"
sleep(5000)
echo "1: End"
return 5
proc heavyMethod2(): int =
echo "2: Start"
sleep(2000)
echo "2: End"
return 20
var result1 = spawn heavyMethod1()
var result2 = spawn heavyMethod2()
sync()
echo ^result1
echo ^result2
1: Start
2: Start
2: End
1: End
5
20
^だけで完了するまで待ってくれます。
var result1 = spawn heavyMethod1()
echo ^result1
var result2 = spawn heavyMethod2()
echo ^result2
1: Start
1: End
5
2: Start
2: End
20
Parallelステートメント
experimentalですが、
Parallelステートメントを使うとFlowVarとか意識しなくてもよくなります。
{.experimental: "parallel".}
parallel:
var result1 = spawn heavyMethod1()
var result2 = spawn heavyMethod2()
echo result1
echo result2
すっきりでとてもよいですね。
しかしexperimentalだからか私の書き方が悪かったのかstring型だとうまく戻ってくれませんでした。
今後に期待です。
まとめ
IOバウンドな場合はasync+await
CPUバウンドな場合はspawn
C#のasync/awaitは一緒になっていてハマりどころだったりします。
わかれているのはわかりやすい。