Events 在多线程代码中使用F#事件和异步

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)子 可处置的

我经常在F#中使用异步工作流和代理。当我深入了解事件时,我注意到Event()类型不是线程安全的

在这里,我不是在谈论举办活动的常见问题。我实际上是说订阅和从事件中删除/处理。出于测试目的,我编写了以下简短程序:

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
的新鲜度。另外,
编写的数据也可以合并
必须在被另一个线程读取之前可见。@FuleSnabel
Combine
Remove
写入局部变量。因此不需要内存屏障。我想你的意思可能是在
compareeexchange
之后?如果确实需要内存载体,我仍然在读取。