エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Ioが面白い・その0 の その8 入場はアンセムにのって

「3日目」の章、の3日目。今回で「7つの言語 7つの世界 Ioの章」のラスト。

7つの言語 7つの世界

7つの言語 7つの世界

コルーティン

Ioで非同期処理をするばあい、コルーティンを使います。


まず、普通の処理の場合。

say := method(
    for(i, 1, 3, writeln("[", name, "] ", i))
)

sub1 := Object clone do(name := "sub1")
sub2 := Object clone do(name := "sub2")
sub3 := Object clone do(name := "sub3")
name := "main"

sub1 say
sub2 say
sub3 say
say

実行結果。

$ io coro1.io 
[sub1] 1
[sub1] 2
[sub1] 3
[sub2] 1
[sub2] 2
[sub2] 3
[sub3] 1
[sub3] 2
[sub3] 3
[main] 1
[main] 2
[main] 3

普通に順番に処理されます。


自分の処理を止めて別のコルーティンに実行権をわたすにはyieldを使います。
上のスクリプトyieldを入れてみます。

say := method(
    for(i, 1, 3,
        writeln("[", name, "] ", i)
        yield
    )
)

sub1 := Object clone do(name := "sub1")
sub2 := Object clone do(name := "sub1")
sub3 := Object clone do(name := "sub1")
name := "main"

sub1 say
sub2 say
sub3 say
say

実行結果…は変わらないので省略。実のところ各オブジェクトが別々のコルーティンで動作している必要があるのでyieldを挿入しただけでは非同期に動作しません。


各オブジェクトを非同期に動作させるためにはメッセージを非同期メッセージとして送ります。そのばあい、メッセージの前に@または@@をつけます。ふたつの違いは値を返すか返さない(nilを返す)かの違いで、動作は同じです。

実例。

say := method(
    for(i, 1, 3,
        writeln("[", name, "] ", i)
        yield
    )
)

sub1 := Object clone do(name := "sub1")
sub2 := Object clone do(name := "sub2")
sub3 := Object clone do(name := "sub3")
name := "main"

sub1 @@say
sub2 @@say
sub3 @@say
say

実行結果

[main] 1
[sub3] 1
[sub2] 1
[sub1] 1
[main] 2
[sub3] 2
[sub2] 2
[sub1] 2
[main] 3
[sub3] 3
[sub2] 3
[sub1] 3

各々のオブジェクトでyieldが実行されるたびに次のオブジェクトに実行権が移り、順に動作しているのがわかります。

オブジェクトが実行待ちしている様子も表示してみます。

say := method(
    for(i, 1, 3,
        writeln("[", name, "] ", i, " : yielding ", 
                Scheduler yieldingCoros map(runTarget name))
        yield
    )
)

sub1 := Object clone do(name := "sub1")
sub2 := Object clone do(name := "sub2")
sub3 := Object clone do(name := "sub3")
name := "main"

sub1 @@say
writeln("yielding ", Scheduler yieldingCoros map(runTarget name))
sub2 @@say
writeln("yielding ", Scheduler yieldingCoros map(runTarget name))
sub3 @@say
writeln("yielding ", Scheduler yieldingCoros map(runTarget name))
say

実行結果。

yielding list(sub1)
yielding list(sub2, sub1)
yielding list(sub3, sub2, sub1)
[main] 1 : yielding list(sub3, sub2, sub1)
[sub3] 1 : yielding list(sub2, sub1, main)
[sub2] 1 : yielding list(sub1, main, sub3)
[sub1] 1 : yielding list(main, sub3, sub2)
[main] 2 : yielding list(sub3, sub2, sub1)
[sub3] 2 : yielding list(sub2, sub1, main)
[sub2] 2 : yielding list(sub1, main, sub3)
[sub1] 2 : yielding list(main, sub3, sub2)
[main] 3 : yielding list(sub3, sub2, sub1)
[sub3] 3 : yielding list(sub2, sub1, main)
[sub2] 3 : yielding list(sub1, main, sub3)
[sub1] 3 : yielding list(main, sub3, sub2)

現在実行中のオブジェクトがyieldすると、キューの先頭のオブジェクトが実行に移り、yieldしたオブジェクトがキューの末尾に移動するのがわかります。


ひとつ謎なところ。
ガイドには次のように書かれています。

The Scheduler object is responsible for resuming coroutines that are yielding. The current scheduling system uses a simple first-in-first-out policy with no priorities.


Io Programming Guide / Scheduluer

