チュートリアルの翻訳、10回目です。
前回までの訳はチュートリアル[日本語訳]にまとめてありますのでごらんください。


素数

やっと、プロセスと平行通信プログラミングのところまで到達しました。
これは重要なセクションであり、このトピックにはある程度の知識が必要です。

「素数の篩(ふるい)」と呼ばれる古くから存在する素数を取得するプログラムがあります。
(エラトステネスの篩はここで扱うアルゴリズムより効率的ではありますが、このセクションでは並列性を重要視しています)

このアルゴリズムでは、まず2以上の自然数を順番に並べて数列を作成します。そこから素数をひとつづ取り出し、その素数の倍数すべてを数列から除外することを繰り返します。残った数列の先頭から取り出した数が次の素数であり、そこからまた倍数をすべて除外して次の数列を作成します。

下が、これのフロー図です。それぞれの箱は数列の先頭から取り出した素数であり、この箱で数列をフィルタリングして次の数列を作成しています。  sieve

 整数の数列を作成するために、チャネルと呼ばれるGo言語の機能を使います。チャネルの基となったものはCSPであり、チャネルとは2つの並列動作している処理をつなぐ通信チャネルです。
Go言語のチャネル変数は、”通信を調整するランタイムオブジェクト”への参照です。
新しいチャネルをつくるには、マップやスライスと同様にmakeを使用してください。

下はprogs/sieve.go内の関数です。

09    // Send the sequence 2, 3, 4, ... to channel 'ch'.
10    func generate(ch chan int) {
11        for i := 2; ; i++ {
12            ch <- i  // Send 'i' to channel 'ch'.
13        }
14    }

このgenerate関数は、2,3,4,5,…という一連の値をパラメータchに送信します。送信には二項演算子<-を使用します。
チャネルの送信ではch変数を受信する側がいなければブロックされ、誰かが受信状態になるまで送信を保留します。

次のfilter関数では3つのパラメータ、入力チャネル・出力チャネル・素数を受け取り、入力チャネルの値を出力チャネルにコピーします。
その際、素数で割り切れる値は除外します。
受信に使用する単項演算子<-は、チャネル上から次の値を取り出します。 

16    // Copy the values from channel 'in' to channel 'out',
17    // removing those divisible by 'prime'.
18    func filter(in, out chan int, prime int) {
19        for {
20            i := <-in;  // Receive value of new variable 'i' from 'in'.
21            if i % prime != 0 {
22                out <- i  // Send 'i' to channel 'out'.
23            }
24        }
25    }

generator関数とfilters関数は平行動作します。
Go言語は独自のプロセス/スレッド/軽量プロセス/コルーチンモデルを持っていますが、
表記上の混乱を避けるためGo言語の平行実行処理をゴルーチン(goroutines)と呼ぶことにしています。
ゴルーチンを開始するためには、キーワードgoを前に付けて関数を呼び出します。
こうすると呼び出した関数はカレントの処理と同じアドレス空間のまま、平行動作を開始します。

    go sum(hugeArray); // calculate sum in the background

計算が終了したことを検知したいときは、つぎのようにして処理終了を通知するチャネルを関数に渡してください。

    ch := make(chan int);
    go sum(hugeArray, ch);
    // ... do something else for a while
    result := <-ch;  // wait for, and retrieve, result

素数の篩に話を戻しましょう。下のようにして篩パイプラインを組み合わせます。 

28    func main() {
29        ch := make(chan int);  // Create a new channel.
30        go generate(ch);  // Start generate() as a goroutine.
31        for {
32            prime := <-ch;
33            fmt.Println(prime);
34            ch1 := make(chan int);
35            go filter(ch, ch1, prime);
36            ch = ch1
37        }
38    }

まずはじめに、29行目でgenerate関数に渡すチャネルを初期化しています。
チャネルから素数をひとつ取り出し、新たにfilterをパイプラインに加えて、そこからの出力でch変数を置き換えます。

この篩プログラムは、共通プログラミングパターンを使って書き換えられます。
下のコードはgenerate関数を変更したバージョンです。このファイルはprogs/sieve1.goにあります。

10    func generate() chan int {
11        ch := make(chan int);
12        go func(){
13            for i := 2; ; i++ {
14                ch <- i
15            }
16        }();
17        return ch;
18    }

このバージョンでは、関数内ですべてのセットアップを行っています。
まず出力チャネルをつくり、次にリテラル関数をゴルーチンとして起動して、呼出元にチャネルを返します。
これはすなわちゴルーチンを起動しその接続を返すという、並列処理のためのファクトリです。

リテラル関数表記(12-16行目)によって、匿名の関数をつくって即座に呼び出しています。
注意していただきたいのは、ローカル変数chはリテラル関数からもアクセス可能で、generate関数から抜けたあとも残り続けることです。

同じ変更をfilter関数にも適用します。 

21    func filter(in chan int, prime int) chan int {
22        out := make(chan int);
23        go func() {
24            for {
25                if i := <-in; i % prime != 0 {
26                    out <- i
27                }
28            }
29        }();
30        return out;
31    }

その結果、メインループはよりシンプルで処理が明確になりました。
これも同様にファクトリに変更します。 

33    func sieve() chan int {
34        out := make(chan int);
35        go func() {
36            ch := generate();
37            for {
38                prime := <-ch;
39                out <- prime;
40                ch = filter(ch, prime);
41            }
42        }();
43        return out;
44    }

その結果、メイン関数とsieve関数との受け渡しはprimesチャネルだけになりました。

46    func main() {
47        primes := sieve();
48        for {
49            fmt.Println(<-primes);
50        }
51    }