【问题标题】:Error Adding To HashSet From Multiple Threads从多个线程添加到 HashSet 时出错
【发布时间】:2012-11-10 02:31:39
【问题描述】:

我正在尝试从多个线程添加到哈希集。如果该项目已经存在,我想更新它,如果它不存在,我想将它添加到列表中。

我使用的代码最终会出现很多重复项,我推测是因为多个项目突然指向同一个引用。但我看不出发生这种情况的地点或原因。

下面是我第一次看到问题时使用的代码,后面是“Log”字符串结尾。您可以看到突然间所有已添加的项目都具有相同的值。

lock (_remoteDevicesLock)
{
    RemoteDevice rDevice = new RemoteDevice(notifyMessage.UUID, notifyMessage.Location);
    log += notifyMessage.UUID + " " + rDevice.UUID;
    if (!_remoteDevices.Add(rDevice))
    {
        log += " Not Added \r\n";
        rDevice = (from d in _remoteDevices
                   where d.UUID.Trim().Equals(notifyMessage.UUID.Trim(), StringComparison.OrdinalIgnoreCase)
                   select d).FirstOrDefault();
        if (rDevice != null)
        {
            //Update Device Expire Time
        }
    }                            
    else
    {
        log += " Added \r\n Current HashSet: \r\n";

        foreach (RemoteDevice rd in _remoteDevices)
        {
            log += rd.UUID + " \r\n";
        }
    }
}


00000000-0000-0001-1000-001cdf885737 00000000-0000-0001-1000-001cdf885737 Added 

Current HashSet: 
00000000-0000-0001-1000-001cdf885737 

00000000-0000-0001-1000-001cdf885737 00000000-0000-0001-1000-001cdf885737 Not Added 
00000000-0000-0001-1000-001cdf885737 00000000-0000-0001-1000-001cdf885737 Not Added 
00000000-0000-0001-0002-001cdf885737 00000000-0000-0001-0002-001cdf885737 Added 

Current HashSet: 
00000000-0000-0001-1000-001cdf885737 
00000000-0000-0001-0002-001cdf885737 

00000000-0000-0001-0002-001cdf885737 00000000-0000-0001-0002-001cdf885737 Not Added 
00000000-0000-0001-0002-001cdf885737 00000000-0000-0001-0002-001cdf885737 Not Added 
00000000-0000-0001-0002-001cdf885737 00000000-0000-0001-0002-001cdf885737 Not Added 
00000000-0000-0001-0002-001cdf885737 00000000-0000-0001-0002-001cdf885737 Not Added 
00000000-0000-0001-0001-001cdf885737 00000000-0000-0001-0001-001cdf885737 Added 

Current HashSet: 
00000000-0000-0001-1000-001cdf885737 
00000000-0000-0001-0002-001cdf885737 
00000000-0000-0001-0001-001cdf885737 

00000000-0000-0001-0001-001cdf885737 00000000-0000-0001-0001-001cdf885737 Not Added 
00000000-0000-0001-0001-001cdf885737 00000000-0000-0001-0001-001cdf885737 Not Added 
00000000-0000-0001-0000-001cdf885737 00000000-0000-0001-0000-001cdf885737 Added 

Current HashSet: 
00000000-0000-0001-1000-001cdf885737 
00000000-0000-0001-0002-001cdf885737 
00000000-0000-0001-0001-001cdf885737 
00000000-0000-0001-0000-001cdf885737 

