我正在开发一个使用Dictionary作为后备存储的线程安全集合。
在C#中,您可以执行以下操作:
private IEnumerable<KeyValuePair<K, V>> Enumerate() {
if (_synchronize) {
lock (_locker) {
foreach (var entry in _dict)
yield return entry;
}
} else {
foreach (var entry in _dict)
yield return entry;
}
}
我发现在F#中执行此操作的唯一方法是使用Monitor,例如:
let enumerate() =
if synchronize then
seq {
System.Threading.Monitor.Enter(locker)
try for entry in dict -> entry
finally System.Threading.Monitor.Exit(locker)
}
else seq { for entry in dict -> entry }
可以使用锁定功能完成吗?或者,有没有更好的方法来做到这一点?我不认为返回集合的副本以进行迭代将起作用,因为我需要绝对同步。
答案 0 :(得分:4)
我认为你不能用锁定功能做同样的事情,因为你会试图从内部屈服。话虽如此,这看起来在任何一种语言中都是一种危险的方法,因为它意味着锁可以保持任意的时间(例如,如果一个线程调用Enumerate()
但是没有一直枚举通过结果IEnumerable<_>
,然后锁将继续保持。)
反转逻辑更有意义,提供iter
方法:
let iter f =
if synchronize then
lock locker (fun () -> Seq.iter f dict)
else
Seq.iter f dict
这会使迭代回到您的控制之下,确保序列完全迭代(假设f
没有阻塞,这在任何情况下都是必要的假设)并且此后立即释放锁定
修改强>
这是一个可以永久保存锁的代码示例。
let cached = enumerate() |> Seq.cache
let firstFive = Seq.take 5 cached |> Seq.toList
我们已经锁定了以开始枚举前5个项目。但是,我们没有继续执行序列的其余部分,因此锁定将不会被释放(也许我们稍后会根据用户反馈或其他内容枚举其余部分,在这种情况下,最终会释放锁定)
在大多数情况下,正确编写的代码将确保它处理原始枚举器,但一般情况下无法保证。因此,您的序列表达式应设计为仅对枚举部分方式具有鲁棒性。如果您打算要求您的调用者一次枚举该集合,那么强制它们传递给每个元素应用的函数比返回他们可以随意枚举的序列更好。
答案 1 :(得分:4)
我同意kvb代码是可疑的,并且您可能不希望持有锁。但是,有一种方法可以使用use
关键字以更舒适的方式编写锁定。值得一提的是,因为它在其他情况下可能会有用。
您可以编写一个开始持有锁并返回IDisposable
的函数,该函数在处理时释放锁定:
let makeLock locker =
System.Threading.Monitor.Enter(locker)
{ new System.IDisposable with
member x.Dispose() =
System.Threading.Monitor.Exit(locker) }
然后你可以写例如:
let enumerate() = seq {
if synchronize then
use l0 = makeLock locker
for entry in dict do
yield entry
else
for entry in dict do
yield entry }
这实际上是使用lock
关键字实现C#,如use
,它具有类似的属性(允许您在离开范围时执行某些操作)。因此,这更接近原始C#版本的代码。