我正在使用一个对象(EventReceiver
),它将成员注册到通过ctor导入的对象(EventSource
)的事件。EventReceiver
实现了IDisposable
,并取消了EventSource
的订阅。
问题是有不同的线程调用事件处理程序并处置EventReceiver
。取消订阅完成后将调用该事件。在事件引发和取消订阅之间存在竞争条件。
如何解决这个问题?
下面的示例实现演示了这个问题:
internal class Program
{
private static void Main(string[] args)
{
var eventSource = new EventSource();
Task.Factory.StartNew(
() =>
{
while (true)
{
eventSource.RaiseEvent();
}
});
Task.Factory.StartNew(
() =>
{
while (true)
{
new EventReceiver(eventSource).Dispose();
}
});
Console.ReadKey();
}
}
public class EventSource
{
public event EventHandler<EventArgs> SampleEvent;
public void RaiseEvent()
{
var handler = this.SampleEvent;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
}
public class EventReceiver : IDisposable
{
private readonly EventSource _source;
public EventReceiver(EventSource source)
{
this._source = source;
this._source.SampleEvent += this.OnSampleEvent;
}
public bool IsDisposed { get; private set; }
private void OnSampleEvent(object sender, EventArgs args)
{
if (this.IsDisposed)
{
throw new InvalidOperationException("This should never happen...");
}
}
public void Dispose()
{
this._source.SampleEvent -= this.OnSampleEvent;
this.IsDisposed = true;
}
}
在多核处理器上,程序启动后几乎会直接抛出异常。是的,我知道var handler = this.SampleEvent
将创建事件处理程序的副本,这导致了问题。
我试图实现RaiseEvent
方法,但它没有帮助:
public void RaiseEvent()
{
try
{
this.SampleEvent(this, EventArgs.Empty);
}
catch (Exception)
{
}
}
问题是:如何在多线程方式下实现线程安全的事件注册和取消注册?
我的期望是取消注册将被暂停,直到当前触发的事件已经完成(也许这只能使用第二个实现)。但我很失望。
如果您有两个线程—一个调用事件处理程序,另一个取消订阅—那么总是是一个竞争条件,您将以以下顺序结束:
- event -raiser开始引发事件
- Event-unsubscriber取消订阅
- Event-raiser执行handler
在。net中,这是不可避免的委托的不变性。即使使用某种线程安全的可变委托类型,也总会有一个细粒度的竞争条件:
- event -raiser开始引发事件
- Event-raiser开始执行处理程序-但它还没有到达用户代码的第一行
- Event-unsubscriber取消订阅 处理程序执行用户代码
更糟的是,您可以在处理程序正在执行时取消订阅——您希望发生什么呢?您希望处理程序代码被任意终止吗?
您可以轻松地在处理程序中实现自己对"我是否真的要在此时运行"的检查,但您需要制定自己的语义。可以使用锁来确保在处理程序执行时处理程序取消订阅不会发生,但这对我来说感觉相当危险,除非您知道您的事件处理程序将在短时间内完成。