00000000-0000-0001-0000-001cdf885737 00000000-0000-0001-0000-001cdf885737 Not Added 
00000000-0000-0001-0000-001cdf885737 00000000-0000-0001-0000-001cdf885737 Not Added 
00000000-0000-0001-0000-001cdf885737 00000000-0000-0001-0000-001cdf885737 Not Added 
00000000-0000-0001-0000-001cdf885737 00000000-0000-0001-0000-001cdf885737 Not Added 
00000000-0000-0001-1000-001cdf885737 00000000-0000-0001-1000-001cdf885737 Not Added 
00000000-0000-0001-1000-001cdf885737 00000000-0000-0001-1000-001cdf885737 Not Added 
00000000-0000-0001-1000-001cdf885737 00000000-0000-0001-1000-001cdf885737 Not Added 
00000000-0000-0001-0002-001cdf885737 00000000-0000-0001-0002-001cdf885737 Not Added 
00000000-0000-0001-0002-001cdf885737 00000000-0000-0001-0002-001cdf885737 Not Added 
00000000-0000-0001-0002-001cdf885737 00000000-0000-0001-0002-001cdf885737 Not Added 
00000000-0000-0001-0002-001cdf885737 00000000-0000-0001-0002-001cdf885737 Not Added 
00000000-0000-0001-0002-001cdf885737 00000000-0000-0001-0002-001cdf885737 Not Added 
00000000-0000-0001-0001-001cdf885737 00000000-0000-0001-0001-001cdf885737 Not Added 
00000000-0000-0001-0001-001cdf885737 00000000-0000-0001-0001-001cdf885737 Not Added 
00000000-0000-0001-0001-001cdf885737 00000000-0000-0001-0001-001cdf885737 Not Added 
00000000-0000-0001-0000-001cdf885737 00000000-0000-0001-0000-001cdf885737 Not Added 
00000000-0000-0001-0000-001cdf885737 00000000-0000-0001-0000-001cdf885737 Not Added 
00000000-0000-0001-0000-001cdf885737 00000000-0000-0001-0000-001cdf885737 Not Added 
00000000-0000-0001-0000-001cdf885737 00000000-0000-0001-0000-001cdf885737 Not Added 
00000000-0000-0001-0000-001cdf885737 00000000-0000-0001-0000-001cdf885737 Not Added 
00000000-0000-0001-1000-001cdf885737 00000000-0000-0001-1000-001cdf885737 Not Added 
00000000-0000-0001-1000-001cdf885737 00000000-0000-0001-1000-001cdf885737 Not Added 
00000000-0000-0001-1000-001cdf885737 00000000-0000-0001-1000-001cdf885737 Not Added 
00000000-0000-0001-0002-001cdf885737 00000000-0000-0001-0002-001cdf885737 Not Added 
00000000-0000-0001-0002-001cdf885737 00000000-0000-0001-0002-001cdf885737 Not Added 
00000000-0000-0001-0002-001cdf885737 00000000-0000-0001-0002-001cdf885737 Not Added 
00000000-0000-0001-0002-001cdf885737 00000000-0000-0001-0002-001cdf885737 Not Added 
00000000-0000-0001-0002-001cdf885737 00000000-0000-0001-0002-001cdf885737 Not Added 
00000000-0000-0001-0001-001cdf885737 00000000-0000-0001-0001-001cdf885737 Not Added 
00000000-0000-0001-0001-001cdf885737 00000000-0000-0001-0001-001cdf885737 Not Added 
00000000-0000-0001-0001-001cdf885737 00000000-0000-0001-0001-001cdf885737 Not Added 
00000000-0000-0001-0000-001cdf885737 00000000-0000-0001-0000-001cdf885737 Not Added 
00000000-0000-0001-0000-001cdf885737 00000000-0000-0001-0000-001cdf885737 Not Added 
00000000-0000-0001-0000-001cdf885737 00000000-0000-0001-0000-001cdf885737 Not Added 
00000000-0000-0001-0000-001cdf885737 00000000-0000-0001-0000-001cdf885737 Not Added 
00000000-0000-0001-0000-001cdf885737 00000000-0000-0001-0000-001cdf885737 Not Added 
00000000-0000-0001-1000-001cdf885737 00000000-0000-0001-1000-001cdf885737 Added 

Current HashSet: 
00000000-0000-0001-0000-001cdf885737 
00000000-0000-0001-0000-001cdf885737 
00000000-0000-0001-0000-001cdf885737 
00000000-0000-0001-0000-001cdf885737 
00000000-0000-0001-1000-001cdf885737 

更新:这里是 GetHashCode And Equals As Requested,尽管我认为问题不在于这里,因为我使用的是带有手动检查的列表并且也有问题。

public override bool Equals(object obj)
{
    var other = obj as RemoteDevice;
    if (other == null)
    {
        return false;
    }
    else
    {
        return UUID.Trim().Equals(other.UUID.Trim(), StringComparison.OrdinalIgnoreCase);
    }
}

public override int GetHashCode()
{
    return UUID.GetHashCode();
}

【问题讨论】:

  • 1.不要锁定暴露在课堂之外的对象;您可能应该创建一个特定的对象只是为了锁定。 2.不要使用字符串连接添加到日志字符串,使用StringBuilder
  • RemoteDevice.Equals 和 RemoteDevice.GetHashCode 是什么样的?
  • 你可以检查这个线程stackoverflow.com/questions/4306936/…
  • @Servy 我如何保护哈希集不被添加和删除。哈希集在类外读取但未修改。 2)“日志”只针对这个问题。
  • 您是否在检索到哈希集中的条目后更新它们的 UUID?如果不是,您如何解释日志中现有条目的 id 的变化?

标签: c# .net c#-4.0


【解决方案1】:

使用字典,将使您的代码更加简单和健壮,并且不依赖于 GetHashCode 覆盖。查找也会快得多,LINQ 查询在性能方面并不是很好。 应该注意一些简单的事情,比如不要多次计算相同的值,尤其是在循环中。 IE。 notifyMessage.UUID.Trim() 在 LINQ 查询中被调用的次数与列表中的设备数量一样多。 id应该在循环之前计算一次并重复使用。

