问题描述
所以我遇到了一个有趣的问题,当使用 PhysicalAddress 类型的键时,我在 C# 字典中得到重复键.这很有趣,因为它只会在很长一段时间后发生,而且我无法在完全不同的机器上使用相同的代码在单元测试中重现它.我可以在 Windows XP SP3 机器上可靠地重现它,但只有在让它一次运行数天之后,它才会出现一次.
So I have run into an interesting problem where I am getting duplicate keys in C# Dictionary when using a key of type PhysicalAddress. It is interesting because it only happens after a very long period of time, and I cannot reproduce it using the same code in a unit test on a completely different machine. I can reproduce it reliably on a Windows XP SP3 machine but only after letting it run for days at a time, and even then it only occurs once.
下面是我正在使用的代码,下面是该部分代码的日志输出.
Below is the code that I am using and beneath that is the log output for that part of the code.
代码:
private void ProcessMessages()
{
IDictionary<PhysicalAddress, TagData> displayableTags = new Dictionary<PhysicalAddress, TagData>();
while (true)
{
try
{
var message = incomingMessages.Take(cancellationToken.Token);
VipTagsDisappeared tagsDisappeared = message as VipTagsDisappeared;
if (message is VipTagsDisappeared)
{
foreach (var tag in tagDataRepository.GetFromTagReports(tagsDisappeared.Tags))
{
log.DebugFormat(CultureInfo.InvariantCulture, "Lost tag {0}", tag);
RemoveTag(tag, displayableTags);
}
LogKeysAndValues(displayableTags);
PublishCurrentDisplayableTags(displayableTags);
}
else if (message is ClearAllTags)
{
displayableTags.Clear();
eventAggregator.Publish(new TagReaderError());
}
else if (message is VipTagsAppeared)
{
foreach (TagData tag in tagDataRepository.GetFromTagReports(message.Tags))
{
log.DebugFormat(CultureInfo.InvariantCulture, "Detected tag ({0}) with Exciter Id ({1})", tag.MacAddress, tag.ExciterId);
if (tagRules.IsTagRssiWithinThreshold(tag) && tagRules.IsTagExciterValid(tag))
{
log.DebugFormat(CultureInfo.InvariantCulture, "Detected tag is displayable ({0})", tag);
bool elementAlreadyExists = displayableTags.ContainsKey(tag.MacAddress);
if (elementAlreadyExists)
{
displayableTags[tag.MacAddress].Rssi = tag.Rssi;
}
else
{
displayableTags.Add(tag.MacAddress, tag);
}
}
else
{
log.DebugFormat(CultureInfo.InvariantCulture, "Detected tag is not displayable ({0})", tag);
RemoveTag(tag, displayableTags);
}
}
LogKeysAndValues(displayableTags);
PublishCurrentDisplayableTags(displayableTags);
}
else
{
log.WarnFormat(CultureInfo.InvariantCulture, "Received message of unknown type {0}.", message.GetType());
}
}
catch (OperationCanceledException)
{
break;
}
}
}
private void PublishCurrentDisplayableTags(IDictionary<PhysicalAddress, TagData> displayableTags)
{
eventAggregator.Publish(new CurrentDisplayableTags(displayableTags.Values.Distinct().ToList()));
}
private void RemoveTag(TagData tag, IDictionary<PhysicalAddress, TagData> displayableTags)
{
displayableTags.Remove(tag.MacAddress);
// Now try to remove any duplicates and if there are then log it out
bool removalWasSuccesful = displayableTags.Remove(tag.MacAddress);
while (removalWasSuccesful)
{
log.WarnFormat(CultureInfo.InvariantCulture, "Duplicate tag removed from dictionary: {0}", tag.MacAddress);
removalWasSuccesful = displayableTags.Remove(tag.MacAddress);
}
}
private void LogKeysAndValues(IDictionary<PhysicalAddress, TagData> displayableTags)
{
log.TraceFormat(CultureInfo.InvariantCulture, "Keys");
foreach (var physicalAddress in displayableTags.Keys)
{
log.TraceFormat(CultureInfo.InvariantCulture, "Address: {0}", physicalAddress);
}
log.TraceFormat(CultureInfo.InvariantCulture, "Values");
foreach (TagData physicalAddress in displayableTags.Values)
{
log.TraceFormat(CultureInfo.InvariantCulture, "Address: {0} Name: {1}", physicalAddress.MacAddress, physicalAddress.Name);
}
}
并且进程消息使用如下:
And process messages is used as follows:
Thread processingThread = new Thread(ProcessMessages);
GetFromTagReports 代码
public IEnumerable<TagData> GetFromTagReports(IEnumerable<TagReport> tagReports)
{
foreach (var tagReport in tagReports)
{
TagData tagData = GetFromMacAddress(tagReport.MacAddress);
tagData.Rssi = tagReport.ReceivedSignalStrength;
tagData.ExciterId = tagReport.ExciterId;
tagData.MacAddress = tagReport.MacAddress;
tagData.Arrived = tagReport.TimeStamp;
yield return tagData;
}
}
public TagData GetFromMacAddress(PhysicalAddress macAddress)
{
TagId physicalAddressToTagId = TagId.Parse(macAddress);
var personEntity = personFinder.ByTagId(physicalAddressToTagId);
if (personEntity.Person != null && !(personEntity.Person is UnknownPerson))
{
return new TagData(TagType.Person, personEntity.Person.Name);
}
var tagEntity = tagFinder.ByTagId(physicalAddressToTagId);
if (TagId.Invalid == tagEntity.Tag)
{
return TagData.CreateUnknownTagData(macAddress);
}
var equipmentEntity = equipmentFinder.ById(tagEntity.MineSuiteId);
if (equipmentEntity.Equipment != null && !(equipmentEntity.Equipment is UnknownEquipment))
{
return new TagData(TagType.Vehicle, equipmentEntity.Equipment.Name);
}
return TagData.CreateUnknownTagData(macAddress);
}
创建物理地址的位置
var physicalAddressBytes = new byte[6];
ByteWriter.WriteBytesToBuffer(physicalAddressBytes, 0, protocolDataUnit.Payload, 4, 6);
var args = new TagReport
{
Version = protocolDataUnit.Version,
MacAddress = new PhysicalAddress(physicalAddressBytes),
BatteryStatus = protocolDataUnit.Payload[10],
ReceivedSignalStrength = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(protocolDataUnit.Payload, 12)),
ExciterId = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(protocolDataUnit.Payload, 14))
};
public static void WriteBytesToBuffer(byte[] oldValues, int oldValuesStartindex, byte[] newValues, int newValuesStartindex, int max)
{
var loopmax = (max > newValues.Length || max < 0) ? newValues.Length : max;
for (int i = 0; i < loopmax; ++i)
{
oldValues[oldValuesStartindex + i] = newValues[newValuesStartindex + i];
}
}
注意以下几点:
- messages.Tags 中的每个标签"都包含一个新"物理地址.
- 返回的每个 TagData 也是新的".
- tagRules"方法不会以任何方式修改传入的tag".
- 尝试将 PhysicalAddress 的两个实例(由相同字节创建)放入 Dictionary 的单独测试会引发KeyAlreadyExists"异常.
- 我也尝试了 TryGetValue,它产生了相同的结果.
一切正常的日志输出:
2013-04-26 18:28:34,347 [8] DEBUG ClassName - Detected tag (000CCC756081) with Exciter Id (0)
2013-04-26 18:28:34,347 [8] DEBUG ClassName - Detected tag is displayable (Unknown: ?56081)
2013-04-26 18:28:34,347 [8] TRACE ClassName - Keys
2013-04-26 18:28:34,347 [8] TRACE ClassName - Address: 000CCC755898
2013-04-26 18:28:34,347 [8] TRACE ClassName - Address: 000CCC756081
2013-04-26 18:28:34,347 [8] TRACE ClassName - Address: 000CCC755A27
2013-04-26 18:28:34,347 [8] TRACE ClassName - Address: 000CCC755B47
2013-04-26 18:28:34,347 [8] TRACE ClassName - Values
2013-04-26 18:28:34,347 [8] TRACE ClassName - Address: 000CCC755898 Name: Scotty McTester
2013-04-26 18:28:34,347 [8] TRACE ClassName - Address: 000CCC756081 Name: ?56081
2013-04-26 18:28:34,347 [8] TRACE ClassName - Address: 000CCC755A27 Name: JDTest1
2013-04-26 18:28:34,347 [8] TRACE ClassName - Address: 000CCC755B47 Name: 33 1
2013-04-26 18:28:34,347 [8] TRACE ClassName - Current tags: Scotty McTester, ?56081, JDTest1, 33 1
我们得到重复键的日志输出:
Log output where we get a duplicate key:
2013-04-26 18:28:35,608 [8] DEBUG ClassName - Detected tag (000CCC756081) with Exciter Id (0)
2013-04-26 18:28:35,608 [8] DEBUG ClassName - Detected tag is displayable (Unknown: ?56081)
2013-04-26 18:28:35,608 [8] TRACE ClassName - Keys
2013-04-26 18:28:35,608 [8] TRACE ClassName - Address: 000CCC755898
2013-04-26 18:28:35,608 [8] TRACE ClassName - Address: 000CCC756081
2013-04-26 18:28:35,618 [8] TRACE ClassName - Address: 000CCC755A27
2013-04-26 18:28:35,618 [8] TRACE ClassName - Address: 000CCC755B47
2013-04-26 18:28:35,618 [8] TRACE ClassName - Address: 000CCC756081
2013-04-26 18:28:35,618 [8] TRACE ClassName - Values
2013-04-26 18:28:35,618 [8] TRACE ClassName - Address: 000CCC755898 Name: Scotty McTester
2013-04-26 18:28:35,618 [8] TRACE ClassName - Address: 000CCC756081 Name: ?56081
2013-04-26 18:28:35,648 [8] TRACE ClassName - Address: 000CCC755A27 Name: JDTest1
2013-04-26 18:28:35,648 [8] TRACE ClassName - Address: 000CCC755B47 Name: 33 1
2013-04-26 18:28:35,648 [8] TRACE ClassName - Address: 000CCC756081 Name: ?56081
2013-04-26 18:28:35,648 [8] TRACE ClassName - Current tags: Scotty McTester, ?56081, JDTest1, 33 1, ?56081
请注意,所有事情都发生在单个线程上(参见 [8]),因此字典不可能同时被修改.摘录来自相同的日志和相同的流程实例.另请注意,在第二组日志中,我们最终得到了两个相同的键!
Notice that everything is happening on a single thread (see the [8]) so there is no chance of the dictionary having been concurrently modified. The excerpts are from the same log and the same process instance. Also notice that in the second set of logs we end up with two keys that are the same!
我正在调查的内容:我已将 PhysicalAddress 更改为一个字符串,以查看是否可以将其从嫌疑人列表中消除.
What I am looking into: I have changed PhysicalAddress to a string to see if I can eliminate that from the list of suspects.
我的问题是:
- 是否存在我在上面的代码中没有看到的问题?
- PhysicalAddress 上的相等方法有问题吗?(那只是偶尔的错误?)
- 词典有问题吗?
推荐答案
Dictionary 期望不可变对象作为键,具有稳定的 GetHashCode/Equals 实现.这意味着对象放入字典后,GetHashCode 返回的值应该不会改变,并且对此对象所做的任何更改都不应影响 Equals 方法.
Dictionary expects immutable object as a key, with a stable GetHashCode / Equals implementation. This means that after object is placed into dictionary, value returned by GetHashCode should not change, and any changes made to this object should not affect Equals method.
虽然 PhysicalAddress 类被设计为不可变的,但它仍然包含一些扩展点,它的不变性是有缺陷的.
Although PhysicalAddress class was designed immutable, it still contains a few extension points, where its immutability is flawed.
首先,可以通过输入字节数组来改变,不是复制而是通过引用传递的,像这样:
First, it can be changed through input byte array, which is not copied but passed by reference, like this:
var data = new byte[] { 1,2,3 };
var mac = new PhysicalAddress(data);
data[0] = 0;
第二,PhysicalAddress 不是密封类,可以通过派生来改变通过重写 Constructor/GetHashCode/Equals 方法实现.但是这个用例看起来更像是一个 hack,所以我们将忽略它,以及通过反射进行修改.
Second, PhysicalAddress is not a sealed class, and can be changed by derived implementation through overriding Constructor / GetHashCode / Equals methods. But this use case looks more like a hack, so we will ignore it, as well as modifications through reflection.
您的情况只能通过首先将 PhysicalAddress 对象放入字典来实现,然后修改其源字节数组,然后将其包装到新的 PhysicalAddress 实例中.
Your situation can only be achieved by first placing PhysicalAddress object into dictionary, and then modifying its source byte array, and then wrapping it into new PhysicalAddress instance.
幸运的是,PhysicalAddress 的 GetHashCode 实现只计算一次哈希,如果同一个实例被修改,它仍然被放入同一个字典桶中,并由 Equals 再次定位.
Luckily, PhysicalAddress' GetHashCode implementation computes hash only once, and if same instance is modified, it is still placed into same dictionary bucket, and located again by Equals.
但是,如果源字节数组被传递到 PhysicalAddress 的另一个实例,其中 hash尚未计算 - 为新的 byte[] 值重新计算哈希,找到新的存储桶,并将副本插入字典.在极少数情况下,可以找到相同的存储桶来自新的哈希,同样,没有重复插入.
But, if source byte array is passed into another instance of PhysicalAddress, where hash was not yet computed - hash is recomputed for new byte[] value, new bucket is located, and duplicate is inserted into dictionary. In rare cases, same bucket can be located from new hash, and again, no duplicate is inserted.
这是重现问题的代码:
using System;
using System.Collections.Generic;
using System.Net.NetworkInformation;
class App
{
static void Main()
{
var data = new byte[] { 1,2,3,4 };
var mac1 = new PhysicalAddress(data);
var mac2 = new PhysicalAddress(data);
var dictionary = new Dictionary<PhysicalAddress,string>();
dictionary[mac1] = "A";
Console.WriteLine("Has mac1:" + dictionary.ContainsKey(mac1));
//Console.WriteLine("Has mac2:" + dictionary.ContainsKey(mac2));
data[0] = 0;
Console.WriteLine("After modification");
Console.WriteLine("Has mac1:" + dictionary.ContainsKey(mac1));
Console.WriteLine("Has mac2:" + dictionary.ContainsKey(mac2));
dictionary[mac2] = "B";
foreach (var kvp in dictionary)
Console.WriteLine(kvp.Key + "=" + kvp.Value);
}
}
注意注释行 - 如果我们取消注释,ContainsKey"方法将预先计算 mac2 的哈希,即使修改后也是一样的.
Note the commented line - if we will uncomment it, "ContainsKey" method will precompute hash for mac2, and it will be the same even after modification.
所以我的建议是找到生成 PhysicalAddress 实例的代码,然后创建每个构造函数调用的新字节数组副本.
So my recommendation is to locate piece of code which generates PhysicalAddress instances, and create new byte array copy for each constructor call.
这篇关于使用 PhysicalAddress 作为键时字典中的重复键的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持跟版网!