2013-04-29 153 views
7

因此,我遇到了一个有趣的问题,即在使用类型为PhysicalAddress的密钥时,在C#字典中获取重复密钥。这很有趣,因为它只发生在很长一段时间之后,我不能在一台完全不同的机器上使用相同的代码在单元测试中重现它。我可以在Windows XP SP3机器上可靠地重现它,但一次只能让它运行几天,即使它只发生一次。使用PhysicalAddress作为密钥时字典中的重复密钥

下面是我使用的代码,下面是该代码部分的日志输出。

代码:

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); 
    } 
} 

和处理消息的使用步骤如下:

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]; 
    } 
} 

注意以下几点:

  • Every在messages.Tags '标签' 包含 '新' PhysicalAddress。
  • 返回的每个TagData也是“新”。
  • 'tagRules'方法不会以任何方式修改传入的'标记'。
  • 尝试将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 

日志输出,其中我们得到了重复键:

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 ]),所以字典没有被同时修改的机会。摘录来自相同的日志和相同的流程实例。另外请注意,在第二组日志中,我们最终得到两个相同的密钥!

我在看什么:我已经将PhysicalAddress更改为字符串,以查看我是否可以从嫌疑犯名单中删除该名字。

我的问题是:

  • 有没有,我不是在上面的代码中看到一个问题吗?
  • PhysicalAddress上的等号方法有问题吗? (现在只有错误?)
  • 字典有问题吗?
+0

您可以注意到非工作运行不会在同一时间发生。这可能是线程有问题的一个论据。你怎么能确定'displayableTags'不是一个共享对象?这是一个局部变量吗?属性?此外,使用'TryGetValue'而不是'ContainsKey'。 – 2013-04-29 07:49:55

+0

我可以肯定,因为'displayableTags'是在Thread构造函数调用的方法中创建的本地创建的变量。我尝试过TryGetValue,它做了同样的事情(我将它添加到问题中)。此外,从TryGetValue在MSDN DOCO: _this方法结合了ContainsKey方法的功能和项目property._ – JohnDRoach 2013-04-29 07:57:37

+0

你能在一个块张贴代码?问题可能在于你的日志功能,我们可以看到吗? – 2013-04-29 08:01:38

回答

9

字典期望不可变的对象作为一个关键,具有稳定的GetHashCode/Equals实现。 这意味着将对象放入字典后,由GetHashCode返回的值应该不会更改,并且对此对象所做的任何更改都不应该影响Equals方法。

尽管PhysicalAddress类被设计为不可变的,它仍然包含一些扩展点,其中不变性是有缺陷的。

首先,它可以通过输入字节数组被改变, 未复制,但通过引用传递,这样的:

var data = new byte[] { 1,2,3 }; 
var mac = new PhysicalAddress(data); 
data[0] = 0; 

其次,PhysicalAddress是不是一个密封类,并且可以通过衍生来改变 通过重写Constructor/GetHashCode/Equals方法来实现。 但是这个用例看起来更像是一个黑客,所以我们会忽略它,以及通过反射进行修改。

您的情况只能通过首先将PhysicalAddress对象放入字典 然后修改其源字节数组,然后将其包装到新的PhysicalAddress实例中来实现。

幸运的是,PhysicalAddress的GetHashCode实现只计算散列一次, ,如果修改了相同的实例,它仍然放在同一个字典存储桶中,并且由Equals再次定位。

但是,如果源字节数组传递到PhysicalAddress,其中散列 尚未计算的另一个实例 - 散列重新计算新的字节[]值,新桶位于, 和重复插入字典。在极少数情况下,可以从新散列找到相同的桶 ,并且再次插入不重复。

这里抄录该问题的代码:

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哈希,甚至修改后,将是相同的。

所以我的建议是找到生成PhysicalAddress实例的代码片段,并为每个构造函数调用创建新的字节数组副本。

+0

谢谢你构造良好的答案:)不幸的是,我们已经创建了每个构造函数调用的新的字节数组。查看最近添加到问题中的代码。 TagReport上的MacAddress属性在此之后从未被分配,并且仅被使用。此外,在那里创建的实例最终使它成为GetTagReports调用的途径。 – JohnDRoach 2013-04-29 23:57:00

+0

构造TagReport后,physicalAddressBytes会发生什么?它在某处被重用了吗?字典的平均大小是多少?它多久修改一次? – Alexander 2013-04-30 07:39:41

+0

几个更多的想法 - 尝试测试您能够重现此问题的服务器的内存。将所有字典访问方法放入lock()中,以确保没有多线程问题。 – Alexander 2013-04-30 17:43:40