自販機例

次のコインと製品を取引できる自販機を考える

  public class Coin {
    private int value;
    public Coin(int value) {
      if (value > 0) {
        this.value = value;
      }
    }
    public int Value {
      get {
        return this.value;
      }
    }
  }

  public class Product {
    private string name;
    public Product(string name) {
      this.name = name;
    }
    
    public string Name {
      get {
        return this.name;
      }
    }
  }

まずは簡単な1コイン、1製品(キャンセルつき)を作る。

  public class Dispencer1 {
    public void CoinIn(Coin coin) {
      Put(coin);
    }
    
    private async Put(Coin coin);
    public Coin Cancel() & Put(Coin coin) {
      return coin;
    }
    public Product Get() & Put(Coin coin) {
      return new Product("coke");
    }
  }

このクラスのpublicメソッドはすべて並列に(も)動く。つまり、CoinInもCancelもGetも別々のスレッドから呼び出せる。コインが入るまでCancelやGetは待つし、コインは入れ続けられる。コインの数だけCancelやGetができる。

ここのポイントは、asyncメソッドはいわばCoinを保持するメンバー変数(さらにマルチスレッドアクセス対応)のように使えるのである。

つぎに、コインごとにCancelやGetするのではなく、Coinの合計を一度にCancelやGetできるようにする。

  public class Dispencer2 {
    private async Put(Coin store);
    
    public Dispencer2() {
      PutValue(0);
    }
    
    private void PutValue(int value) {
      Put(new Coin(value));
    }
    
    public void CoinIn(Coin coin) & Put(Coin store) {
      PutValue(store.Value + coin.Value);
    }
    
    public Coin Cancel() & Put(Coin store) {
      PutValue(0);
      return store;
    }
    public Product Get() & Put(Coin store) {
      PutValue(0);
      return new Product("coke");
    }
  }

ここでは、publicメソッドすべてにasyncメソッドのPutを条件に置き、さらにコンストラクタで0をPutし、各メソッドで最後にPutしている。このPutによって保持しているコインは必ず整合性の取れた値を維持する。つまり、CoinIn、Cancel、Getの中の(Putを呼び出すまでの)処理は1つの処理列に整列されることになる。

ここのポイントは、メソッドをあるasyncメソッドを条件にし、その中で同じasyncメソッドを呼び出すことで、変更可能なメンバーのように使えることである。

さらに製品の値段を固定にして、その値段だけ消費するようにする。

  public class Dispencer3 {
    private async NotReady(Coin store);
    private async Ready(Coin store);
    private int price = 120;
    
    public Dispencer3() {
      PutValue(0);
    }
    
    private void PutValue(int value) {
      Coin newStore = new Coin(value);
      if (value > price) {
        Ready(newStore);
      } else {
        NotReady(newStore);
      }
    }
    
    public void CoinIn(Coin coin) & Ready(Coin store) {
      PutValue(store.Value + coin.Value);
    } & NotReady(Coin store) {
      PutValue(store.Value + coin.Value);
    }
    
    public Coin Cancel() & Ready(Coin store) {
      PutValue(0);
      return store;
    } & NotReady(Coin store) {
      PutValue(0);
      return store;
    }
    
    public Product Get() & Ready(Coin store) {
      PutValue(store.Value - price);
      return new Product("coke");
    }
  }

ここでのポイントはasyncメソッドはPutひとつではなく、ReadyとNotReadyの二つに分け、publicメソッドでもそれぞれの場合にわけて処理を書くことができることである。これによって、条件による平行制御が可能になる。この場合、条件に合えば進行し、合わなければ待ち状態になるというものになる(ReadyでないかぎりGetだけはできない)。

asyncメソッドのパラメータでなくても平行制御は可能である

  public class Dispencer3Dash {
    private async NotReady();
    private async Ready();
    private int total;
    private int price = 120;
    
    public Dispencer3Dash() {
      PutValue(0);
    }
    
    private void PutValue(int total) {
      this.total = value;
      if (total > price) {
        Ready();
      } else {
        NotReady();
      }
    }
    
    public void CoinIn(Coin coin) & Ready() {
      PutValue(total + coin.Value);
    } & NotReady() {
      PutValue(total + coin.Value);
    }
    
    public Coin Cancel() & Ready() {
      Coin coin = new Coin(total);
      PutValue(0);
      return coin;
    } & NotReady() {
      Coin coin = new Coin(total);
      PutValue(0);
      return coin;
    }
    
    public Product Get() & Ready() {
      PutValue(total - price);
      return new Product("coke");
    } 
  }

単純にメンバーを制御するすべてのコード部分をasyncメソッドで囲んでいれば、その間は排他制御可能(アトミック)になる。