LoginSignup
16
15

More than 5 years have passed since last update.

Concurrent CollectionのGetEnumeratorメソッドの挙動に関して

Last updated at Posted at 2013-04-18

このトピックのさまりー

System.Collections.Concurrent名前空間には、スレッドセーフなStack,QueueそしてDictinaryがあります。これらのコレクションクラスはSystem.Collections.Generic名前空間に存在するStack,QueueそしてDictionaryとは異なり、GetEnumerator()メソッドでEnumeratorを取得した後、元となるコレクションに対して変更を加えたとしても、InvalidOperationExceptionが発生すること無く列挙操作を継続することが可能です。

但し、Enumerator取得後の変更がどのように取り扱われるかという点において挙動が違うのでそれをまとめてみました。

ConcurrentStackとConcurrentQueueの場合

この2つのコレクションクラスは、GetEnumeratorメソッドを呼び出した時点におけるコレクションのスナップショットを構築した上で、そのスナップショットに対する列挙操作を行います。

The enumeration represents a moment-in-time snapshot of the contents of the stack.
ConcurrentStack.GetEnumerator Method

The enumeration represents a moment-in-time snapshot of the contents of the queue.
ConcurrentQueue.GetEnumerator Method

以下にConcurrentQueue及びConcurrentStackの挙動を示す、コードとそのアウトプットを示します。

ConcurrentStackの検証コード

ConcurrentStack.cs
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;

namespace ConcurrentStackTest
{
    class Program
    {
        private static void Main(string[] args)
        {
            ConcurrentStack<int> concurrentStack = new ConcurrentStack<int>();

            for (int i = 0; i < 10; i++) concurrentStack.Push(i);

            IEnumerator<int> enumerator = concurrentStack.GetEnumerator();

            int dummy;
            concurrentStack.TryPop(out dummy);
            concurrentStack.Push(42);

            while (enumerator.MoveNext())
            {
                Console.WriteLine(enumerator.Current);
            }

        }
    }
}

実行結果

9
8
7
6
5
4
3
2
1
0

ConcurrentQueueの検証コード

ConcurrentQueueTest.cs
using System;
using System.Collections.Concurrent;

namespace ConcurrentQueueTest
{
    class Program
    {
        private static void Main(string[] args)
        {
            ConcurrentQueue<int> concurrentQueue = new ConcurrentQueue<int>();

            for (int i = 0; i < 10; i++) concurrentQueue.Enqueue(i);

            var enumerator = concurrentQueue.GetEnumerator();

            int dummy;
            concurrentQueue.TryDequeue(out dummy);

            concurrentQueue.Enqueue(42);

            while (enumerator.MoveNext())
            {
                Console.WriteLine(enumerator.Current);
            }
        }
    }
}

実行結果

0
1
2
3
4
5
6
7
8
9

以上のように、ConcurrentStack及びConcurrentQueueは、GetEnumeratorメソッドによってコレクションを反復する列挙子を取得以降の変更が反映されないことがおわかりいただけるかと思います。

ConcurrentDictionaryの場合

先に述べた、ConcurrentStack/Queueとは違って、ConcurrentDictionaryの場合は、少々様相が異なり、GetEnumeratorメソッドにより、反復する列挙子を取得後の変更も反映されることがあり、
コレクションの状態によっては場合によってはスナップショットのような挙動を取り得ることも有り、反復する列挙子を取得後に元となるConcurrentDictionaryに変更を加えたとしても、InvalidOperationExceptionが発生すること無く列挙操作を実行可能ですが、実行可能であるという保証があるだけで、列挙内容が元となるコレクションを完全に反映しているか否か又は、取得時点のスナップショットであるか否かの保証は無いので、注意が必要です。

The enumerator returned from the dictionary is safe to use concurrently with reads and writes to the dictionary, however it does not represent a moment-in-time snapshot of the dictionary.
ConcurrentDictionary.GetEnumerator Method

以下に、検証コードを示します。

取得後の変更が反映される検証用コード

ConcurrentDictionaryA.cs
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace ConcurrentDictionaryTest
{
    class Program
    {
        private static void Main(string[] args)
        {
            ConcurrentDictionary<int, string> concurrentDictionary = new ConcurrentDictionary<int, string>();

            for (int i = 0; i < 10; i++)
                concurrentDictionary.AddOrUpdate(i, x => x.ToString("000"), (x, y) => "Already existed.");

            IEnumerator<KeyValuePair<int, string>> enumerator = concurrentDictionary.GetEnumerator();

            concurrentDictionary.TryUpdate(5, "hogehoge", "005");

            while (enumerator.MoveNext()) Console.WriteLine(enumerator.Current.Value);
        }
    }
}

実行結果

000
001
002
003
004
hogehoge
006
007
008
009

このように、先のStack/Queueとは異なり、反復する列挙子を取得以降に行われた変更が反映されます。

スナップショット的な挙動が発生する検証用コード

ConcurrentDictonaryB.cs

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ConcurrentDictionaryTest
{
    class Program
    {
        private static void Main(string[] args)
        {
            ConcurrentDictionary<int, string> concurrentDictionary = new ConcurrentDictionary<int, string>();

            for (int i = 0; i < 100; i++)
                concurrentDictionary.AddOrUpdate(i, x => x.ToString("000"), (x, y) => "Already existed.");

            IEnumerator<KeyValuePair<int, string>> enumerator = concurrentDictionary.GetEnumerator();

            Action clearAction = () =>
                                     {
                                         System.Threading.Thread.Sleep(0);
                                         concurrentDictionary.Clear();
                                         Console.WriteLine("Cleared!");
                                     };

            Task ret = Task.Run(clearAction);

            while (enumerator.MoveNext()) Console.WriteLine(enumerator.Current.Value);

            ret.Wait();
            ret.Dispose();
        }
    }
}

実行結果

000
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
Cleared!
023
024
...(省略)
097
098
099

このように、元になるコレクションに対して、Clearメソッドを呼び出して要素を全てクリアしても、あたかもスナップショットを取得したかのような振る舞いをします。

まとめ

ConcurrentStack及びConcurrentQueueはGetEnumeratorメソッドの呼び出しによって反復する列挙子を取得後に取得元となるコレクションに変更を加えても、安全に列挙操作を行うことが可能で有り、
加えて、取得された反復する列挙子の列挙結果にその変更が反映されません。
他方ConcurrentDictionaryに関しては、列挙操作を安全に行うことは可能ですが、列挙結果がどのようなものであるかは未定義であるといえると思います。

16
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
15