ですが、最後に非同期メッセージを送ったオブジェクトがキューの先頭に挿入されています。Ioのソースコード該当部分を見てみても、たしかに先頭に挿入しています。末尾に追加するコードもコメントとして残っているんですが、方針の変更かなにかがあったんでしょうか。

アクター

コルーティンの説明をしてしまうと、独立してアクターについて説明することは実のところ少なくて。Ioにおいては非同期メッセージを受け取るオブジェクトはアクターになる、というシンプルなことのようです。

one := Object clone
two := Object clone
three := Object clone

one say := method(wait(1); writeln("1, "))
two say := method(wait(2); writeln("2, "))
three say := method(wait(3); writeln("3, "))

three @@say
two @@say
one @@say
wait(4)
writeln("Vitoria!")

実行結果。

$ io actor.io 
1, 
2, 
3, 
Vitoria!

実行すると1秒おきに「1, 」「2, 」「3, 」「Vitoria!」と表示されます。非同期メッセージを送った順序に関係なく、各々のオブジェクトが各々にメッセージを処理するためです。

フューチャ

実行権を受け渡しつつ動作するというのは、メインのプロセッサだけを使うばあいには、あまり恩恵を感じないのですが、プロセッサを使わない待ち状態が長い処理に使うと、待ち時間を有効に使うことができるようになります。

HugeFile := Object clone
HugeFile readContents := method(
    writeln("\treading huge file...")
    wait(5)
    writeln("\t...done")
    "huge contents"
)

hugeFile := HugeFile clone

contents := hugeFile @readContents
writeln("Huge file's contents is ...")
writeln(contents)

実行結果。

$ io future1.io 
Huge file's contents is ...
    reading huge file...
    (ここで5秒停止)
    ...done
huge contents

巨大ファイルを読み込むばあいを考えます。
単純にcontents := hugeFile readContentsとすると、contentsに値が入るまですべての処理がブロックしてしまいます。
そこでcontents := hugeFile @readContentsと非同期で処理されるようにします。contentsには必要な値がまだ入っていませんが、すぐに処理が戻り次の行のwriteln("Huge file's contents is ...")が実行されます。
次にwriteln(contents)contentsにアクセスすると、contentsの値はまだ有効でないので動作がブロックされhugeFileに処理が移り読み込みが始まります。
読み込みが終了するとcontentsの値が有効になりwritelnによって表示されます。


正直、これでもまだ利点がわかりにくいかも。
さらに手を加えて。

HugeFile := Object clone
HugeFile readContents := method(
    writeln("\treading huge file...")
    wait(5)
    writeln("\t...done")
    "huge contents"
)

hugeFile := HugeFile clone

contents := hugeFile @readContents
writeln("Huge file's contents is ...")
wait(1)
writeln("... do something ...")
writeln(contents)

実行結果。

$ io future2.io 
Huge file's contents is ...
    reading huge file...
... do something ...
    ...done
huge contents

hugeFileにメッセージを送ったあとに、処理をブロックするようななにかを実行したと考えてください(ここでは処理がブロックされたのをあらわすためにwait(1)を挿入してます)。これで実行権がhugeFileに移り、「reading huge file...」を表示しています。
hugeFilewait(5)で処理が停止すると、実行権が戻りwait(1)のブロックから抜けて「... do something ...」を表示します。次にcontentsにアクセスすることで再び処理がブロックます。「...done」を表示してreadContentsの処理が終わるとcontentsの値が有効になり、その値を表示して終了します。


もっと現実的な例として本書にもあるHTTPリクエストの例。

googleSite := Object clone
googleSite readContents := method(
    url := "http://www.google.com/"
    writeln("\tfetch \"", url, "\"")
    contents := URL with(url) fetch
    writeln("\tfetched")
    contents
)

contents := googleSite @readContents
writeln("read Google site")
wait(0.1)
writeln("...do something...")
writeln(contents sizeInBytes, " bytes read")
writeln("done")

実行結果。

$ io future3.io 
read Google site
    fetch "http://www.google.com/"
...do something...
    fetched
15412 bytes read


Futureについては、結城浩先生の書籍「増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編」に詳しい説明があります。とてもわかりやすく丁寧な説明がされていますので、関心のある方は一読をお勧めします。


「Futureパターン」はサイトでも紹介されています。

ちがった。


結城先生のサイトの書籍紹介ページ

いつか読むはずっと読まない:どっちを向いても未来、どこまで行っても未来

買い忘れてた。買ってこなきゃ。

FUTURE

FUTURE