F# 锁函数能否用于实现线程安全枚举?
我正在开发一个线程安全的集合,它使用字典作为备份存储 在C中,您可以执行以下操作:F# 锁函数能否用于实现线程安全枚举?,f#,locking,thread-safety,enumeration,F#,Locking,Thread Safety,Enumeration,我正在开发一个线程安全的集合,它使用字典作为备份存储 在C中,您可以执行以下操作: private IEnumerable<KeyValuePair<K, V>> Enumerate() { if (_synchronize) { lock (_locker) { foreach (var entry in _dict) yield return entry; } }
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;
}
}
可以使用锁定功能完成此操作吗?或者,总的来说,有没有更好的方法?我不认为返回集合的副本进行迭代会起作用,因为我需要绝对同步。我不认为您可以使用lock函数做同样的事情,因为您将试图从它内部进行让步。话虽如此,在这两种语言中,这看起来都是一种危险的方法,因为这意味着锁可以被持有任意时间,例如,如果一个线程调用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项,我们使用了锁。但是,我们没有继续完成序列的其余部分,因此锁不会被释放。也许我们稍后会根据用户反馈或其他什么来列举剩余部分,在这种情况下,锁最终会被释放
在大多数情况下,正确编写的代码将确保它处理原始枚举数,但通常无法保证这一点。因此,序列表达式应该设计为只以部分方式枚举的健壮性。如果您打算要求调用者一次枚举所有集合,那么强制他们向您传递要应用于每个元素的函数比返回一个他们可以随意枚举的序列要好。我不认为您可以使用lock函数做同样的事情,因为你会试图从内心屈服。话虽如此,在这两种语言中,这看起来都是一种危险的方法,因为这意味着锁可以被持有任意时间,例如,如果一个线程调用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项,我们使用了锁。但是,我们没有继续完成序列的其余部分,因此锁不会被释放。也许我们稍后会根据用户反馈或其他什么来列举剩余部分,在这种情况下,锁最终会被释放
在大多数情况下,正确编写的代码将确保它处理原始枚举数,但通常无法保证这一点。因此,序列表达式应该设计为只以部分方式枚举的健壮性。如果您打算要求调用者一次枚举所有集合,那么强制他们向您传递要应用于每个元素的函数比返回一个他们可以随意枚举的序列要好。我同意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 }
这本质上是使用use关键字实现类似于C的锁,它具有类似的属性,允许您在离开作用域时执行某些操作。因此,这与代码的原始C版本非常接近。我同意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 }
这本质上是使用use关键字实现类似于C的锁,它具有类似的属性,允许您在离开作用域时执行某些操作。因此,这与代码的原始C版本非常接近。I
同意@kvb,原始代码/设计充其量感觉可疑。我同意@kvb,原始代码/设计充其量感觉可疑。很好的建议。谢谢我很好奇,一个没有完全枚举集合的线程怎么能保持锁,除非你是说调用thread.sleep之类的东西在for循环中需要很长时间。否则,它似乎会跳出循环,调用枚举数上的Dispose,然后释放锁。@Daniel-我添加了一个不释放锁的示例。如果调用代码总是使用for循环,您可能是安全的,但是还有其他方法可以使用IEnumerables。谢谢您的额外解释。很好的建议。谢谢我很好奇,一个没有完全枚举集合的线程怎么能保持锁,除非你是说调用thread.sleep之类的东西在for循环中需要很长时间。否则,它似乎会跳出循环,调用枚举数上的Dispose,然后释放锁。@Daniel-我添加了一个不释放锁的示例。如果调用代码总是使用for循环,您可能是安全的,但是还有其他方法可以使用IEnumerables。谢谢您的额外解释。