有一天,我偶然发现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
通过致电myClass
向Add
注册。虽然some1
在同一个线程中创建了所有这些对象,但some2
在不同的线程中执行。并some2
调用Add
会导致NullReferenceException
(日志文件显示线程ID)。
我的印象是存在一些线程问题。在多核机器上,不同的线程(核心)有myClass
的不同“副本”,一个是完全构造的,另一个不是。
我可以通过Dictionary
替换ConcurrentDictionary
来解决问题。
我希望更好地理解:
NullReferenceException
如何发生,以及
ConcurrentDictionary
如何解决此问题
编辑:
我的第一印象 - _MyList为null - 可能是错误的。相反,异常发生在Dictionary中:例如它包含两个数组buckets
和entries
,它们在第一次调用Add
时被初始化。在这里,竞争发生,一个线程开始初始化,而另一个线程假定初始化完成。
因此,我最初的一些寄存器缓存导致多线程环境中的问题以及ConcurrentDictionary的某些“魔力”在某种程度上隐含地“挥发”是错误的 - 我的问题的第二部分已经没有任何意义了。< / p>
答案 0 :(得分:4)
如何发生NullReferenceException
字典不知道将在其中存储多少数据,因此当您添加项目时,它必须自行调整大小。这个调整大小操作需要时间,如果两个线程都检测到需要额外的空间,他们都会尝试同时执行调整大小。这种竞争条件的“宽松”会将他的数据写入被“获胜者”覆盖的数据副本,这可能会导致宽松写入的位置看起来像内部数组中的有效位置但实际上它保留未初始化的数据。当您尝试访问此未初始化的数据时,它会抛出NullReferenceException
。 (注意:如果多个线程同时尝试写入字典,此调整大小竞争只是众多竞争条件中的一个)
ConcurrentDictionary如何解决此问题
在内部检查任何可能导致竞争条件的操作,以查看该线程是否更宽松。如果是它它会丢弃它所做的工作并重新尝试再次插入数据,直到它成功。一旦成功,函数调用就会返回。
以下是进行该检查的逻辑的副本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
}