Skip to content

使用 Dictionary 时的 System.InvalidOperationException

🏷️ C#

最近写了一个使用静态 Dictionary 实例的方法,但有时会出现 System.InvalidOperationException 异常,而且一旦出了一次,之后所有的访问都会报这个异常。

System.InvalidOperationException: Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.

代码大概是如下这样的。

csharp
if (!_dic.ContainsKey(val))
{
    _dic.Add(val, val);
}

问题就出在这个 Dictionary 是静态的,会导致并发访问时报错。

最后写了段代码来模拟一下,确实会出现该异常,而且还不止这一种异常。

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

namespace NonConcurrentCollections
{
    class Program
    {
        static Dictionary<string, string> _dic = new Dictionary<string, string>();

        static void Main(string[] args)
        {
            int dicCount = 100;
            bool isFirstNullReferenceException = false;
            bool isFirstInvalidOperationException = false;
            bool isFirstIndexOutOfRangeException = false;
            bool isFirstArgumentException = false;
            bool isFirstOtherException = false;
            for (int i = 0; i < 10; i++)
            {
                Task.Run(() => {
                    Random rd = new Random();
                    while (true)
                    {
                        var val = rd.Next(dicCount).ToString();
                        try
                        {
                            if (!_dic.ContainsKey(val))
                            {
                                _dic.Add(val, val);
                            }
                            if (_dic.Count == dicCount)
                            {
                                _dic.Clear();
                            }
                        }
                        catch (NullReferenceException ex)
                        {
                            if (!isFirstNullReferenceException)
                            {
                                isFirstNullReferenceException = true;
                                Console.WriteLine(ex.ToString());
                            }
                        }
                        catch (InvalidOperationException ex)
                        {
                            if (!isFirstInvalidOperationException)
                            {
                                isFirstInvalidOperationException = true;
                                Console.WriteLine(ex.ToString());
                            }
                            _dic = new Dictionary<string, string>();
                        }
                        catch (IndexOutOfRangeException ex)
                        {
                            if (!isFirstIndexOutOfRangeException)
                            {
                                isFirstIndexOutOfRangeException = true;
                                Console.WriteLine(ex.ToString());
                            }
                        }
                        catch (ArgumentException ex)
                        {
                            if (!isFirstArgumentException)
                            {
                                isFirstArgumentException = true;
                                Console.WriteLine(ex.ToString());
                            }
                        }
                        catch (Exception ex)
                        {
                            if (!isFirstOtherException)
                            {
                                isFirstOtherException = true;
                                Console.WriteLine(ex.ToString());
                            }
                        }

                    }
                });
            }

            Console.ReadLine();
        }
    }
}

打印的异常消息如下,出人意料的是总共可能触发 4 种异常类型: NullReferenceExceptionArgumentExceptionInvalidOperationExceptionIndexOutOfRangeException
其中 ArgumentException 还能

csharp
System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
   at NonConcurrentCollections.Program.<>c__DisplayClass1_0.<Main>b__0() in C:\Users\liujiajia\source\repos\NonConcurrentCollections\NonConcurrentCollections\Program.cs:line 30

System.ArgumentException: An item with the same key has already been added. Key: 20
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
   at NonConcurrentCollections.Program.<>c__DisplayClass1_0.<Main>b__0() in C:\Users\liujiajia\source\repos\NonConcurrentCollections\NonConcurrentCollections\Program.cs:line 30

System.InvalidOperationException: Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.
   at System.Collections.Generic.Dictionary`2.FindEntry(TKey key)
   at System.Collections.Generic.Dictionary`2.ContainsKey(TKey key)
   at NonConcurrentCollections.Program.<>c__DisplayClass1_0.<Main>b__0() in C:\Users\liujiajia\source\repos\NonConcurrentCollections\NonConcurrentCollections\Program.cs:line 28

System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
   at NonConcurrentCollections.Program.<>c__DisplayClass1_0.<Main>b__0() in C:\Users\liujiajia\source\repos\NonConcurrentCollections\NonConcurrentCollections\Program.cs:line 30

另外还有较小的几率出现不一样的 NullReferenceExceptionArgumentExceptionInvalidOperationException 异常:

csharp
System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Collections.Generic.Dictionary`2.FindEntry(TKey key)
   at System.Collections.Generic.Dictionary`2.ContainsKey(TKey key)
   at NonConcurrentCollections.Program.<>c__DisplayClass1_0.<Main>b__0() in C:\Users\liujiajia\source\repos\NonConcurrentCollections\NonConcurrentCollections\Program.cs:line 28

