新世代の並列処理言語Google Goをひもとく

第3回 ハロー、goroutine!

赤坂 けい
チームWordProgress

2010/1/22

goroutineが生成した値を、そのつど受け取るパターン

 さて、goroutineの処理結果をチャネル経由で順序立てて受け取るパターンをマスターしよう。並列処理されるgoroutineでは、それぞれのgoroutineがチャネル経由で返す値の順序は保証されない(処理が終わるつど、チャネルに渡される)。

 そのため、goroutineの処理結果を順々に受け取るためには、ちょっとした工夫が必要だ。具体的には、チャネルを返す関数を定義し、その関数を変数に代入する工夫を行う。

 以下の例を見てみよう(この例は、無名関数for文による無限ループの例ともなっている。同時にマスターしてしまおう)。

●goroutine3.go
package main

// チャネルを返す関数gochの定義
func goch () chan int {
    ch := make(chan int);
    // goroutineの呼び出し
    go func (){ //無名関数を定義し、goroutineに渡す
        for i := 0; ; i++ { //無限ループ1
            ch <- i;
        }
    }();
    //ch経由で値を受け取るたびに、gochはchを返す
    return ch;
}

func main() {
    stream := goch();
    for  {  //無限ループ2
        println(<-stream); // 戻ってきた値を表示する
    }; 
}

 goch関数は、内部でチャネルchを定義した後、無名関数を定義し、goroutineとして呼び出している。go文は、引数として無名関数を受け取ることができる(ここでは、以下のような無名関数が定義されている)。

func (){ 
        for i := 0; ; i++ {
            ch <- i;
        }
    }

 この関数内のfor文は、初期値が0の無限ループを構成する。forの条件式内で、セミコロンが2つならんでいる。ここは、本来ループが継続する条件を書くところである。ここの記述が省略された場合、値が「真」として評価され、結果として無限ループとなる。この無限ループでは、ループがひと回りするごとにチャネルに値(i=0、1、2、3……)が代入されている。

 goch関数内では、続いてチャネルchを返すreturn文が定義されている。

 一方、main関数は、内部でgoch関数にstreamという変数名をつけて保持している(定義した関数からオブジェクトstreamを生成していると理解しておこう)。その後、次のような無限ループを定義している。

    for  {  //無限ループ2
        println(<-stream); // 戻ってきた値を表示する
    }; 

 Goでは、forの条件式をすべて省略すると無限ループとなるのだ。

 そして、ループ内ではチャネルから取り出された値をプリント(println)している。println関数の引数である「<-stream」という表記には、慣れるまで注意が必要だろう。

 この表記は、streamから値が取り出されるたびに、println関数の呼び出しが完了することを意味する(streamの値が更新されない限り、関数呼び出しは完了しない)。

 さて、以上で確認したように、定義されている2つの関数のそれぞれに無限ループが存在する。これらは、並列で実行される。「無限ループ1」とコメントした部分のループは、goroutine内に存在し、チャネルに次々と値を代入している。

 また、「無限ループ2」とコメントした部分のループでは、チャネルから次々と値を取り出しプリントしている。このようにチャネルは、複数の無限ループに次々と値を渡す目的で用いることができる。

 実行結果は以下のとおりとなる(無限ループであるため、Ctrl + Cなどで実行を止めてほしい)。

●実行結果
1
2
3
4
5
(以下省略)

 以上がGo流の並列処理の基本だ。他言語で、並列処理を記述したことのない方は、実行順序が少しイメージしづらかったかもしれない。そのような方は、以下のgoroutine4.goも見ておこう。

●goroutine4.go
package main

func goch (x int) chan int {
        ch := make(chan int);
    go func (){ 
        for { 
            ch <- x+1;
            print(".")      // ……(1)
        }
    }();
    print("*");             // ……(2)
        return ch;
}

