c# - 这是在 C# 中引发事件的有效模式吗?

标签 c# multithreading events locking thread-safety

更新:为了阅读本文的任何人的利益,自 .NET 4 以来,由于自动生成事件的同步发生变化,因此不需要锁定,所以我现在只使用它:

public static void Raise<T>(this EventHandler<T> handler, object sender, T e) where T : EventArgs
{
    if (handler != null)
    {
        handler(sender, e);
    }
}

并提高它:

SomeEvent.Raise(this, new FooEventArgs());

一直在阅读 Jon Skeet 的一本书 articles on multithreading ,我试图封装他提倡的在扩展方法中引发事件的方法(具有类似的通用版本):

public static void Raise(this EventHandler handler, object @lock, object sender, EventArgs e)
{
    EventHandler handlerCopy;
    lock (@lock)
    {
        handlerCopy = handler;
    }

    if (handlerCopy != null)
    {
        handlerCopy(sender, e);
    }
}

然后可以这样调用:

protected virtual void OnSomeEvent(EventArgs e)
{
    this.someEvent.Raise(this.eventLock, this, e);
}

这样做有什么问题吗?

此外,我对锁的必要性一开始有点困惑。据我了解,在文章的示例中复制了委托(delegate),以避免它在 null 检查和委托(delegate)调用之间发生变化(并变为 null)的可能性。但是,我的印象是这种访问/分配是原子的,那么为什么需要锁呢?

更新:关于下面马克·辛普森的评论,我做了一个测试:

static class Program
{
    private static Action foo;
    private static Action bar;
    private static Action test;

    static void Main(string[] args)
    {
        foo = () => Console.WriteLine("Foo");
        bar = () => Console.WriteLine("Bar");

        test += foo;
        test += bar;

        test.Test();

        Console.ReadKey(true);
    }

    public static void Test(this Action action)
    {
        action();

        test -= foo;
        Console.WriteLine();

        action();
    }
}

这个输出:

Foo
Bar

Foo
Bar

这说明方法的委托(delegate)参数 (action) 没有反射(reflect)传递给它的参数 (test),这是意料之中的,我猜测。我的问题是这会影响我的 Raise 扩展方法上下文中锁的有效性吗?

更新:这是我现在使用的代码。它不像我希望的那样优雅,但它似乎有效:

public static void Raise<T>(this object sender, ref EventHandler<T> handler, object eventLock, T e) where T : EventArgs
{
    EventHandler<T> copy;
    lock (eventLock)
    {
        copy = handler;
    }

    if (copy != null)
    {
        copy(sender, e);
    }
}

最佳答案

锁的目的是在覆盖默认事件连接时保持线程安全。如果其中一些解释了您已经能够从 Jon 的文章中推断出的事情,我们深表歉意;我只是想确保我对所有事情都完全清楚。

如果你这样声明你的事件:

public event EventHandler Click;

然后事件订阅会自动与 lock(this) 同步。您不需要编写任何特殊的锁定代码来调用事件处理程序。完全可以这样写:

var clickHandler = Click;
if (clickHandler != null)
{
    clickHandler(this, e);
}

但是,如果您决定覆盖默认事件,即:

public event EventHandler Click
{
    add { click += value; }
    remove { click -= value; }
}

现在您遇到了问题,因为不再有隐式锁。您的事件处理程序刚刚失去了线程安全性。这就是你需要使用锁的原因:

public event EventHandler Click
{
    add
    {
        lock (someLock)      // Normally generated as lock (this)
        {
            _click += value;
        }
    }
    remove
    {
        lock (someLock)
        {
            _click -= value;
        }
    }
}

就我个人而言,我对此并不在意,但 Jon 的理由很充分。但是,我们确实有一个问题。如果您使用私有(private) EventHandler 字段来存储您的事件,您可能在类内部有执行此操作的代码:

protected virtual void OnClick(EventArgs e)
{
    EventHandler handler = _click;
    if (handler != null)
    {
        handler(this, e);
    }
}

这很糟糕,因为我们正在访问相同的私有(private)存储字段,而没有使用属性使用的相同锁

如果类外部的一些代码:

MyControl.Click += MyClickHandler;

通过公共(public)属性的外部代码正在兑现锁。但你不是,因为你接触的是私有(private)领域。

clickHandler = _click变量赋值部分是原子的,是的,但是在该赋值期间,_click 字段可能处于 transient 状态,由外部类编写了一半。当您同步对字段的访问时,仅同步写访问是不够的,您还必须同步读访问:

protected virtual void OnClick(EventArgs e)
{
    EventHandler handler;
    lock (someLock)
    {
        handler = _click;
    }
    if (handler != null)
    {
        handler(this, e);
    }
}

更新

事实证明,评论周围的一些对话实际上是正确的,OP 的更新证明了这一点。这不是扩展方法本身的问题,而是委托(delegate)具有值类型语义并在赋值时被复制的事实。即使您从扩展方法中取出 this 并将其作为静态方法调用,您也会得到相同的行为。

可以使用静态实用方法来绕过此限制(或功能,取决于您的观点),尽管我很确定您不能使用扩展方法。这是一个有效的静态方法:

public static void RaiseEvent(ref EventHandler handler, object sync,
    object sender, EventArgs e)
{
    EventHandler handlerCopy;
    lock (sync)
    {
        handlerCopy = handler;
    }
    if (handlerCopy != null)
    {
        handlerCopy(sender, e);
    }
}

此版本有效,因为我们实际上并未传递 EventHandler,只是对其的引用(请注意方法签名中的 ref ).不幸的是,您不能在扩展方法中将 refthis 一起使用,因此它必须保留为纯静态方法。

(如前所述,您必须确保传递与在公共(public)事件中使用的 sync 参数相同的锁定对象;如果传递任何其他对象,则整个讨论没有实际意义。)

关于c# - 这是在 C# 中引发事件的有效模式吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2123608/

相关文章:

c# - 事件支持 WP 8.1

c# - 是否有允许多个键的 Dictionary<string, object> 集合?

Python 线程并没有提高速度

c# - HttpHeaders.TryAddWithoutValidation 是否验证?

c# - 阻塞 ForEach 直到异步事件完成

c++ - 多线程/ fork 服务器守护进程的模型

events - 从 JQuery 中的元素获取点击处理程序

android - ExpandableListView 没有反应并收到任何点击

c# - 确保长时间运行的任务只触发一次,后续请求排队但队列中只有一个条目

c# - 在运行时创建动态 View asp.net mvc