Polyphonic C#

Polyphonic C#とはなんぞや、という人もいるかも知れませんが、ちょっと前に話題になったの一部となった非同期処理に関するプロジェクトです。詳しくはサイトを見ていただくとして、実は、Nemerleでも同様の機能をライブラリとしてサポートしていたりします。Polyphonic C#の紹介ついでに解説してみようかと思います。
ぶっちゃけ、Polyphonic C#だと何が嬉しいのか?

非同期処理(マルチスレッド)を分かりやすく書くことが出来ます。非同期処理で何が問題になるというと状態管理ですが、通常は変数を用いて状態管理を行います。しかし、Polyphonic C#はchordと呼ばれるメソッドを使って状態管理するところが、特徴的だと言えます。メソッドで状態管理とは何のこっちゃ?かもしれませんが、しばらくお付き合いください。

良くある例として、2つのスレッドの一方が書き込んで、もう一方が読み込むという処理を考えてみます。データの状態は値がある/ないの2つを取るとします。書き込む側は値が「ない」時にしか書き込めません。読み込む側は値が「ある」時にしか読み込めません。

さて、実装ですが、まず、状態を表すメソッドを用意します。

  • Empty () : 値がない
  • Contains (s : string) : 値がある

また、このデータへの操作として、

  • Put (s : string) : 値の設定
  • Get () : 値の取得

このようなメソッドを用意することにしましょう。

PutとGetはいつでも使えるわけではなく、PutはEmpty状態の場合のみ呼びだせ、状態をContainsに遷移させます。一方、GetはContains状態の場合のみ呼び出せ、状態をEmptyに遷移させます。Nemerleで実装すると以下のようになります。

public class OnePlaceBuffer {
    public this () {
        Empty ();
    }

    [ChordMember]
    Empty () : void;

    [ChordMember]
    Contains (s : string) : void;

    public Put (s : string) : void chord {
        | Empty => Contains (s);
    }

    public Get () : string chord {
        | Contains => Empty (); s;
    }
}

ソースコードを見ていきましょう。まず、コンストラクタ(this)で状態をEmptyにしています。Empty、Containsメソッドは状態を表すために使うのでChordMember属性を付け、実装は行いません。Putにはchordブロックがあり、そこでEmpty状態を待ちます。もし、Empty状態でない場合、このメソッドはブロックされます。Getも同様にContains状態になるのを待ちます。

では、このクラスを使ったサンプルコードを見てみましょう。

using System;
using System.Threading;
using Nemerle.Concurrency;

public class OnePlaceBuffer {
    public this () {
        Empty ();
    }

    [ChordMember]
    Empty () : void;

    [ChordMember]
    Contains (s : string) : void;

    public Put (s : string) : void chord {
        | Empty => Contains (s);
    }

    public Get () : string chord {
        | Contains => Empty (); s;
    }
}

def b = OnePlaceBuffer ();

// asyncブロックは非同期
async {
    // ノーウェイト
    for (mutable i = 0; i < 5; ++i)
        b.Put (i.ToString ());

    // 1秒待つ
    for (mutable i = 0; i < 5; ++i) {
        b.Put (i.ToString ());
        Thread.Sleep (1000);
    }
}

// 0.5秒待つ
for (mutable i = 0; i < 10; ++i) {
    Console.WriteLine ($"$(b.Get ()) : $(DateTime.Now)");
    Thread.Sleep (500);
}

/* 結果
0 : 2006/05/06 5:33:28
1 : 2006/05/06 5:33:28
2 : 2006/05/06 5:33:29
3 : 2006/05/06 5:33:29
4 : 2006/05/06 5:33:30
0 : 2006/05/06 5:33:30
1 : 2006/05/06 5:33:31
2 : 2006/05/06 5:33:32
3 : 2006/05/06 5:33:33
4 : 2006/05/06 5:33:34
 */

処理としてはまず、Putメソッドをノーウェイトで5回呼び出し、その後、1秒間隔で5回呼んでいます。一方、Getメソッドは0.5秒間隔で10回呼び出しています。結果の時間をみると分かりますが、まず、最初はPutよりもGetの方が遅いので、Getに合わせて0.5秒間隔で処理が行われ、その後、GetよりもPutの方が遅くなるので1秒間隔で処理が行われます。

単純な例なので面倒なだけに感じるかも知れませんが、処理が複雑になれば有り難みが増すと思います。多分。(^^;