System.ArgumentException: Destination array was not long enough. Check the destination index, length, and the array's lower bounds. (Parameter 'destinationArray')
   at System.Array.Copy(Array sourceArray, Int32 sourceIndex, Array destinationArray, Int32 destinationIndex, Int32 length, Boolean reliable)
   at System.Collections.Generic.Dictionary`2.Resize(Int32 newSize, Boolean forceNewHashCodes)
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
   at NonConcurrentCollections.Program.<>c__DisplayClass1_0.<Main>b__0() in C:\Users\liujiajia\source\repos\NonConcurrentCollections\NonConcurrentCollections\Program.cs:line 30

System.InvalidOperationException: Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
   at NonConcurrentCollections.Program.<>c__DisplayClass1_0.<Main>b__0() in C:\Users\liujiajia\source\repos\NonConcurrentCollections\NonConcurrentCollections\Program.cs:line 30

从上可以看出 NullReferenceExceptionInvalidOperationException 异常在调用 ContainsKeyAdd 方法时都有可能发生,ArgumentException 则是在 Add 时发生了两种异常。

下面是之前曾修改过的一版,加了 lock 处理。这种应该是比较常规的写法,但当时没有意识到 ContainsKey 方法也会发生 InvalidOperationException 异常。

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

namespace NonConcurrentCollections
{
    class ProgramWithLock
    {
        static Dictionary<string, string> _dic = new Dictionary<string, string>();
        static object _dic_obj = new object();

        static void Main(string[] args)
        {
            int dicCount = 100;
            bool isFirstNullReferenceException = false;
            bool isFirstInvalidOperationException = false;
            bool isFirstIndexOutOfRangeException = false;
            bool isFirstOtherException = false;
            for (int i = 0; i < 10; i++)
            {
                Task.Run(() => {
                    Random rd = new Random();
                    while(true) { 
                        var val = rd.Next(dicCount).ToString();
                        try
                        {
                            if (!_dic.ContainsKey(val))
                            {
                                lock (_dic_obj)
                                {
                                    try
                                    {
                                        if (!_dic.ContainsKey(val))
                                        {
                                            _dic.Add(val, val);
                                        }
                                        if (_dic.Count == dicCount)
                                        {
                                            _dic.Clear();
                                        }

                                    }
                                    catch (NullReferenceException ex)
                                    {
                                        if (!isFirstNullReferenceException)
                                        {
                                            isFirstNullReferenceException = true;
                                            Console.WriteLine($"InnerException: {ex.ToString()}");
                                        }
                                    }
                                    catch (InvalidOperationException ex)
                                    {
                                        if (!isFirstInvalidOperationException)
                                        {
                                            isFirstInvalidOperationException = true;
                                            Console.WriteLine($"InnerException: {ex.ToString()}");
                                        }
                                        _dic = new Dictionary<string, string>();
                                    }
                                    catch (IndexOutOfRangeException ex)
                                    {
                                        if (!isFirstIndexOutOfRangeException)
                                        {
                                            isFirstIndexOutOfRangeException = true;
                                            Console.WriteLine($"InnerException: {ex.ToString()}");
                                        }
                                    }
                                    catch (Exception ex)
                                    {
                                        if (!isFirstOtherException)
                                        {
                                            isFirstOtherException = true;
                                            Console.WriteLine($"InnerException: {ex.ToString()}");
                                        }
                                    }
                                }
                            }
                        }
                        catch (NullReferenceException ex)
                        {
                            if (!isFirstNullReferenceException)
                            {
                                isFirstNullReferenceException = true;
                                Console.WriteLine($"OuterException: {ex.ToString()}");
                            }
                        }
                        catch (InvalidOperationException ex)
                        {
                            if (!isFirstInvalidOperationException)
                            {
                                isFirstInvalidOperationException = true;
                                Console.WriteLine($"OuterException: {ex.ToString()}");
                            }
                            _dic = new Dictionary<string, string>();
                        }
                        catch (IndexOutOfRangeException ex)
                        {
                            if (!isFirstIndexOutOfRangeException)
                            {
                                isFirstIndexOutOfRangeException = true;
                                Console.WriteLine($"OuterException: {ex.ToString()}");
                            }
                        }
                        catch (Exception ex)
                        {
                            if (!isFirstOtherException)
                            {
                                isFirstOtherException = true;
                                Console.WriteLine($"OuterException: {ex.ToString()}");
                            }
                        }

                    }
                });
            }

            Console.ReadLine();
        }
    }
}

运行结果如下,仍有可能出现 InvalidOperationExceptionNullReferenceException 异常,但是在 lock 的作用域内,由于最多只会有一个线程访问,所以不会出现这些错误。

csharp
OuterException: System.InvalidOperationException: Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.
   at System.Collections.Generic.Dictionary`2.FindEntry(TKey key)
   at System.Collections.Generic.Dictionary`2.ContainsKey(TKey key)
   at NonConcurrentCollections.ProgramWithLock.<>c__DisplayClass2_0.<Main>b__0()
 in C:\Users\liujiajia\source\repos\NonConcurrentCollections\NonConcurrentCollections\ProgramWithLock.cs:line 27

OuterException: System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Collections.Generic.Dictionary`2.FindEntry(TKey key)
   at System.Collections.Generic.Dictionary`2.ContainsKey(TKey key)
   at NonConcurrentCollections.ProgramWithLock.<>c__DisplayClass2_0.<Main>b__0()
 in C:\Users\liujiajia\source\repos\NonConcurrentCollections\NonConcurrentCollections\ProgramWithLock.cs:line 27

综上, Dictionary 是非线程安全的,在多线程的处理中,应避免使用 Dictionary 类型来共享数据。如一定要使用,则必须使用 lock 关键字来避免多个线程同时访问。