在多线程代码

时间:2016-01-31 01:34:01

标签: events asynchronous f# thread-safety observable

我在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?

2 个答案:

答案 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) } }

关于设计的一些注意事项:

  • 我开始使用递归循环。但是这样做并查看编译后的代码,它创建了一个匿名类,并调用AddHandler或RemoveHandler创建了一个这样的对象。通过直接实现while循环,无论何时添加/删除新的Handler,都可以避免实例化对象。
  • 我明确使用了obj.ReferenceEquals来避免Generic Hash Equality。

至少在我的测试中,添加/删除处理程序现在似乎是线程安全的。 ConcurrentEvent可以根据需要与Event类型进行交换。如果某人对正确性或性能有其他改进,欢迎提出这些意见。否则我会将此答案标记为解决方案。

基准如果人们对ConcurrentEventEvent

的比较慢一点感到好奇
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才能完成。所以我认为开销很低。