倭マン's BLOG

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

Groovy で Read-Write Lock パターン

増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編』に載っているデザインパターンを Groovy/GPars で書こうシリーズ、今回は Read-Write Lock パターン。 このパターンも Java SE 5 で導入された java.util.concurrent パッケージ(正確には java.util.concurrent.lock パッケージ)に定義されているクラス群を使うと簡単に実装できます。

今までのデザインパターンで処理を待たせる、つまりある意味でロックをとるものがいくつかありましたが、Read-Write Lock パターンでは、読み込みと書き込みで違ったロックの取り方をするのが特徴です。

  • 複数のスレッドから並行に読み込みができる
  • 単一のスレッドからしか書き込みができない
  • 書き込んでいる最中は読み込みができない

といった具合です。 こういうロックの取り方は結構必要になることが多いようなので、標準 API としてこれらを実装したクラス群が提供されてるんでしょう。

java.util.concurrent に定義されている Read-Write Lock に関連するインターフェースは

  • ReadWriteLock
  • Lock

です。


ロックの取得や解放は Lock オブジェクトを用いて行いますが(Lock#lock(), Lock#unlock() メソッド)、これらの Lock オブジェクトは ReadWriteLock オブジェクトから生成もしくは取得します。 ReadWriteLock インターフェースを実装した具象クラスは、標準 APIReentrantReadWriteLock クラスのみが提供されています。 ちなみに、Read-Write Lock ではなく、単純なロックを使いたい場合は Lock インターフェースとそれを実装した ReentrantLock クラスも使えます。

サンプル・コード


サンプルで作成するソースは

  • Data クラス
  • WriterActor
  • 実行スクリプト (readActor を含む)

です。 Read-Write Lock パターンのキモは Data クラスです。 Data オブジェクトからの読み書きに Read-Write Lock パターンを使っています。 Data 以外のクラス、スクリプトは Read-Write Lock パターンとあまり関係ありません。

Data クラス

Read-Write Lock パターンを実装したクラス。 ReadWriteLock オブジェクトや Lock オブジェクトを保持し、read(), write() メソッド内で対応するロックの取得・解放を行っています:

import java.util.concurrent.locks.ReadWriteLock
import java.util.concurrent.locks.ReentrantReadWriteLock
import java.util.concurrent.locks.Lock

class Data {
    
    private final char[] buffer
    private final int n
    private final ReadWriteLock lock = new ReentrantReadWriteLock(true)
    private final Lock readLock = lock.readLock()
    private final Lock writeLock = lock.writeLock()
    
    Data(int size){
        this.n = size
        this.buffer = ['*']*n as char[]
    }
    
    char[] read(){
        this.readLock.lock()    // 読み込みのロック取得
        try{
            char[] newBuffer = Arrays.copyOf(this.buffer, n)
            slowly()
            return newBuffer
            
        }finally{
            this.readLock.unlock()    // 読み込みのロック解放
        }
    }
    
    void write(char c){
        this.writeLock.lock()    // 書き込みのロック取得
        try{
           (0..<n).each{ this.buffer[it] = c }
            slowly()
            
        }finally{
            this.writeLock.unlock()    // 書き込みのロック解放
        }
    }
    
    private void slowly(){
        Thread.sleep 50
    }
}
  • ReentrantReadWriteLock クラスのコンストラクタに渡している boolean 値はフェアネスを設定しています。 これが true だと、長く待たされているスレッドに優先的にロックが渡されるようになります。

WriterActor クラス

データ書き込みを行うスレッドを Actor で実装:

import groovyx.gpars.actor.DefaultActor

class WriterActor extends DefaultActor{
    
    private static final Random RANDOM = new Random()
    
    private final Data data
    private final String filler
    private int index = 0
    
    WriterActor(Data data, String filler){
        this.data = data
        this.filler = filler
    }
    
    @Override
    void act(){
        loop{
            char c = nextChar()
            this.data.write(c)
            Thread.sleep RANDOM.nextInt(3000)
        }
    }
    
    private char nextChar(){
        char c = this.filler.charAt(this.index)
        this.index = (this.index + 1) % this.filler.size()
        return c
    }
}

実行スクリプト

実行スクリプト。 データを読み込むスレッドを Actor として作成もしています:

import groovyx.gpars.actor.Actors

final Data data = new Data(10)

def actors = []
6.times{
    actors << Actors.actor{
        loop{
            char[] readBuffer = data.read()
            println "${Thread.currentThread().name} reads ${String.valueOf(readBuffer)}"
        }
    }
}

actors << new WriterActor(data, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ').start()
actors << new WriterActor(data, 'abcdefghijklmnopqrstuvwxyz').start()

actors*.join()

java.util.concurrent パッケージのクラス群のお陰で、Read-Write Lock パターン関連の実装はかなり簡単にできますね。 なんか、チャチな Actor 実装を書く練習と化しております。

追記


id:uehaj 氏にご指摘いただいた Groovy の @WithReadLock, @WithWriteLock アノテーションを使ったサンプル。 上記のコードをもっと手軽に書けます:

import groovy.transform.WithReadLock
import groovy.transform.WithWriteLock

class GroovyData {

    private final char[] buffer
    private final int n

    GroovyData(int size){
        this.n = size
        this.buffer = ['*']*n as char[]
    }

    @WithReadLock
    char[] read(){
        char[] newBuffer = Arrays.copyOf(this.buffer, n)
        slowly()
        return newBuffer
    }

    @WithWriteLock
    void write(char c){
       (0..<n).each{ this.buffer[it] = c }
        slowly()
    }
    
    private void slowly(){
        Thread.sleep 50
    }
}

これらのアノテーションが付与されたメソッドは、上記のサンプルの対応するメソッドとほぼ同じコードに AST 変換されるようです。

参考 URL

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

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


プログラミングGROOVY

プログラミングGROOVY