我在F#中使用异步工作流和代理工作了很多,而我在事件中更深入一点,我注意到事件< _>()类型不是线程安全的。在这里,我不是在谈论举办活动的常见问题。我实际上是在谈论订阅和删除/处理事件。出于测试目的,我编写了这个简短的程序
let event = Event<int>()
let sub = event.Publish
[<EntryPoint>]
let main argv =
let subscribe sub x = async {
let mutable disposables = []
for i=0 to x do
let dis = Observable.subscribe (fun x -> printf "%d" x) sub
disposables <- dis :: disposables
for dis in disposables do
dis.Dispose()
}
Async.RunSynchronously(async{
let! x = Async.StartChild (subscribe sub 1000)
let! y = Async.StartChild (subscribe sub 1000)
do! x
do! y
event.Trigger 1
do! Async.Sleep 2000
})
0
程序很简单,我创建了一个事件和一个向它订阅特定数量事件的函数,然后处理每个处理程序。我使用另一个异步计算来使用Async.StartChild生成这些函数的两个实例。两个函数完成后,我触发事件以查看是否还有一些处理程序。
但是当调用event.Trigger(1)
时,结果是仍有一些处理程序被重新命名为事件。作为一些&#34; 1&#34;将打印到控制台。这通常意味着订阅和/或处置不是线程安全的。
这就是我没想到的。如果订阅和Disposing不是线程安全的,那么一般情况下如何安全地使用事件?肯定的事件也可以在线程之外使用,并且触发器不会并行或在不同的线程上产生任何函数。但是对我来说,事件在Async,基于代理的代码中使用或者通常与Threads一起使用在某种程度上是正常的。它们通常用作收集Backroundworker线程信息的通信。使用Async.AwaitEvent,可以订阅一个事件。如果订阅和Disposing不是线程安全的,那么在这样的环境中如何使用事件呢? Async.AwaitEvent的目的是什么?考虑到Async工作流确实线程希望只使用Async.AwaitEvent基本上是&#34;被设计打破&#34;如果默认情况下订阅/处置事件不是线程安全的。
我面临的一般问题是。订阅和处置不是线程安全的吗?从我的例子来看,它似乎看起来像,但可能我错过了一些重要的细节。我目前在我的设计中经常使用Event,我通常有MailboxProcessors并使用事件进行通知。所以问题是。如果事件不是线程安全的,那么我目前使用的整个设计根本不是线程安全的。那么这种情况有什么解决方法呢?创建一个全新的线程安全事件实现?他们是否已经存在一些面临这个问题的实现?或者是否有其他选项可以在高度线程化的环境中安全地使用Event?
答案 0 :(得分:4)
FYI; Event<int>
的实施可以找到here。
有趣的一点似乎是:
member e.AddHandler(d) =
x.multicast <- (System.Delegate.Combine(x.multicast, d) :?> Handler<'T>)
member e.RemoveHandler(d) =
x.multicast <- (System.Delegate.Remove(x.multicast, d) :?> Handler<'T>)
订阅事件将当前事件处理程序与传递给subscribe的事件处理程序组合在一起。这个组合的事件处理程序替换了当前的事件处理程序。
从并发性角度来看问题是,在这里我们有一个竞争条件,即并发订阅者可能使用来当前事件处理程序来组合,并且“最后一个”回写处理程序获胜(最后是一个困难的概念)这些天在并发中,但是nvm)。
这里可以做的是使用Interlocked.CompareAndExchange
引入CAS循环,但会增加伤害非并发用户的性能开销。这可以让PR关闭,看看它是否被F#社区看好。
写下关于如何处理的第二个问题我可以说出我会做什么。我会选择创建支持受保护订阅/取消订阅的FSharpEvent
版本。如果您的公司FOSS政策允许,可能以FSharpEvent
为基础。如果它成功了,那么它可以形成F#核心库的未来PR。
我不知道你的要求,但是如果你需要的是协程(即异步)而不是线程,那么也可以重写程序只使用1个线程,因此你不会受此影响竞争条件。
答案 1 :(得分:2)
首先,感谢FuleSnable的回答。他指出了正确的方向。根据他提供的信息,我自己实施了ConcurrentEvent
类型。此类型使用Interlocked.CompareExchange
添加/删除其处理程序,因此它是无锁的,并且希望是最快的方法。我通过从F#编译器复制Event
类型开始实现。 (我也按原样留下评论)。目前的实现看起来像这样。
type ConcurrentEvent<'T> =
val mutable multicast : Handler<'T>
new() = { multicast = null }
member x.Trigger(arg:'T) =
match x.multicast with
| null -> ()
| d -> d.Invoke(null,arg) |> ignore
member x.Publish =
// Note, we implement each interface explicitly: this works around a bug in the CLR
// implementation on CompactFramework 3.7, used on Windows Phone 7
{ new obj() with
member x.ToString() = "<published event>"
interface IEvent<'T>
interface IDelegateEvent<Handler<'T>> with
member e.AddHandler(d) =
let mutable exchanged = false
while exchanged = false do
System.Threading.Thread.MemoryBarrier()
let dels = x.multicast
let newDels = System.Delegate.Combine(dels, d) :?> Handler<'T>
let result = System.Threading.Interlocked.CompareExchange(&x.multicast, newDels, dels)
if obj.ReferenceEquals(dels,result) then
exchanged <- true
member e.RemoveHandler(d) =
let mutable exchanged = false
while exchanged = false do
System.Threading.Thread.MemoryBarrier()
let dels = x.multicast
let newDels = System.Delegate.Remove(dels, d) :?> Handler<'T>
let result = System.Threading.Interlocked.CompareExchange(&x.multicast, newDels, dels)
if obj.ReferenceEquals(dels,result) then
exchanged <- true
interface System.IObservable<'T> with
member e.Subscribe(observer) =
let h = new Handler<_>(fun sender args -> observer.OnNext(args))
(e :?> IEvent<_,_>).AddHandler(h)
{ new System.IDisposable with
member x.Dispose() = (e :?> IEvent<_,_>).RemoveHandler(h) } }
关于设计的一些注意事项:
至少在我的测试中,添加/删除处理程序现在似乎是线程安全的。 ConcurrentEvent
可以根据需要与Event
类型进行交换。如果某人对正确性或性能有其他改进,欢迎提出这些意见。否则我会将此答案标记为解决方案。
基准如果人们对ConcurrentEvent
与Event
let stopWatch () = System.Diagnostics.Stopwatch.StartNew()
let event = Event<int>()
let sub = event.Publish
let cevent = ConcurrentEvent<int>()
let csub = cevent.Publish
let subscribe sub x = async {
let mutable disposables = []
for i=0 to x do
let dis = Observable.subscribe (fun x -> printf "%d" x) sub
disposables <- dis :: disposables
for dis in disposables do
dis.Dispose()
}
let sw = stopWatch()
Async.RunSynchronously(async{
// Amount of tries
let tries = 10000
// benchmarking Event subscribe/unsubscribing
let sw = stopWatch()
let! x = Async.StartChild (subscribe sub tries)
let! y = Async.StartChild (subscribe sub tries)
do! x
do! y
sw.Stop()
printfn "Event: %O" sw.Elapsed
do! Async.Sleep 1000
event.Trigger 1
do! Async.Sleep 2000
// benchmarking ConcurrentEvent subscribe/unsubscribing
let sw = stopWatch()
let! x = Async.StartChild (subscribe csub tries)
let! y = Async.StartChild (subscribe csub tries)
do! x
do! y
sw.Stop()
printfn "\nConcurrentEvent: %O" sw.Elapsed
do! Async.Sleep 1000
cevent.Trigger 1
do! Async.Sleep 2000
})
在我的系统上,使用非线程安全的Event
订阅/取消订阅10,000个处理程序需要1.4 seconds
才能完成。
线程安全的ConcurrentEvent
需要1.8 seconds
才能完成。所以我认为开销很低。