4

私はいくつかの同期構造をテストしていましたが、混乱することに気付きました。同時に書き込み中にコレクションを列挙していると、例外がスローされましたが (これは予期されていました)、for ループを使用してコレクションをループすると、そうではありませんでした。誰かがこれを説明できますか?リストでは、リーダーとライターが同時に操作することはできないと思いました。コレクションをループすると、列挙子を使用した場合と同じ動作を示すと予想していました。

更新: これは純粋に学術的な演習です。リストが同時に書き込まれている場合、リストを列挙するのは悪いことだと理解しています。また、同期構造が必要であることも理解しています。私の質問は、操作 1 では期待どおりに例外がスローされるのに、もう 1 つは例外がスローされない理由についてです。

コードは以下のとおりです。

   class Program
   {
    private static List<string> _collection = new List<string>();
    static void Main(string[] args)
    {
        ThreadPool.QueueUserWorkItem(new WaitCallback(AddItems), null);
        System.Threading.Thread.Sleep(5000);
        ThreadPool.QueueUserWorkItem(new WaitCallback(DisplayItems), null);
        Console.ReadLine();
    }

    public static void AddItems(object state_)
    {
        for (int i = 1; i <= 50; i++)
        {
            _collection.Add(i.ToString());
            Console.WriteLine("Adding " + i);
            System.Threading.Thread.Sleep(150);
        }
    }

    public static void DisplayItems(object state_)
    {
        // This will not throw an exception
        //for (int i = 0; i < _collection.Count; i++)
        //{
        //    Console.WriteLine("Reading " + _collection[i]);
        //    System.Threading.Thread.Sleep(150);
        //}

        // This will throw an exception
        List<string>.Enumerator enumerator = _collection.GetEnumerator();
        while (enumerator.MoveNext())
        {
            string value = enumerator.Current;
            System.Threading.Thread.Sleep(150);
            Console.WriteLine("Reading " + value);
        }
    }
}
4

7 に答える 7

15

列挙中にコレクションを変更することはできません。そのルールは、スレッドの問題を考慮しなくても存在します。MSDNから:

コレクションが変更されない限り、列挙子は有効なままです。要素の追加、変更、または削除などの変更がコレクションに加えられた場合、列挙子は回復できないほど無効になり、その動作は未定義になります。

整数ベースの for ループは、実際には列挙子ではありません。ほとんどのシナリオでは、同じことを達成します。ただし、IEnumerator のインターフェイスは、コレクション全体を反復処理できることを保証します。コレクションが変更された後に MoveNext への呼び出しが発生した場合、プラットフォームは例外をスローすることにより、これを内部的に強制します。この例外は、列挙子オブジェクトによってスローされます。

整数ベースの for ループは、数値のリストのみを処理します。コレクションを整数でインデックス付けすると、その位置にあるアイテムを取得するだけです。リストに何かが挿入または削除された場合、項目をスキップするか、同じ項目を 2 回実行することができます。これは、トラバース中にコレクションを変更する必要がある特定の状況で役立ちます。for ループには、IEnumerator コントラクトを保証する列挙子オブジェクトがないため、例外はスローされません。

于 2009-04-11T01:54:00.880 に答える
2

実際の質問に答えるには...

列挙すると、要求したときのリストの状態にバインドされた IEnumerator が取得されます。以降の操作は、列挙子 (MoveNext、Current) で実行されます。

for ループを使用する場合、インデックスによって特定のアイテムを取得するために if を呼び出すと、シーケンスが作成されます。ループに入っていることを認識している列挙子などの外部コンテキストはありません。コレクションが知っている限りでは、あなたが求めているのは 1 つのアイテムだけです。コレクションは列挙子を渡さなかったので、アイテム 0、アイテム 1、アイテム 2 などを要求している理由が、リストを歩いているためであることを知る方法はありません。

リストをたどるのと同時にリストをいじっていると、どちらにしてもエラーが発生します。項目を追加する場合、for ループはサイレントにスキップする可能性がありますが、foreach ループはスローします。アイテムを削除する場合、運が悪いと for ループが範囲外のインデックスをスローする可能性がありますが、ほとんどの場合は機能します。

しかし、あなたはそれをすべて理解していると思います。あなたの質問は、単純に、反復の 2 つの方法が異なる動作をする理由でした。その答えは、ある場合には GetEnumerator を呼び出し、別の場合には get_Item を呼び出すと、コレクションの状態が (コレクションに) 知られているということです。

于 2009-04-11T04:46:40.950 に答える
1

リストが変更されると、列挙子は無効になります。リストを列挙しているときにリストを変更する場合は、戦略を少し考え直す必要があります。

表示機能を開始するときに新しい列挙子を取得し、これが行われている間はリストをロックします。または、List のディープ コピーを新しい _displayCollection List に作成し、この別のコレクションを列挙します。これは、表示プロセスが開始される前に埋められることを除いて書き込まれません。お役に立てれば。

于 2009-04-11T01:41:04.123 に答える
1

違いは、「コレクションをループしている」と言うときは、実際にはコレクションをループしているのではなく、1 から 50 までの整数を反復処理し、それらのインデックスでコレクションに追加していることです。これは、1 から 50 までの数値がまだ存在するという事実には影響しません。

リストを列挙するときは、インデックスではなくアイテムを列挙しています。したがって、列挙中にアイテムを追加すると、列挙が無効になります。インデックス 6 にアイテムを挿入すると同時にリストのアイテム 6 に列挙する可能性がある場合、古いアイテムまたは新しいアイテムを列挙する可能性がある場合、またはいくつかの場合を防ぐために、そのように構築されています。未定義状態。

これを行いたい場合は「スレッドセーフ」リストを探しますが、同時に読み取りと書き込みの不正確さに対処する準備をしてください:)

于 2009-04-11T01:52:06.577 に答える
1

リストには内部バージョン カウンターがあり、リストの内容を変更すると更新されます。列挙子はバージョンを追跡し、リストが変更されたことを確認すると例外をスローします。

リストをループしているだけの場合、バージョンを追跡するものは何もないため、リストが変更されたことをキャッチするものは何もありません。

ループ中にリストを変更すると、不要な影響が発生する可能性がありますが、これは列挙子によって保護されます。たとえば、ループ インデックスを変更せずにリストから項目を削除して、同じ項目を指すようにすると、ループ内の項目を見逃す可能性があります。同様に、インデックスを修正せずにアイテムを挿入すると、同じアイテムを複数回反復する可能性があります。

于 2009-04-11T01:58:52.863 に答える
0

コレクションを列挙中にコレクションを変更することはできません。

問題は、コレクションがいっぱいでないときに列挙を開始し、列挙中にアイテムを追加し続けようとすることです

于 2009-04-11T01:45:32.617 に答える
-1

このコードには欠陥があり、5 秒間眠っているのにすべての項目がリストに追加されていません。これは、最初のスレッドがリストへのアイテムの追加を完了する前に、あるスレッドでアイテムの表示を開始することを意味します。これにより、基になるコレクションが変更され、列挙子が無効になります。

Add コードから Thread.Sleep を削除すると、次のことが強調されます。

public static void AddItems(object state_)
{     
   for (int i = 1; i <= 50; i++)      
   {        
       _collection.Add(i.ToString());      
       Console.WriteLine("Adding " + i);  
   }   
} 

スリープするのではなく、最初のスレッドがアイテムを追加する作業を完了するまで待機する同期メカニズムを使用する必要があります。

于 2009-04-11T01:44:10.530 に答える