func main() {
    var x = 0;
   for ;x <10; {
        stream := goch(x);
        x = <-stream;
        println(x)           // ……(3)
    }; 
}
●実行結果
*.1
*.2
*.3
*.4
*.5
*.6
*.7
*.8
*.9
*.10

 このプログラムの実行結果から、(1)、(2)、(3)とコメントした部分のプリント文が順に実行されていることが分かるだろう。

 ここでは、以下の2点を補足しておきたい。

 まず、main関数中のfor文では、条件式内に2番目の値のみが、ループを継続する条件として記述されている。このような記述をすると、Javaなどのwhile文と同様の振る舞いとなる。

 ここの「for ;x <10; {……}」という表記は、「for x <10 {……}」と書き換えても動作する。この表記ならば、見た目もwhile文とほぼ同様だろう(Goにはwhile文がないため、この記法に親しんでおくとよいだろう)。

 また、forループ中では、<-演算子を用い、あらかじめ定義された変数xにstreamの実行結果を代入している(「x = <-stream」という記述)。

goroutineでクロージャを活用するパターン

 goroutine3.goに出てきたように、Goでは、関数は値として代入可能である。

 すなわち、Goでは、関数はファーストクラスオブジェクトであり、関数をほかの関数への引数とすることもできる(いわゆるクロージャ)。ファーストクラスオブジェクトやクロージャについては、馴染みのない方もおられるだろうが、今回はgoroutineの「雰囲気」をつかむことを目的としているため、これらの解説については省略させていただきたい(またの機会に解説したい)。

 そして、クロージャはgoroutineでも活用可能である。例を見ていこう。

●goroutine5.go
package main
import ("fmt")
 
func goch (g func (int ) int) chan int {
        ch := make(chan int);
    go func (){
        for i := 0; ; i++ { //無限ループ
            ch <-g(i); //受け取った関数をgoroutine内で実行
        }
    }();
        return ch;
}
 
func main() {
    //クロージャ1の定義
    g1 :=func(i int) int{
        even := i*2;
        fmt.Printf ("[e:%d]",even); //偶数のプリント
        return even
    };
    stream1 := goch(g1);
 
    //クロージャ2の定義
     g2 :=  func(i int) int{
            odd := i*2+1;
            fmt.Printf ("[o:%d]",odd); //奇数のプリント
            return odd
        };
    stream2 :=goch(g2);
    for i:=0; i <= 20;i++ {
        println(<-stream1,"\t",<-stream2); //goroutineから返された値をプリント
    };
}

 main関数内では、goch関数から2つのオブジェクト、stream1とstream2が定義されている。定義時に、stream1のgoch関数には、任意の整数から偶数を生成する関数g1が渡される。stream2のgoch関数には、任意の整数から偶数を生成する無名関数が渡される。

 stream1とstream2は、for文内のprint文内で実行される。

 実行結果を確認しやすくするため、3カ所にプリント文を記述している。実行結果を確認してみよう(ここでは、Windows VistaとUbuntuマシンそれぞれの実行結果を記す)。

●実行結果
[Windowsマシンで実行]
[e:0][o:1][e:2]0        1
2        [e:4][o:3][o:5]3
4        [e:6]5
6        [e:8][o:7]7
8        [e:10][o:9][o:11]9
10       [e:12]11
12       [e:14][o:13]13
14       [e:16][o:15]15
16       [o:17][o:19]17
[e:18]18         19
[o:21][e:20]20          21
[o:23][e:22]22          [e:24]23
24       [e:26][o:25]25
26       [e:28][o:27]27
28       [e:30][o:29]29
30       [e:32][o:31]31
32       [e:34][o:33]33
34       [e:36][o:35]35
36       [e:38][o:37]37
38       [e:40][o:39]39
40       [e:42][o:41]41

