非同期の例題として sleep sort は実によろしい。などという妄想の下に sleep sort を Nemerle で実装してみましょう。
Concurrency
Nemerle には Concurrency マクロなるマクロがあります[A]。
using Nemerle.Assertions; using Nemerle.Concurrency; using System.Threading.Thread; public class SleepSort { private weight : int; public this(weight : int) requires weight > 0 { this.weight = weight; } [ChordMember] public Put(value : int) : void requires value >= 0; public Get() : int chord { | Put => { Sleep(value * this.weight); value; } } }
ChordMember
なメソッド (Put
) は状態を表します, Get
メソッドは状態依存で呼び出され, Put
状態の時に Put
の引数 (value
) × weight
ミリ秒待ってからその値を返します。パターンマッチのような書き方ができるのが面白いですね。
Put
を呼び出さずに Get
を同期呼び出しすると, Put
を待つためにスレッドをロックするのでにっちもさっちもいかなくなります。使い方が難しいので,あまり public
にしない方が良いかもしれません。
これを使ってソートします。
using System; using Nemerle.Collections; using Nemerle.Concurrency; def data = { def r = Random(); $[1..10].Map(_ => r.Next(10, 30)); }; Console.WriteLine($"Sort $data"); def s = SleepSort(50); repeat (data.Length) { async { s.Get() |> Console.WriteLine; } } data.Iter(s.Put);
Put
が呼び出されたら Get
がロックを解放して Sleep
後に値を返します。 Sleep
の時間は Put
で与えた値が小さい程短いので,値が小さい順に返ってくる,つまりソートされた状態で値が返ってくるというわけですね。
値をコンソールに出力しているだけなので厳密にソートと呼べるかは疑問ですが,もしソートした結果をちゃんとキープしたい場合は F# でやったみたいに ConcurrentQueue
に値を突っ込んでいけば良いでしょう。今回は concurrency マクロを使いたかっただけなので省略します。次の Async 節ではきちんと ConcurrentQueue
を使ってソートしています。
Async
もう 1 つの解法です。 F# でやった方法と同じですが,少し注意する点があります。
using System; using System.Collections.Concurrent; using Nemerle.Collections; using Nemerle.ComputationExpressions; using Nemerle.ComputationExpressions.Async; using System.Threading.Thread; def data = { def r = Random(); $[1..10].Map(_ => r.Next(10, 30)); } Console.WriteLine($"Sort $data"); def queue = ConcurrentQueue(); def sleepWeight = 50; def enqueue(value) { Sleep(value * sleepWeight); queue.Enqueue(value); FakeVoid.Value; } data.AsyncMap(enqueue).WaitAll(); queue.Iter(Console.WriteLine);
20 行目の FakeVoid.Value
は FakeVoid
型のシングルトンインスタンスです。これを返しておかないと enqueue
が void
を返す値となってしまい AsyncMap
が使えません[B]。その AsyncMap
を使うことで非同期にリストの値を Sleep
してからキューに突っ込んでいきます。
これでうまくいくように見えるのですが,実はうまくいきません。というのは AsyncMap
は, ExecutionContext
引数を与えない場合では ThreadPool.QueueUserWorkItem
を利用しているからです。スレッドプールは同時実行数が調節されているので,リストの要素すべてに対して enqueue
が同時に実行されるとは限りません。そのため同時実行スレッド数が管理されないように,自身でスレッドを立ち上げて実行してやるような ExecutionContext
を用意する必要があります。なお ExecutionContext
クラスは System.Threading
名前空間にもありますが,ここでは Nemerle.ComputationExpressions.Async
名前空間のクラスです。
using Nemerle.DesignPatterns; using Nemerle.ComputationExpressions.Async; [Singleton(Public)] public class GreedyExecutionContext : ExecutionContext { private this() { } public override Execute(computation : void -> void) : void { System.Threading.Thread(computation).Start(); } }
Execute
メソッドをオーバーライドして,計算要求がきたら別スレッドで計算を行うようにします。なんとなくデザインパターンマクロを使ってみました。
これを用いて先ほどのコードを次のように修正します。
data.AsyncMap(enqueue, GreedyExecutionContext.Instance).WaitAll();
これで期待通りに動作するようになるはずです。
Async コンピューテーション式もあるのですが,今回は出番がありませんでした。 async
キーワードがかぶっているので concurrency マクロと一緒に使うことができません。残念です。
ソースコード
脚注
- このマクロは昔 Polyphonic C# という言語があって,そのモデルを Nemerle に取り入れたものだそうです。 [↩]
- 実際のところ
enqueue
が返す値はFakeVoid
である必要はありません。ですがenqueue
が本来void
を返すということを示す意味でFakeVoid.Value
を返すのは良いことだと思います。 [↩]