有一天,我偶然发现了一个 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
设置为 null
。 protected
仅适用于某些测试设置。
MyClass
在启动时实例化。配置引擎实例化许多对象并将它们连接起来。
MyClass myClass = new MyClass();
ISomething some1 = new ...();
some1.Inject(myClass);
ISomething some2 = new ...();
some2.Inject(myClass);
稍后,在顶级对象上调用 Start()
,该对象在多个线程中向下传播。其中,some1
和 some2
通过调用 Add
向 myClass
注册。虽然 some1
在同一个线程中创建所有这些对象,但 some2
在不同的线程中执行它。正是 some2
对 Add
的调用导致了 NullReferenceException
(日志文件显示线程 ID)。
我的印象是存在一些线程问题。不知何故,在多核机器上,不同线程(核心)有不同的 myClass
“副本”,一个是完全构造的,另一个不是。
我可以通过用 ConcurrentDictionary
替换 Dictionary
来解决这个问题。
我想更好地了解:
NullReferenceException
是如何发生的,以及ConcurrentDictionary
如何解决此问题
编辑:
我的第一印象 - _MyList 为空 - 可能是错误的。相反,异常发生在字典内部:例如,它包含两个数组 buckets
和 entries
,它们在第一次调用 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/