Events 在多线程代码中使用F#事件和异步
我经常在F#中使用异步工作流和代理。当我深入了解事件时,我注意到Event()类型不是线程安全的 在这里,我不是在谈论举办活动的常见问题。我实际上是说订阅和从事件中删除/处理。出于测试目的,我编写了以下简短程序:Events 在多线程代码中使用F#事件和异步,events,asynchronous,f#,thread-safety,observable,Events,Asynchronous,F#,Thread Safety,Observable,我经常在F#中使用异步工作流和代理。当我深入了解事件时,我注意到Event()类型不是线程安全的 在这里,我不是在谈论举办活动的常见问题。我实际上是说订阅和从事件中删除/处理。出于测试目的,我编写了以下简短程序: let event=event() let sub=event.Publish [] 让主argv= 让subscribe x=async{ 让可变一次性物品=[] 对于i=0到x do 让dis=Observable.subscribe(乐趣x->printf“%d”x)子 可处置的
let event=event()
let sub=event.Publish
[]
让主argv=
让subscribe x=async{
让可变一次性物品=[]
对于i=0到x do
让dis=Observable.subscribe(乐趣x->printf“%d”x)子
可处置的FYI;可以找到事件
的实现
有趣的是:
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>)
成员e.AddHandler(d)=
x、 多播处理程序)
订阅事件将当前事件处理程序与传递到订阅中的事件处理程序组合。此组合事件处理程序将替换当前事件处理程序
从并发性的角度来看,问题在于我们这里有一个竞争条件,并发订阅者可能会使用当前事件处理程序与“最后一个”事件处理程序相结合,而“最后一个”事件处理程序会写回处理程序win(最近在并发性方面,last是一个困难的概念,但nvm除外)
这里可以做的是引入一个CAS循环,使用interlocated.CompareAndExchange
,但这会增加性能开销,从而伤害非并发用户。这是一个可以让人做一个PR,看看F#社区是否看好它的东西
关于你的第二个问题,我可以说我会怎么做。我会选择创建一个支持受保护订阅/取消订阅的FSharpEvent
版本。如果你公司的自由和开放源码软件政策允许,也许可以以FSharpEvent
为基础。如果成功,那么它可能会形成未来的公关到核心图书馆
我不知道您的需求,但如果您需要的是协同路由(即异步),也有可能而不是线程,那么就可以重写程序,只使用一个线程,这样您就不会受到这种竞争条件的影响。首先,多亏了Fulesable的回答。他为我指出了正确的方向。根据他提供的信息,我自己实现了一个ConcurrentEvent
类型。这种类型使用Interlocked.CompareExchange
用于添加/删除其处理程序,因此它是无锁的,希望是最快的方法
我通过从F#编译器复制事件
类型来启动实现。(我也保留注释原样。)当前实现如下所示:
类型ConcurrentEvent
new()={multicast=null}
成员x.Trigger(参数:'T)=
将x.multicast与
|空->()
|d->d.Invoke(null,arg)|>忽略
成员十:出版=
//注意,我们显式地实现了每个接口:这解决了CLR中的一个bug
//CompactFramework 3.7上的实现,用于Windows Phone 7
{new obj()与
成员x.ToString()=“”
接口IEvent>与
成员e.AddHandler(d)=
设可变值=false
交换时=假
System.Threading.Thread.MemoryBarrier()
设dels=x
让newDels=System.Delegate.Combine(dels,d):?>Handler
让结果=System.Threading.Interlocated.CompareExchange(&x.multicast,newDels,dels)
如果对象引用等于(dels,result),则
交换为您的最后一句话。每个异步都是以async启动的。Start在线程池上运行,但也可以使用let!或do!将异步切换到另一个线程,例如仅使用“async.Sleep”切换到线程池或使用Async.StartChild。在顶部,我从MailboxProcessor触发事件,并且这些事件也总是在线程池上运行一些线程。因此,我不知道如何使所有线程都在单个线程上运行。但我也不希望出现这种行为。我希望出现这种行为,因为我不希望在所有线程上运行所有线程一个单一的线程。看来,事件的线程安全性是F.Y.Cype库的一个重要问题。您会考虑为此打开一个问题吗?您可以滚动自己的协同程序,或者确保每个<代码>让!<代码> >代码> DO!/<代码>返回到“主线程”。.但正如您所说,您无论如何都不希望出现这种行为,所以这无关紧要。需要考虑的是:我不确定compareeexchange
是否插入了内存屏障(或它是什么类型的),或者您必须手动设置内存屏障。似乎compareeexchange
意味着完全屏障().AFAIK x86 reads使用acquire
屏障,因此这应该意味着您在x86上很好,但如果您希望代码以ARM/PowerPC为目标,则在读取x.multicast
@FuleSnabel时可能需要插入互锁的调用。无论如何,我犯了一个错误,因为我使用了“x.multicast”在Delegate.Combined
和Delegate.Remove
中。该操作应该更好地处理获取的dels
变量。据我所知,在读取x.multicast
之前,我只需要一个内存载体,以确保x.multicast
的新鲜度。另外,编写的数据也可以合并
必须在被另一个线程读取之前可见。@FuleSnabelCombine
和Remove
写入局部变量。因此不需要内存屏障。我想你的意思可能是在compareeexchange
之后?如果确实需要内存载体,我仍然在读取。