7

そのため、タイプ PhysicalAddress のキーを使用すると、C# ディクショナリでキーが重複するという興味深い問題に遭遇しました。非常に長い時間が経過した後にのみ発生し、まったく別のマシンでの単体テストで同じコードを使用して再現できないため、これは興味深いことです。Windows XP SP3 マシンで確実に再現できますが、一度に何日も実行した後にのみ発生し、それでも発生するのは 1 回だけです。

以下は私が使用しているコードで、その下にはコードのその部分のログ出力があります。

コード:

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」メソッドは、渡された「tag」を決して変更しません。
  • PhysicalAddress の 2 つのインスタンス (同じバイトから作成されたもの) をディクショナリに配置しようとする個々のテストでは、'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] を参照)。したがって、辞書が同時に変更される可能性はありません。抜粋は、同じログと同じプロセス インスタンスからのものです。また、ログの 2 番目のセットでは、同じ 2 つのキーになっていることに注意してください。

私が調べていること: PhysicalAddress を文字列に変更して、容疑者のリストから削除できるかどうかを確認しました。

私の質問は次のとおりです。

  • 上記のコードに見られない問題はありますか?
  • PhysicalAddress の等価メソッドに問題はありますか? (それは時々エラーだけですか?)
  • 辞書に問題はありますか?
4

1 に答える 1

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 実装はハッシュを 1 回だけ計算し、同じインスタンスが変更された場合でも同じ辞書バケットに配置され、Equals によって再度検索されます。

ただし、ハッシュがまだ計算されていない PhysicalAddress の別のインスタンスにソース バイト配列が渡された場合、ハッシュは新しい 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 インスタンスを生成するコードの一部を見つけて、コンストラクター呼び出しごとに新しいバイト配列のコピーを作成することをお勧めします。

于 2013-04-29T19:25:22.447 に答える