【问题标题】:Is there a thread-safe way to copy a decorated object's event handlers?是否有一种线程安全的方法来复制装饰对象的事件处理程序?
【发布时间】:2019-12-10 13:00:47
【问题描述】:

我正在编写一个 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);
    }
}

【问题讨论】:

  • 可以在类代码中进行分配。虽然外部代码只能添加和删除事件处理程序 - 通过(隐藏的)添加/删除函数/类似属性的构造 - 类代码可以完全访问支持变量,包括赋值。

标签: c# events thread-safety decorator


【解决方案1】:

执行此操作的正常方法是在 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 的使用似乎是线程安全的,请参阅C# Is it thread safe to subscribe Same event handler for all Objects。此外,由于事件处理程序是按顺序调用的,因此事件处理程序相互干扰(至少不会直接)的风险不大。

【讨论】:

  • 我希望避免锁定对 Bar() 的每个请求(或每个事件调用),因为这需要特别高效。不幸的是,引入线程争用的东西是行不通的。虽然我认为这种跟踪事件的方法与读写锁相结合可能就足够了。我会试一试,如果我确认它有效,我会将其标记为答案。
  • Interlocked 的使用只是为了进行线程安全交换,而不是全锁。
  • 如果你想让多个任务调用装饰的 Bar() 那么你真的别无选择,只能使用锁。话虽如此,Semaphore Slim 还是相当轻量级的。
  • 我上面提到的设计的一个缺陷是它使包裹的 IFoo 暴露在外。问题在于它意味着另一个调用者可以在没有锁定的情况下调用它的 Bar() 方法,这将是不安全的,因为还有通过装饰器进入的调用。更好的设计是让 BasicFoo 私有并由装饰者分配。为了处理需要拥有各种派生类的需求,您可以将工厂 Func 传递给构造函数。
猜你喜欢
  • 1970-01-01
  • 2016-02-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多