6

列挙中に変更できるスレッドセーフなコレクションを作成したいと考えています。

サンプルActionSetクラスにはActionハンドラが格納されています。Addリストに新しいハンドラーを追加するメソッドInvokeと、収集されたすべてのアクション ハンドラーを列挙して呼び出すメソッドがあります。意図された作業シナリオには、列挙中に時折変更される非常に頻繁な列挙が含まれます。

Add列挙が終了していないときにメソッドを使用して変更すると、通常のコレクションは例外をスローします。

この問題には、簡単ではあるが時間のかかる解決策があります。列挙する前にコレクションを複製するだけです。

class ThreadSafeSlowActionSet {
    List<Action> _actions = new List<Action>();

    public void Add(Action action) {
        lock(_actions) {
            _actions.Add(action);
        }
    }

    public void Invoke() {
        lock(_actions) {
            List<Action> actionsClone = _actions.ToList();
        }
        foreach (var action in actionsClone ) {
            action();
        }
    }
}

このソリューションの問題は、列挙のオーバーヘッドであり、列挙を非常に高速にしたいと考えています。

列挙中でも新しい値を追加できる、かなり高速な「再帰セーフ」なコレクションを作成しました。メイン コレクションの列挙中に新しい値を追加すると、その値はメイン コレクションではなく_actions一時_deltaコレクションに追加されます。すべての列挙が終了すると、_delta値が_actionsコレクションに追加されます。_actionsメインコレクションの列挙 (コレクションの作成)中にいくつかの新しい値を追加し_delta、Invoke メソッドを再度入力する場合は、新しいマージされたコレクション ( _actions+ _delta) を作成し、それで置き換える_actions必要があります。

したがって、このコレクションは「再帰セーフ」に見えますが、スレッドセーフにしたいと考えています。Interlocked.*このコレクションをスレッド セーフにするには、コンストラクト、クラス フロム、およびその他の同期プリミティブを使用する必要があると思いますが、System.Threadingその方法がよくわかりません。

このコレクションをスレッドセーフにする方法は?

class RecursionSafeFastActionSet {
    List<Action> _actions = new List<Action>(); //The main store
    List<Action> _delta; //Temporary buffer for storing added values while the main store is being enumerated
    int _lock = 0; //The number of concurrent Invoke enumerations

    public void Add(Action action) {
        if (_lock == 0) { //_actions list is not being enumerated and can be modified
            _actions.Add(action);
        } else { //_actions list is being enumerated and cannot be modified
            if (_delta == null) {
                _delta = new List<Action>();
            }
            _delta.Add(action); //Storing the new values in the _delta buffer
        }
    }

    public void Invoke() {
        if (_delta != null) { //Re-entering Invoke after calling Add:  Invoke->Add,Invoke
            Debug.Assert(_lock > 0);
            var newActions = new List<Action>(_actions); //Creating a new list for merging delta
            newActions.AddRange(_delta); //Merging the delta
            _delta = null;
            _actions = newActions; //Replacing the original list (which is still being iterated)
        }
        _lock++;
        foreach (var action in _actions) {
            action();
        }
        _lock--;
        if (_lock == 0 && _delta != null) {
            _actions.AddRange(_delta); //Merging the delta
            _delta = null;
        }
    }
}

更新ThreadSafeSlowActionSet:バリアントを追加しました。

4

3 に答える 3

3

より簡単なアプローチ(たとえば、によって使用されるConcurrentBag)はGetEnumerator()、コレクションのコンテンツのスナップショットに対して列挙子を返すことです。あなたの場合、これは次のようになります。

public IEnumerator<Action> GetEnumerator()
{
    lock(sync)
    {
        return _actions.ToList().GetEnumerator();
    }
}

これを行う場合、_deltaフィールドとそれが追加する複雑さは必要ありません。

于 2012-12-17T13:07:32.180 に答える
1

スレッドセーフのために変更されたクラスは次のとおりです。

class SafeActionSet
{
    Object _sync = new Object();
    List<Action> _actions = new List<Action>(); //The main store
    List<Action> _delta = new List<Action>();   //Temporary buffer for storing added values while the main store is being enumerated
    int _lock = 0; //The number of concurrent Invoke enumerations

    public void Add(Action action)
    {
        lock(sync)
        {
            if (0 == _lock)
            { //_actions list is not being enumerated and can be modified
                _actions.Add(action);
            }
            else
            { //_actions list is being enumerated and cannot be modified
                _delta.Add(action); //Storing the new values in the _delta buffer
            }
        }
    }

    public void Invoke()
    {
        lock(sync)
        {
            if (0 < _delta.Count)
            { //Re-entering Invoke after calling Add:  Invoke->Add,Invoke
                Debug.Assert(0 < _lock);
                var newActions = new List<Action>(_actions); //Creating a new list for merging delta
                newActions.AddRange(_delta); //Merging the delta
                _delta.Clear();
                _actions = newActions; //Replacing the original list (which is still being iterated)
            }
            ++_lock;
        }
        foreach (var action in _actions)
        {
            action();
        }
        lock(sync)
        {
            --_lock;
            if ((0 == _lock) && (0 < _delta.Count))
            {
                _actions.AddRange(_delta); //Merging the delta
                _delta.Clear();
            }
        }
    }
}

次の理由により、他にもいくつかの調整を行いました。

  • 最初に定数値を持つように IF 式を逆にしたため、タイプミスをして「==」や「!=」などの代わりに「=」を入力すると、コンパイラはすぐにタイプミスを教えてくれます。(: 脳と指が同期していないことが多いため、私が身に付けた習慣 :)
  • _delta を事前に割り当て、 null に設定する代わりに.Clear()を呼び出しました。これは、読みやすいためです。
  • さまざまなlock(_sync) {...}により、すべてのインスタンス変数へのアクセスでスレッド セーフが提供されます。:( 列挙自体の _action へのアクセスを除いて。):
于 2012-11-07T05:47:59.947 に答える
0

実際にはコレクションからアイテムを削除する必要もあったため、最終的に使用した実装は、書き換えられた LinkedList に基づいていました。削除/挿入時に隣接するノードをロックし、列挙中にコレクションが変更されても文句を言いません。Dictionaryまた、要素検索を高速化するために を追加しました。

于 2012-12-17T17:08:05.647 に答える