2013-04-29 164 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