这是一个使用字典的示例:

var _remoteDevices = new Dictionary<string, RemoteDevice>();

...

var deviceId = notifyMessage.UUID.Trim().ToLowerInvariant();

RemoteDevice remoteDevice;

if (_remoteDevices.TryGetValue(deviceId, out remoteDevice))
{
    UpdateDevice(remoteDevice);
}
else
{
    var newDevice = CreateDevice(notifyMessage);

    _remoteDevices.Add(deviceId, newDevice);
}

上面的代码在您问题的代码中执行一次查找,而不是两次,其中 _remoteDevices.Add 和 LINQ 查询都执行查找。 LINQ 查询实际上进行了完整的迭代,因为使用 Where 而不是 FirstOrDefault 和谓词,除非编译器足够聪明地将表达式转换为 FirstOrDefault(d => d.UUID.Trim().Equals(notifyMessage.UUID.Trim() , StringComparison.OrdinalIgnoreCase)。

【讨论】:

    【解决方案2】:

    按照设计,重要的是集合中的项目的哈希码在集合内时永远不会发生。该集合无法检测到对象的内部状态发生了变化,因此它将在“桶”中,因为它是旧的哈希值,所以当您尝试使用新的哈希码添加另一个项目时,它会看到它是“ bucket" 为空并添加项目。如果您想“更改”当前位于集合中的项目,您应该删除它,更改它,然后将其重新添加。或者,更好的是(从设计角度)删除旧值,并添加一个新对象完全(可能是从删除的部分复制了某些方面)。

    这似乎是您的问题;尽管不是您遇到的问题,但我会将其余的建议留在下面。

    您的RemoteDevice 类可能不会用有意义的实现覆盖EqualsGetHashCode。默认实现(在object 中定义仅基于对象内存中的地址,因此根据该定义,具有所有相同值的两个不同实例将“不相等”。因为您似乎只有一个 GUID作为唯一 ID 有效(GUID 具有合理的 EqualsGetHashCode 定义)您的实现应该遵循这一点。

    即:

    public class RemoteDevice
    {
        public Guid UUID { get; set; }
    
        public override bool Equals(object obj)
        {
            RemoteDevice other = obj as RemoteDevice;
            if (other == null) return false;
            return UUID.Equals(other.UUID);
        }
    
        public override int GetHashCode()
        {
            return UUID.GetHashCode();
        }
    }
    

    您似乎也误解了lock 的工作原理。使用lock(myObject) 不会阻止任何其他对象使用myObject。它所做的只是导致其他人在同一实例上尝试lock,它会等到您退出lock,然后他们才能进入他们的实例。这意味着在任何人访问 HashSet 之前,您的代码需要在对象的同一实例上使用 lock(因为 HashSet 不是为多线程使用而设计的)。

    如果这样做不是一个选项,或者不受欢迎,您需要考虑创建一个可以从多个线程访问的集合。许多集合在System.Collections.Concurrent 中都有实现,但不幸的是没有ConcurrentSet。有几种选择;我们可以自己制作一个,但另一种选择是使用ConcurrentDictionary 并简单地忽略值而只使用键。这会有点混乱,但是有效地创建自己的并发集合是......很难。就个人而言,如果我想使用一个,我只需在 ConcurrentDictionary 周围创建一个包装器,以隐藏它存储对的事实。

    【讨论】:

    • 我已经更新了问题并且覆盖看起来几乎相同。
    • 我对锁的问题仍然有些困惑。我正在锁定 _remoteDevicesLock 而不是 _remoteDevices |我锁定了那部分代码,以便线程等待并一次执行一个,从而使哈希集线程安全。如果我没有这样做,请解释为什么以及我做错了什么。
    • Servy-“集合中的项目在集合内时永远不会有机会”是问题所在。事实证明(我不会命名品牌)但我的无线路由器出于某种原因根据 Upnp 的规范更改了它的 UUID。我想我只需要弄清楚如何解决这个问题。
    • @Oli 祝你好运。我会将该部分移到顶部以强调它。
    【解决方案3】:

    以下两个UUIDs 将具有不同的哈希码,但比较相等:“x”、“x”。原因:您对待空白的方式不同。

    您需要使GetHashCodeEquals 保持不变。如果Equals 返回真,则两个哈希码必须相同。如果您不遵守本合同,HashSet 的行为方式未定义(可能重复)。

    解决方案:Trim 在两个地方都存在,或者都不在。

    【讨论】:

    • 我刚刚尝试在两个地方都进行修剪,但仍然没有问题
    猜你喜欢
    • 2011-05-17
    • 1970-01-01
    • 2014-06-30
    • 2018-05-28
    • 2019-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-28
    相关资源
    最近更新 更多