[Linux(Ubuntu)マシンで実行]
[e:0][o:1][e:2]0         1
[e:4][o:3]2         [o:5]3
4         [o:7][e:6]5
[e:8]6         7
[e:10][o:9]8         [o:11]9
10         [o:13][e:12]11
[e:14]12         13
[e:16][o:15]14         [o:17]15
16         [o:19][e:18]17
[e:20]18         19
[e:22][o:21]20         [o:23]21
22         [o:25][e:24]23
[e:26]24         25
[e:28][o:27]26         [o:29]27
28         [o:31][e:30]29
[e:32]30         31
[e:34][o:33]32         [o:35]33
34         [o:37][e:36]35
[e:38]36         37
[e:40][o:39]38         [o:41]39
40         [o:43][e:42]41

 WindowsマシンとUbuntuマシンの実行で、プリントされる順序が変わっている(読者の環境では、別途の実行結果が得られていることだろう)。詳しくみていくと、並列処理されているstream1とstream2が実行される順序が、それぞれの実行結果で異なっていることが分かるだろう。イメージがつかめるだろうか。

2つの実行結果のうち、goroutineから返された値をプリントする「println(<-stream1,"\t",<-stream2);」の実行結果(プリントされる数字の順序)が完全に一致している理由を考えてみてほしい。

 今回は、Goでもっとも興味深い機能であろうgoroutineの並列処理を体験した。また、無名関数やクロージャも同時に試してみた。並列処理やクロージャになじみのない方には、やや難しかったかもしれない。実行結果を丁寧に追いかけてじっくり理解していただきたい。

 次回は、goroutineの並列処理をもう少し深めるとともに、Goのオブジェクト指向や型システムのイントロダクションに着手したい。Goの賢いビルド方法やGoのライブラリの概要、さらには、新バージョンのライブラリなどへの更新方法なども随時取り上げていきたい。

【追記】

 なお、クロージャは、無名関数として渡すことができる。そのため、goroutine5.goのstream5は、以下のように定義することもできる。

//クロージャの記述法2(無名関数として渡す)
stream2 :=goch(
    func(i int) int{
        odd := i*2+1;
        fmt.Printf ("[o:%d]}",odd);//奇数のプリント
        return odd 
    }
);

 しかし、2010年1月13日のアップデートを適用したUbuntuマシンでは、このコードは動作しなかった(原因は調査中)。ほかのマシンでは問題なく動作している。

prev
2/2

Index
ハロー、goroutine!
  Page1
Goのマーケットシェアは1.25%
Goの軽量で簡潔な並列処理記述
チャネル間の通信を記述する<-演算子
  Page2
goroutineが生成した値を、そのつど受け取るパターン
goroutineでクロージャを活用するパターン
index 新世代の並列処理言語Google Goをひもとく

 Coding Edgeお勧め記事
いまさらアルゴリズムを学ぶ意味
コーディングに役立つ! アルゴリズムの基本(1)
 コンピュータに「3の倍数と3の付く数字」を判断させるにはどうしたらいいか。発想力を鍛えよう
Zope 3の魅力に迫る
Zope 3とは何ぞや?(1)
 Pythonで書かれたWebアプリケーションフレームワーク「Zope 3」。ほかのソフトウェアとは一体何が違っているのか?
貧弱環境プログラミングのススメ
柴田 淳のコーディング天国
 高性能なIT機器に囲まれた環境でコンピュータの動作原理に触れることは可能だろうか。貧弱なPC上にビットマップの直線をどうやって引く?
Haskellプログラミングの楽しみ方
のんびりHaskell(1)
 関数型言語に分類されるHaskell。C言語などの手続き型言語とまったく異なるプログラミングの世界に踏み出してみよう
ちょっと変わったLisp入門
Gaucheでメタプログラミング(1)
 Lispの一種であるScheme。いくつかある処理系の中でも気軽にスクリプトを書けるGaucheでLispの世界を体験してみよう
  Coding Edgeフォーラムフィード  2.01.00.91


Coding Edge フォーラム 新着記事
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

>

Coding Edge 記事ランキング

本日 月間