OITA: Oika's Information Technological Activities

@oika 情報技術的活動日誌。

LINQの遅延評価と排他制御

作ったきりロクに更新していないブログなわけですけども
SyntaxHighlighter Evolvedという、コードを表示するためのプラグインを入れてみたので*1
せっかくなので試してみるための記事を書きます。

最近C#のLinqというものが非常に便利なので
クラス間のインタフェースには配列とかよりも積極的にIEnumerableを使おう的な
流れがあるんじゃないかなと思ったりするんですけども、これが最初の頃はけっこうはまった。

たとえば状態を管理するクラスとかで下のような書き方をするとまずいのだ。

class StateClass {

    List<int> numList = new List<int>();

    /// <summary>
    /// リストから偶数の値を取得する
    /// </summary>
    /// <returns></returns>
    public IEnumerable<int> GetEvenNums() {
        lock (numList) {
            return numList.Where(p => p % 2 == 0);
        }
    }

    /// <summary>
    /// リストに値を追加する
    /// </summary>
    /// <param name="num"></param>
    public void AddNum(int num) {
        lock (numList) {
            numList.Add(num);
        }
    }
}

lockで排他しているつもりになっているんだけど、
「numList.Where(p => p % 2 == 0);」が評価されるのは実際に使われるときなので、
例えば以下のような使い方をするとInvalidOperationExceptionが投げられる。

    var state = new StateClass();

    //リスト更新処理
    Task.Factory.StartNew(() => {
        for (int i = 0; i < 10000; i++) {
            state.AddNum(i);
            Console.WriteLine("追加:" + i);
        }
    });
    //リスト読み取り処理
    Task.Factory.StartNew(() => {
        for (int i = 0; i < 10000; i++) {
            int cnt = state.GetEvenNums().Count();  //※
            Console.WriteLine("偶数件数:" + cnt);
        }
    });

GetEventNums()内に書いたWhere~のクエリが実際に走るのは
※の部分でCount()が実行されるタイミングだが、Count()はlockの外にある。
そのため、Count()実行中に別スレッドでAddが走ってリストが変更されるとエラーになるのである。

それでどうやって解決するかというと、手っ取り早いのはGetEvenNums()を以下のように直す方法。

public IEnumerable<int> GetEvenNums() {
    lock (numList) {
        return numList.Where(p => p % 2 == 0).ToArray(); //ToArrayを追加
    }
}

ToArray()で配列オブジェクト化することで、その場で式を評価させる。
でもこれをやると結局戻り値がIEnumerableである意味がないじゃんということになる。

あとは、Concurrentなリストを使う方法がある。

        ConcurrentBag<int> numList = new ConcurrentBag<int>();

        /// <summary>
        /// リストから偶数の値を取得する
        /// </summary>
        /// <returns></returns>
        public IEnumerable<int> GetEvenNums() {
            return numList.Where(p => p % 2 == 0);
        }

        /// <summary>
        /// リストに値を追加する
        /// </summary>
        /// <param name="num"></param>
        public void AddNum(int num) {
            numList.Add(num);
        }

この場合はlockすら必要ない。
ちなみに、ConcurrentBagは要素の順番を保証しない。
順番を気にする場合は、BlockingCollectionとかを使うことになるのかな。

個人的にはConcurrent系は処理が重いイメージがあって、あまり使っていない。
なので結局配列化しているんだけど、もうちょいスマートな設計がありそうな気もする。

*1:はてなブログに移ったので、現在はこのプラグインで表示しているわけではありません