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

Is HashSet<T> thread safe as a value of ConcurrentDictionary<TKey, HashSet<T>>?

如果我有以下代码:

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());
}

因为 HashSet 是并发字典的值 属性,所以将项目添加到 HashSet 的行为本质上是线程安全的吗?

没有。将容器放入线程安全容器中并不能使内部容器线程安全。

dictionary[user.GroupId].Add(user.Id.ToString());

正在从 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());
}

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

  1. 如@TheGeneral 所述使用AddOrUpdate

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

    ConcurrentDictionary<int, ConcurrentBag<string>>
    

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

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());
        }
    }
}
  1. 如果你真的想要一个内置的并发 HashSet 类集合,你需要使用 ConcurrentDictionary<int, ConcurrentDictionary<string, string>>,并关心内部的键或值。