非并发集合在并发集合内部是否安全?

3

我希望开始在我的项目中实现一些并发函数。最近我发现了System.Collections.Concurrent命名空间,我打算利用它。

我使用的对象来跟踪操作的整体状态本质上是一个带有一些嵌套自定义对象的字典。我的想法是只要最高级别的集合被配置为并发/线程安全,那么嵌套的集合是否安全并不重要,因为数据将由更高级别的集合锁定。

这个假设正确吗?

例如,像以下PowerShell代码这样的东西是否安全使用?

[System.Collections.Concurrent.ConcurrentDictionary[[String], [MyCustomClass]]]::new()

此外,我有一些自定义类来扩展HashSet以避免重复。由于System.Collections.Concurrent没有HashSet类,那么获取类似功能但具有并发性的推荐方法是什么?


2
不,那绝对是错误的,它只是锁定了对字典的访问,而不是返回值的任何属性和方法。我不确定你是怎么想到它可以这样做的。但是,如果每个键/值对只被一个线程访问(即使其他键/值对被其他线程访问),那么是安全的。 - Charlieface
我觉得我可能解释得不够清楚。嵌套类本质上是一个私有类,只能通过父类上的方法访问,而父类持有字典。在这种情况下,这个实现是否合适呢?因为字典需要线程等待,直到它们可以查找到字典时才能访问嵌套类。 - Efie
就像我之前说的一样,这取决于不同的线程是否访问相同的键/值。如果是的话,那么另一个线程可以对你的私有对象进行不安全的操作。如果不是的话,那么就是安全的。在你的情况下,哪个是真实的呢? - Charlieface
1个回答

6
我的想法是只要最高级别的集合被配置为并发/线程安全,就不必在意嵌套集合是否也是如此,因为数据将被高级别的集合锁定。
这个假设是不正确的。
假设您创建了一个包含许多常规哈希表的并发字典:
using namespace System.Collections.Concurrent

# Create thread-safe dictionary
$rootDict = [ConcurrentDictionary[string,hashtable]]::new()
  • $rootDict现在是线程安全的 - 不能有多个线程同时通过覆盖哈希表引用来修改'A'条目。
  • 我们添加到$rootDict的任何内部哈希表都不是线程安全的 - 它仍然只是一个普通的哈希表。

在PowerShell 7中,当使用ForEach-Object -Parallel操作此类数据结构时,可以观察到这一点:

using namespace System.Collections.Concurrent

# Create thread-safe dictionary
$rootDict = [ConcurrentDictionary[string,hashtable]]::new()

1..100 |ForEach-Object -Parallel {
  # We need a reference to our safe top-level dictionary
  $dict = $using:rootDict

  # ... and we need a key
  $rootKey = $_ % 2 -eq 0 ? 'even' : 'odd'

  # Thread-safe acquisition of inner hashtable
  $innerDict = $dict.GetOrAdd($rootKey, {param($key) return @{}})

  # Add a bit of jitter for realism
  Start-Sleep -Milliseconds (Get-Random -Minimum 50 -Maximum 250)

  # Update inner hashtable entry
  $innerDict['Counter'] += 1
} -ThrottleLimit 10

# Are these really the results we're expecting...? 
$rootDict['odd','even']

如果内部哈希表条目在同时更新时是线程安全的,您可以期望两个计数器都为 50 ,但是我在我的笔记本电脑上得到了如下结果:
Name                           Value
----                           -----
Counter                        46
Counter                        43

我们可以看到,在该过程中,内部“Counter”条目的多个更新被丢失,这可能是由于并发更新所致。


为了测试这个假设,让我们进行同样的实验,但使用另一种并发字典类型代替哈希表:

using namespace System.Collections.Concurrent

# Create thread-safe dictionary with a thread-safe item type
$rootDict = [ConcurrentDictionary[string,ConcurrentDictionary[string,int]]]::new()

1..100 |ForEach-Object -Parallel {
  # We need a reference to our safe top-level dictionary
  $dict = $using:rootDict

  # ... and we need a key
  $rootKey = $_ % 2 -eq 0 ? 'even' : 'odd'

  # Thread-safe acquisition of inner hashtable
  $innerDict = $dict.GetOrAdd($rootKey, {param($key) return @{}})

  # Add a bit of jitter for realism
  Start-Sleep -Milliseconds (Get-Random -Minimum 50 -Maximum 250)

  # Thread-safe update of inner dictionary
  [void]$innerDict.AddOrUpdate('Counter', {param($key) return 1}, {param($key,$value) return $value + 1})
} -ThrottleLimit 10

# These should be the exact results we're expecting! 
$rootDict['odd','even']

现在我明白了:
Key     Value
---     -----
Counter    50
Counter    50

我有一些自定义的类扩展了HashSet以避免重复。由于System.Collections.Concurrent中没有HashSet类,那么获取类似功能但具备并发性的推荐方法是什么?
我强烈建议你不要直接继承HashSet,而是将HashSet进行包装,并使用ReaderWriterLockSlim来保护你想向用户公开的所有方法。这样,你就可以在不不必要地牺牲读取访问性能的情况下实现线程安全。
这里,以[int]作为示例数据类型:
using namespace System.Collections.Generic
using namespace System.Threading

class ConcurrentSet
{
    hidden [ReaderWriterLockSlim]
    $_lock

    hidden [HashSet[int]]
    $_set

    ConcurrentSet()
    {
        $this._set = [HashSet[int]]::new()
        $this._lock = [System.Threading.ReaderWriterLockSlim]::new()
    }

    [bool]
    Add([int]$item)
    {
        # Any method that modifies the set should be guarded
        # by a WriteLock - guaranteeing exclusive update access
        $this._lock.EnterWriteLock()
        try{
            return $this._set.Add($item)
        }
        finally{
            $this._lock.ExitWriteLock()
        }
    }

    [bool]
    IsSubsetOf([IEnumerable[int]]$other)
    {
        # For the read-only methods a read-lock will suffice
        $this._lock.EnterReadLock()
        try{
            return $this._set.IsSubsetOf($other)
        }
        finally{
            $this._lock.ExitReadLock()
        }
    }

    # Repeat appropriate lock pattern for all [HashSet] methods you want to expose
}

您可以通过将一个 HashSet<object> 包装并使用自定义比较器来控制其行为,从而使包装更加灵活。


是的,我明白你的观点。我假设在嵌套的键/值对上完成操作之前,锁定不会解除,这是一个错误的假设。 - Efie
@Efie 关于自定义并发集合类型,请查看我回答的更新部分。 - Mathias R. Jessen
太棒了!非常感谢你提供的额外例子。 - Efie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接