HashSet <T>线程是否安全,作为ConcurrentDictionary <TKey,HashSet <T >>的值?

Mar*_*rko 4 c# multithreading concurrentdictionary

如果我有以下代码:

var dictionary = new ConcurrentDictionary<int, HashSet<string>>();

foreach (var user in users)
{
   if (!dictionary.ContainsKey(user.GroupId))
   {
       dictionary.TryAdd(user.GroupId, new HashSet<string>());
   }

   dictionary[user.GroupId].Add(user.Id.ToString());
}
Run Code Online (Sandbox Code Playgroud)

将项添加到HashSet本身就是线程安全的行为是因为HashSet是并发字典的值属性吗?

Jos*_*hua 5

否.将容器放入线程安全的容器中不会使内部容器线程安全.

dictionary[user.GroupId].Add(user.Id.ToString());
Run Code Online (Sandbox Code Playgroud)

从ConcurrentDictionary中检索HashSet之后调用它.如果同时从两个线程查找此GroupId,则会以奇怪的故障模式破坏您的代码.我看到我的一个队友犯了一个错误,就是没有锁定他的套装,结果并不漂亮.

这是一个合理的解决方案.我自己会做一些不同的事情,但这更接近你的代码.

if (!dictionary.ContainsKey(user.GroupId)
{
    dictionary.TryAdd(user.GroupId, new HashSet<string>());
}
var groups = dictionary[user.GroupId];
lock(groups)
{
    groups.Add(user.Id.ToString())
}
Run Code Online (Sandbox Code Playgroud)


SO *_*ood 5

不,集合(字典本身)是线程安全的,而不是你放入其中的任何内容。您有几个选择:

  1. 正如@TheGeneral提到的那样使用AddOrUpdate

    dictionary.AddOrUpdate(user.GroupId,  new HashSet<string>(), (k,v) => v.Add(user.Id.ToString());
    
    Run Code Online (Sandbox Code Playgroud)
  2. 使用并发集合,例如ConcurrentBag<T>

    ConcurrentDictionary<int, ConcurrentBag<string>>
    
    Run Code Online (Sandbox Code Playgroud)

每当您构建字典时,就像在代码中一样,您最好尽可能少地访问它。想想这样的事情:

var dictionary = new ConcurrentDictionary<int, ConcurrentBag<string>>();
var grouppedUsers = users.GroupBy(u => u.GroupId);

foreach (var group in grouppedUsers)
{
    // get the bag from the dictionary or create it if it doesn't exist
    var currentBag = dictionary.GetOrAdd(group.Key, new ConcurrentBag<string>());

    // load it with the users required
    foreach (var user in group)
    {
        if (!currentBag.Contains(user.Id.ToString())
        {
            currentBag.Add(user.Id.ToString());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
  1. 如果您确实想要一个内置的并发类似 HashSet 的集合,则需要使用ConcurrentDictionary<int, ConcurrentDictionary<string, string>>, 并关心内部集合中的键或值。

  • 第一个解决方案是不正确的。根据[文档](https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2.addorupdate) *“updateValueFactory委托在锁之外调用,以避免在锁下执行未知代码可能引起的问题。”* 因此,第一个解决方案可能会导致 `HashSet&lt;string&gt;` 损坏,或丢失更新。 (2认同)
  • 第二个解决方案也是不正确的。`Contains`+`Add` 组合不是原子的。`Contains` 甚至不是线程安全的,因为 *“通过 ConcurrentBag&lt;T&gt; 实现的接口之一访问的成员(包括扩展方法)不能保证线程安全”* ([参考](https:// learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentbag-1#thread-safety))。除此之外,“Contains”是一个 O(N) 操作,因此不适合替换“HashSet”。 (2认同)