search
LoginSignup
15

More than 1 year has passed since last update.

posted at

updated at

Nimの非同期IOとマルチスレッド

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は一緒になっていてハマりどころだったりします。
わかれているのはわかりやすい。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
15