問題描述
所以我遇到了一個有趣的問題,當使用 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 作為鍵時字典中的重復鍵的文章就介紹到這了,希望我們推薦的答案對大家有所幫助,也希望大家多多支持html5模板網!