【问题标题】:Event is called after unsubscription取消订阅后调用事件
【发布时间】:2014-05-19 11:47:41
【问题描述】:

我正在使用一个对象 (EventReceiver),它将成员注册到通过 ctor 导入的对象 (EventSource) 的事件中。 EventReceiverimplements 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)
    {
    }
}

问题是:如何以多线程的方式实现线程安全的事件注册和注销?

我的期望是取消注册将被暂停,直到当前触发的事件完成(也许这只能使用第二个实现)。但我很失望。

【问题讨论】:

  • 看起来OnSampleEventDispose 应该通过互斥锁同步。

标签: c# multithreading events event-handling


【解决方案1】:

如果您有两个线程 - 一个调用事件处理程序,一个取消订阅 - 将总是出现竞争条件,您最终会得到以下顺序:

  • 事件引发者开始引发事件
  • 事件退订者退订
  • 事件引发程序执行处理程序

由于 .NET 中委托的不变性,这是不可避免的。即使使用某种线程安全、可变的委托类型,也总会存在更细粒度的竞争条件:

  • 事件引发者开始引发事件
  • 事件引发程序开始执行处理程序 - 但尚未到达用户代码的第一行
  • 事件退订者退订
  • 处理程序执行用户代码

更糟糕的是,您可以在处理程序正在执行时取消订阅 - 您期望会发生什么?你会期望处理程序代码被任意终止吗?

您可以在处理程序中轻松实现自己的检查“我是否真的打算在此时运行”,但您需要制定自己的语义。您可以使用锁定来确保在处理程序执行时不会发生处理程序取消订阅,但这对我来说感觉很危险,除非您知道您的事件处理程序将在短时间内完成。

【讨论】:

  • 是的,你死定了——但我已经知道了。 This post 表明(取消)注册事件是线程安全的。但是为什么当不可能正确地引发事件时。我尝试使用锁来同步事件处理程序和取消订阅 - 但我仍在锁定以获得更好的解决方案。
  • @Verarind:您的选择实际上只是“锁定以避免并发取消订阅/执行”或“接受可能发生并发取消订阅/执行”。这不是“不可能正确地引发事件”的问题——而是根本上没有一个好的定义。
猜你喜欢
  • 2017-03-07
  • 1970-01-01
  • 2018-01-05
  • 1970-01-01
  • 2017-04-25
  • 1970-01-01
  • 1970-01-01
  • 2016-07-27
  • 2020-11-11
相关资源
最近更新 更多