倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Groovy で Single Thread Execution パターン その1

Groovy でのマルチスレッド・プログラミングを練習するために、『増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編』にあるマルチスレッド・プログラミングのデザインパターンを Groovy/GPars で書いてみます。

今回は「Single Thread Execution」パターンです。 Critical Section, Critical Region とも呼ばれるそうです。 このパターンでは、複数のスレッドから変更される可能性のあるフィールドやリソースがある場合、その変更は同時に1つのスレッドからしか行えないようにする、というパターンです。 Single Thread Execution パターンはマルチスレッド・プログラミングの基本となるものだそうなので、いろいろな方法でイジってみます。

サンプル・コード


増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編』にあるコードを Groovy で書き換えたものをサンプルに使わせてもらいます(ちょっと変更ありますが)。 まずは実行するスクリプト:

println 'Testing Gate, hit CTRL+C to exit.'

Gate gate = new Gate();

// 各旅人を別スレッドで実行
Thread.start passenger(gate, "Alice", "Alaska")
Thread.start passenger(gate, "Bobby", "Brazil")
Thread.start passenger(gate, "Chris", "Canada")

// 旅人(Closure オブジェクト)を返す
def passenger(Gate gate, String name, String address){
    return {
        println "$name BEGIN"
        while(true){
            gate.pass(name, address)    // 門を通る
        }
    }
}

1つの門 (gate) があり(動作は後述)、3人の旅人 (passenger) がその門を独立したスレッドでひたすら通り続けます。 門の実装は

class Gate {

    private int counter = 0;
    private String name = 'Nobody'
    private String address = 'Nowhere'

    void pass(String name, String address){
        this.counter++;
        this.name = name
        this.address = address
        check();
    }

    private void check(){
        if(name.charAt(0) != address.charAt(0)){    // 名前と住所の頭文字が一致してなければ「BROKEN」
            println "***** BROKEN ***** No. $counter: $name, $address"
        }
    }
}

となっていて、通るたび(pass() が呼び出されるたび)名前と住所を記録してチェックします。 チェック時に名前と住所の頭文字が異なっていれば「BROKEN」と表示します。 シングル・スレッドで pass() が呼ばれれば問題ありませんが、マルチスレッドで、あるスレッドが pass() メソッドを呼び出しているときに別のスレッドが pass() の処理に入ると問題が起こります。

synchronized でスレッドセーフにする


スレッドセーフにする一番簡単な方法は Gate クラスの pass() メソッドを synchronized にするだけです:

class Gate {

    private int counter = 0;
    private String name = "Nobody";
    private String address = "Nowhere";

    // synchronized にする
    public synchronized void pass(String name, String address){
        this.counter++;
        this.name = name;
        this.address = address;
        check();
    }

    private void check(){
        if(name.charAt(0) != address.charAt(0)){
            println "***** BROKEN ***** No. $counter: $name, $address"
        }
    }
}

Actor でスレッドセーフにする


上記の方法で手軽にスレッドセーフにできるんですが、せっかく Groovy でやってるので GPars なんかも使ってやりたいところ。 ってことで、GPars での解決法も考えてみます。

GPars で Single Thread Execution パターンに使えそうなのは Actor と Agent でしょうかね。 さらに Actor ではいくつかやり方があるようです。 今回はあえて一番手間がかかりそうな、独自の Actor クラスを作成する方法を見ていきます。 その他のものはそのうちに。

独自の Actor クラスを作成する場合、DefaultActor を継承して作成するのが通例のようです。 実装が必要なメソッドは act() メソッドです:

import groovyx.gpars.actor.DefaultActor

class GateActor extends DefaultActor{

    private int counter = 0;
    private String name = "Nobody";
    private String address = "Nowhere";

    @Override
    void act() {
        loop {
            react { args ->     // メッセージの受信
                pass(args[0], args[1])
            }
        }
    }

    private void pass(String name, String address){
        this.counter++;
        this.name = name;
        this.address = address;
        check();
    }

    private void check(){
        if(name.charAt(0) != address.charAt(0)){
            println "***** BROKEN ***** No. $counter: $name, $address"
        }
    }
}
  • act() メソッド内の loop(), react() メソッドの呼び出しは大抵そのようにするようです。 react() メソッドに渡されているクロージャは、この Actor がメッセージを受信した際に呼び出されるようです。
  • pass() メソッド、check() メソッドは元のサンプルの Gate メソッドに定義されていたものと(ほぼ)同じです。 特に pass() メソッドは synchronized にする必要はありません。

この GateActor を使って、実行するスクリプトは以下のようになります:

println 'Testing Gate, hit CTRL+C to exit.'

GateActor gate = new GateActor().start()    // Actor もスレッドみたく start する

Thread.start passenger(gate, 'Alice', 'Alaska')
Thread.start passenger(gate, 'Bobby', 'Brazil')
Thread.start  passenger(gate, 'Chris', 'Canada')

def passenger(GateActor gate, String name, String address){
    return {
        println "$name BEGIN"
        while(true){
            gate << [name, address]    // Actor へメッセージを送信
        }
    }
}
  • Actor オブジェクトはスレッドのように start() メソッドを呼び出す必要があります。
  • Actor オブジェクトに対する処理は、メソッド呼び出しではなくメッセージ送信となるので、<< 演算子send() メソッドなどを使用します。

独自の Actor クラスを作成するとそこそこコードの分量が増えますが、実際にはもっと簡単に Actor オブジェクトを作成できます。 これは次回に。

参考 URL

増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編

増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編