c# - 多线程和构造函数完整性

标签 c# multithreading constructor

有一天,我偶然发现了一个 NullReferenceException,我认为这是完全不可能的。我有一个包含字典的小类,该字典在构造函数中实例化:

public MyClass
{
    protected readonly Dictionary<string, ISomething> _MyList;
    public MyClass()
    {
        _MyList = new List<ISomething>();
    }
    public void Add(string name, ISomething something)
    {
        _MyList.Add(name, something);
    }
}

难以置信,但却是事实,异常发生在 _MyList.Add 处。当我在调试器中运行它时,它显示 _MyList 已经包含一项,但异常仍然发生在另一个线程中。注意:没有代码将 _MyList 设置为 nullprotected 仅适用于某些测试设置。

MyClass 在启动时实例化。配置引擎实例化许多对象并将它们连接起来。

MyClass myClass = new MyClass();
ISomething some1 = new ...();
some1.Inject(myClass);
ISomething some2 = new ...();
some2.Inject(myClass);

稍后,在顶级对象上调用 Start(),该对象在多个线程中向下传播。其中,some1some2 通过调用 AddmyClass 注册。虽然 some1 在同一个线程中创建所有这些对象,但 some2 在不同的线程中执行它。正是 some2Add 的调用导致了 NullReferenceException(日志文件显示线程 ID)。

我的印象是存在一些线程问题。不知何故,在多核机器上,不同线程(核心)有不同的 myClass “副本”,一个是完全构造的,另一个不是。

我可以通过用 ConcurrentDictionary 替换 Dictionary 来解决这个问题。

我想更好地了解:

  • NullReferenceException 是如何发生的,以及

  • ConcurrentDictionary 如何解决此问题

编辑: 我的第一印象 - _MyList 为空 - 可能是错误的。相反,异常发生在字典内部:例如,它包含两个数组 bucketsentries,它们在第一次调用 Add 时初始化。在这里,发生了竞争,一个线程开始初始化,而另一个线程则假设初始化已完成。

因此,我最初关于某些寄存器缓存导致多线程环境中的问题以及 ConcurrentDictionary 的某些“魔法”以某种方式隐含地“ volatile ”的结论是错误的 - 我的问题的第二部分不再具有任何意义。

最佳答案

how the NullReferenceException could happen

字典不知道其中将存储多少数据,因此当您添加项目时,它必须自行调整大小。此调整大小操作需要时间,如果两个线程都检测到需要额外的空间,它们将尝试同时执行调整大小。这种竞争条件的“失败者”将把他的数据写入到被“获胜者”覆盖的数据副本中,这可能会导致失败者写入的位置看起来像内部数组中的有效位置,但实际上它保存未初始化的数据。当您尝试访问此未初始化的数据时,它会抛出NullReferenceException。 (注意:此调整大小竞争只是多个线程尝试同时写入字典时可能发生的众多竞争条件之一)

how ConcurrentDictionary resolves this issue

任何可能导致竞争条件的操作都会在内部进行检查,以确定线程是否较松。如果是,它会丢弃所做的工作并重新尝试再次插入数据,直到成功为止。一旦成功,函数调用就会返回。

