有没有一种线程安全的方法来复制装饰对象的事件处理程序?

时间:2019-08-01 21:42:20

标签: c# events thread-safety decorator

我正在编写一个C#类,该类装饰可以引发事件的对象。装饰器可以实例化一个新的装饰对象,并交换旧的对象,以响应在任何使用线程上发生的异步事件。在初始化新的装饰对象时,我需要将所有事件处理程序从旧的装饰对象添加到新的装饰对象,并继续从两个对象添加/删除处理程序,直到发生交换。是否存在解决此问题的通用解决方案?

这是正在发生的事情的概念性示例:

interface IFoo
{
    event Action Barred;

    void Bar();
}

class BasicFoo : IFoo
{
    public event Action Barred;
    public void Bar()
    {
        Console.WriteLine("Barring");
        Barred?.Invoke();
    }
}

class DecoratedFoo : IFoo
{
    private IFoo _Decorated;

    public DecoratedFoo()
    {
        _Decorated = new BasicFoo();
    }

    public event Action Barred
    {
        add => _Decorated.Barred += value;
        remove => _Decorated.Barred -= value;
    }

    public void Bar() => _Decorated.Bar();

    public void SwapDecoratedFoo()
    {
        // Can occur at any time from any thread.

        var newFoo = new BasicFoo();

        /* 
         * How to reassign events from _Decorated to newFoo, and in a way
         * that's thread safe while _Decorated.Barred may still be adding
         * or removing handlers while the swap is occurring?
         */

        Interlocked.Exchange(ref _Decorated, newFoo);
    }
}

1 个答案:

答案 0 :(得分:0)

执行此操作的正常方法是在Decorated包装器中拥有您自己的事件,并在包装​​的(基本)对象的事件触发时调用该事件。您还需要除Interlocked之外的另一种锁定,因为Interlocked不足以将多个语句组合在一起。您可以使用SemaphoreSlim(1,1)进行操作,如下所示:

interface IFoo
{
    event Action Barred;
    void Bar();
}

class BasicFoo : IFoo
{
    public event Action Barred;
    public void Bar() { … }
}

class DecoratedFoo : IFoo
{
    public DecoratedFoo(IFoo foo = null) => this.Decorated = foo;
    public event Action Barred;

    public void Bar()
    {
        try {
            this._lock.Wait();
            this._decorated?.Bar();
        } finally {
            this._lock.Release();
        }
    }
    private SemaphoreSlim _lock = new SemaphoreSlim(1, 1);

    public IFoo Decorated {
        get {
            try {
                this._lock.Wait();
                return this._decorated;
            } finally {
                this._lock.Release();
            }
        }
        set {
            try {
                this._lock.Wait();
                if (this._decorated != null) {
                    this._decorated.Barred -= this.OnDecoratedBarred;
                }
                this._decorated = value;
                if (this._decorated != null) {
                    this._decorated.Barred += this.OnDecoratedBarred;
                }
            } finally {
                this._lock.Release();
            }
        }
    }
    private IFoo _decorated = null;

    private void OnDecoratedBarred() => this.Barred?.Invoke();
}

对此有一个评论,设计假定IFoo.Bar()不回调用户代码。如果确实如此,那么当/当用户代码调用DecoratedFoo.Bar()时,死锁的可能性就很高。在这种情况下,应该使设计异步以防止死锁。

使用this.Barred似乎是线程安全的,请参见Maven repo。另外,由于事件处理程序是按顺序调用的,因此事件处理程序不会(至少不会直接)相互干扰的风险很小。