【问题标题】:Duplicate keys in Dictionary when using PhysicalAddress as the key使用 PhysicalAddress 作为键时字典中的重复键
【发布时间】:2013-04-22 19:19:20
【问题描述】:

所以我遇到了一个有趣的问题,当使用 PhysicalAddress 类型的键时,我在 C# 字典中得到重复键。这很有趣,因为它只会在很长一段时间后发生,而且我无法在完全不同的机器上使用相同的代码在单元测试中重现它。我可以在 Windows XP SP3 机器上可靠地重现它,但只有在让它一次运行数天之后,它才会出现一次。

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

代码:

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

注意以下几点:

  • messages.Tags 中的每个“标签”都包含一个“新”物理地址。
  • 返回的每个 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 上的相等方法是否存在问题? (那只是偶尔的错误?)
  • 字典有问题吗?

【问题讨论】:

  • 您可以注意到非工作运行不会同时发生。这可能是线程问题的一个论据。您如何确定displayableTags 不是共享对象?这是局部变量吗?财产?此外,请使用TryGetValue 而不是ContainsKey
  • 我可以肯定,因为'displayableTags'是在线程构造函数调用的方法中创建的本地创建变量。我尝试了 TryGetValue,它做了同样的事情(我将把它添加到问题中)。此外,来自 TryGetValue 上的 msdn 文档:此方法结合了 ContainsKey 方法和 Item 属性的功能。
  • 你能把代码贴在一块吗?问题也可能出在您的 Log 函数中,我们可以看到吗?
  • 我原本不想添加太多代码,以防帖子混乱。现在发布更多代码。
  • 反编译 PhysicalAddress 上的 Equals 方法表明它只是比较底层字节数组 - 它检查长度然后逐个元素遍历数组。您是使用带有 byte[] 的构造函数还是使用 PhysicalAddress.Parse(string) 创建标签地址?

标签: c# .net dictionary duplicates


【解决方案1】:

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

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

首先,可以通过输入字节数组来改变, 不是复制而是通过引用传递的,像这样:

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

第二,PhysicalAddress 不是密封类,可以通过派生来改变 通过重写 Constructor / GetHashCode / Equals 方法实现。 但是这个用例看起来更像是一个 hack,所以我们将忽略它,以及通过反射进行修改。

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

幸运的是,PhysicalAddress 的 GetHashCode 实现只计算一次哈希, 如果同一个实例被修改,它仍然被放入同一个字典桶中, 并由 Equals 再次定位。

但是,如果源字节数组被传递到 PhysicalAddress 的另一个实例,其中 hash 尚未计算 - 为新的 byte[] 值重新计算哈希,找到新的存储桶, 并将副本插入字典。在极少数情况下,可以找到相同的存储桶 来自新的哈希,同样,没有重复插入。

这是重现问题的代码:

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 实例的代码,然后创建 每个构造函数调用的新字节数组副本。

【讨论】:

  • 感谢您提供结构良好的答案 :) 不幸的是,我们已经为每个构造函数调用创建了一个新的字节数组。请参阅最近添加到问题中的代码。 TagReport 上的 MacAddress 属性在此之后从未分配给它,并且仅被使用。此外,在那里创建的实例最终也可以调用 GetTagReports。
  • TagReport 构建后,physicalAddressBytes 会发生什么?它在某处重复使用吗?字典的平均大小是多少?多久修改一次?
  • 更多想法 - 尝试测试能够重现此问题的服务器内存。将所有字典访问方法放入 lock() 中,以确保没有多线程问题。
  • 在调用 PhysicalAddress 构造函数后,不会在任何地方使用变量“physicalAddressBytes”。在我的测试过程中,字典的最大大小为 6,并且平均每秒修改 2-3 次。这些修改主要只是对现有项目的更新,尽管大约每 10 秒就会删除和添加相同的标签(没有被复制)。我已将密钥更改为字符串而不是 PhysicalAddress 并且没有再次看到问题。我不认为我将来会使用 PhysicalAddress 类:/
  • 即使我遇到的问题不是由与您的示例代码相同的原因引起的,我仍将其标记为答案,因为它似乎是唯一可能的原因。我觉得问题是某个底层字节数组中的某种随机内存损坏导致 GetHashCode 调用发生变化。 PhysicalAddress 的设计方式确实很愚蠢。
猜你喜欢
  • 2017-03-23
  • 2012-02-20
  • 2016-01-07
  • 2019-11-08
  • 1970-01-01
  • 2018-10-30
  • 1970-01-01
  • 2013-07-25
  • 1970-01-01
相关资源
最近更新 更多