这是一份副本 from the reference source该检查的逻辑。

        /// <summary>
        /// Shared internal implementation for inserts and updates.
        /// If key exists, we always return false; and if updateIfExists == true we force update with value;
        /// If key doesn't exist, we always add value and return true;
        /// </summary>
        [SuppressMessage("Microsoft.Concurrency", "CA8001", Justification = "Reviewed for thread safety")]
        private bool TryAddInternal(TKey key, TValue value, bool updateIfExists, bool acquireLock, out TValue resultingValue)
        {
            while (true)
            {
                int bucketNo, lockNo;
                int hashcode;

                Tables tables = m_tables;
                IEqualityComparer<TKey> comparer = tables.m_comparer;
                hashcode = comparer.GetHashCode(key);
                GetBucketAndLockNo(hashcode, out bucketNo, out lockNo, tables.m_buckets.Length, tables.m_locks.Length);

                bool resizeDesired = false;
                bool lockTaken = false;
#if FEATURE_RANDOMIZED_STRING_HASHING
#if !FEATURE_CORECLR                
                bool resizeDueToCollisions = false;
#endif // !FEATURE_CORECLR
#endif

                try
                {
                    if (acquireLock)
                        Monitor.Enter(tables.m_locks[lockNo], ref lockTaken);

                    // If the table just got resized, we may not be holding the right lock, and must retry.
                    // This should be a rare occurence.
                    if (tables != m_tables)
                    {
                        continue;
                    }

#if FEATURE_RANDOMIZED_STRING_HASHING
#if !FEATURE_CORECLR
                    int collisionCount = 0;
#endif // !FEATURE_CORECLR
#endif

                    // Try to find this key in the bucket
                    Node prev = null;
                    for (Node node = tables.m_buckets[bucketNo]; node != null; node = node.m_next)
                    {
                        Assert((prev == null && node == tables.m_buckets[bucketNo]) || prev.m_next == node);
                        if (comparer.Equals(node.m_key, key))
                        {
                            // The key was found in the dictionary. If updates are allowed, update the value for that key.
                            // We need to create a new node for the update, in order to support TValue types that cannot
                            // be written atomically, since lock-free reads may be happening concurrently.
                            if (updateIfExists)
                            {
                                if (s_isValueWriteAtomic)
                                {
                                    node.m_value = value;
                                }
                                else
                                {
                                    Node newNode = new Node(node.m_key, value, hashcode, node.m_next);
                                    if (prev == null)
                                    {
                                        tables.m_buckets[bucketNo] = newNode;
                                    }
                                    else
                                    {
                                        prev.m_next = newNode;
                                    }
                                }
                                resultingValue = value;
                            }
                            else
                            {
                                resultingValue = node.m_value;
                            }
                            return false;
                        }
                        prev = node;

#if FEATURE_RANDOMIZED_STRING_HASHING
#if !FEATURE_CORECLR
                        collisionCount++;
#endif // !FEATURE_CORECLR
#endif
                    }

#if FEATURE_RANDOMIZED_STRING_HASHING
#if !FEATURE_CORECLR
                    if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer)) 
                    {
                        resizeDesired = true;
                        resizeDueToCollisions = true;
                    }
#endif // !FEATURE_CORECLR
#endif

                    // The key was not found in the bucket. Insert the key-value pair.
                    Volatile.Write<Node>(ref tables.m_buckets[bucketNo], new Node(key, value, hashcode, tables.m_buckets[bucketNo]));
                    checked
                    {
                        tables.m_countPerLock[lockNo]++;
                    }

                    //
                    // If the number of elements guarded by this lock has exceeded the budget, resize the bucket table.
                    // It is also possible that GrowTable will increase the budget but won't resize the bucket table.
                    // That happens if the bucket table is found to be poorly utilized due to a bad hash function.
                    //
                    if (tables.m_countPerLock[lockNo] > m_budget)
                    {
                        resizeDesired = true;
                    }
                }
                finally
                {
                    if (lockTaken)
                        Monitor.Exit(tables.m_locks[lockNo]);
                }

                //
                // The fact that we got here means that we just performed an insertion. If necessary, we will grow the table.
                //
                // Concurrency notes:
                // - Notice that we are not holding any locks at when calling GrowTable. This is necessary to prevent deadlocks.
                // - As a result, it is possible that GrowTable will be called unnecessarily. But, GrowTable will obtain lock 0
                //   and then verify that the table we passed to it as the argument is still the current table.
                //
                if (resizeDesired)
                {
#if FEATURE_RANDOMIZED_STRING_HASHING
#if !FEATURE_CORECLR
                    if (resizeDueToCollisions)
                    {
                        GrowTable(tables, (IEqualityComparer<TKey>)HashHelpers.GetRandomizedEqualityComparer(comparer), true, m_keyRehashCount);
                    }
                    else
#endif // !FEATURE_CORECLR
                    {
                        GrowTable(tables, tables.m_comparer, false, m_keyRehashCount);
                    }
#else
                    GrowTable(tables, tables.m_comparer, false, m_keyRehashCount);
#endif
                }

                resultingValue = value;
                return true;
            }
        }

相比之下,这里是 normal dictionary's version 的代码具有相同的功能。

        private void Insert(TKey key, TValue value, bool add) {

            if( key == null ) {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
            }

            if (buckets == null) Initialize(0);
            int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
            int targetBucket = hashCode % buckets.Length;

#if FEATURE_RANDOMIZED_STRING_HASHING
            int collisionCount = 0;
#endif

            for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next) {
                if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
                    if (add) { 
                        ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
                    }
                    entries[i].value = value;
                    version++;
                    return;
                } 

#if FEATURE_RANDOMIZED_STRING_HASHING
                collisionCount++;
#endif
            }
            int index;
            if (freeCount > 0) {
                index = freeList;
                freeList = entries[index].next;
                freeCount--;
            }
            else {
                if (count == entries.Length)
                {
                    Resize();
                    targetBucket = hashCode % buckets.Length;
                }
                index = count;
                count++;
            }

            entries[index].hashCode = hashCode;
            entries[index].next = buckets[targetBucket];
            entries[index].key = key;
            entries[index].value = value;
            buckets[targetBucket] = index;
            version++;

#if FEATURE_RANDOMIZED_STRING_HASHING

#if FEATURE_CORECLR
            // In case we hit the collision threshold we'll need to switch to the comparer which is using randomized string hashing
            // in this case will be EqualityComparer<string>.Default.
            // Note, randomized string hashing is turned on by default on coreclr so EqualityComparer<string>.Default will 
            // be using randomized string hashing

            if (collisionCount > HashHelpers.HashCollisionThreshold && comparer == NonRandomizedStringEqualityComparer.Default) 
            {
                comparer = (IEqualityComparer<TKey>) EqualityComparer<string>.Default;
                Resize(entries.Length, true);
            }
#else
            if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer)) 
            {
                comparer = (IEqualityComparer<TKey>) HashHelpers.GetRandomizedEqualityComparer(comparer);
                Resize(entries.Length, true);
            }
#endif // FEATURE_CORECLR

#endif

        }

关于c# - 多线程和构造函数完整性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42788514/

相关文章:

java - 为什么在泛型类(Java)的构造函数中提供类型参数是错误的?

c# - EF Code First 外键必须映射到一些在概念方面参与外键关联的 AssociationSet 或 EntitySet

c# - 如何验证文本框?

c# - WNetAddConnection2 返回错误 1200 - 本地名称有效

c# - 如何在C#中实现八叉树?

构造函数的 JavaScript prototype.constructor 属性不在规范中?

ruby-on-rails - Ruby 中的线程会提高并发性吗?

c++ - 将 cout 重定向到每个线程的文件以管理线程安全日志记录

java - Android CountDownTimer 类滞后于主线程

Java构造函数最终变量赋值