【问题标题】:Are non-concurrent collections safe inside concurrent collections?并发集合中的非并发集合是否安全?
【发布时间】:2021-07-10 20:42:15
【问题描述】:

我希望开始在我正在处理的项目中实现一些并发功能。我最近发现了我打算利用的 System.Collections.Concurrent 命名空间。

我用来跟踪动作整体状态的对象本质上是一个包含一些嵌套自定义对象的字典。我的想法是,只要将最高级别的集合配置为并发/线程安全,嵌套集合是否是无关紧要的,因为数据将被更高级别的集合锁定。

这是正确的假设吗?

例如,PowerShell 中的以下内容是否可以安全使用?

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

此外,我有一些自定义类扩展 HashSet 以避免重复。由于 System.Collections。 Concurrent 没有 HashSet 类,有什么推荐的方式来获得类似的功能但同时呢?

【问题讨论】:

  • 不,这绝对不正确,它只锁定对字典的访问,而不锁定任何返回值的属性和方法。我不完全确定你认为它是如何做到的。但是如果每个键/值对只被一个线程访问(即使其他对被其他线程访问),那么它是安全的
  • 我想我可能解释得不好。嵌套类本质上是一个私有类,它只能通过包含字典的父类上的方法访问。在那种情况下,这是一个很好的实现,因为字典需要线程等待访问嵌套类,直到当字典可供它们使用时它们可以查找它?
  • 再次,正如我所说,这取决于不同的线程是否访问相同的键/值。如果是,那么没有什么可以阻止另一个线程对您的私有对象执行非线程安全的操作。如果没有,那将是安全的。在你的情况下,哪些是正确的?

标签: .net powershell collections concurrency thread-safety


【解决方案1】:

我的想法是,只要将最高级别的集合配置为并发/线程安全,嵌套集合是否是无关紧要的,因为数据将被更高级别的集合锁定。

这是正确的假设吗?

不,这是一个安全的假设。

假设您创建了一个包含一堆常规哈希表的并发字典:

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

我们可以看到内部“计数器”条目的多个更新在此过程中丢失,可能是由于并发更新。


为了验证这个假设,让我们做同样的实验,但使用另一种并发字典类型而不是哈希表:

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
}

您可以通过wrapping a HashSet<object> 使包装器更加灵活,并使用custom comparer 控制行为

【讨论】:

  • 是的,我明白你的意思。我假设在嵌套键/值对上的操作完成之前不会解除锁定,这是一个糟糕的假设。
  • @Efie 关于自定义并发集类型,请参阅我的答案的更新
  • 这太棒了。非常感谢您提供的其他示例
猜你喜欢
  • 1970-01-01
  • 2014-03-29
  • 1970-01-01
  • 2012-03-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-06
相关资源
最近更新 更多