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

Wil*_*den 10 c# events multithreading 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

并提出它:

SomeEvent.Raise(this, new FooEventArgs());
Run Code Online (Sandbox Code Playgroud)

在阅读过Jon Skeet 关于多线程文章之后,我试图将他提倡的方法封装在像这样的扩展方法中引发事件(使用类似的通用版本):

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后可以这样调用:

protected virtual void OnSomeEvent(EventArgs e)
{
    this.someEvent.Raise(this.eventLock, this, e);
}
Run Code Online (Sandbox Code Playgroud)

这样做有什么问题吗?

另外,我首先对锁的必要性感到有些困惑.据我所知,委托被复制到文章的示例中,以避免在null检查和委托调用之间更改(并变为null)的可能性.但是,我认为这种访问/分配是原子的,为什么锁是必要的呢?

更新:关于Mark Simpson在下面的评论,我总结了一个测试:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

这输出:

Foo
Bar

Foo
Bar
Run Code Online (Sandbox Code Playgroud)

这说明method(action)的delegate参数不会镜像传递给它的参数(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);
    }
}
Run Code Online (Sandbox Code Playgroud)

Aar*_*ght 7

锁定的目的是在覆盖默认事件连线时保持线程安全.如果其中一些内容解释了你已经能够从Jon的文章中推断出的东西,那就道歉了; 我只是想确保我对一切都完全清楚.

如果你宣布你的事件是这样的:

public event EventHandler Click;
Run Code Online (Sandbox Code Playgroud)

然后订阅该事件将自动与a同步lock(this).你不是不需要写任何特殊的锁定代码来调用事件处理程序.写完是完全可以接受的:

var clickHandler = Click;
if (clickHandler != null)
{
    clickHandler(this, e);
}
Run Code Online (Sandbox Code Playgroud)

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

public event EventHandler Click
{
    add { click += value; }
    remove { click -= value; }
}
Run Code Online (Sandbox Code Playgroud)

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

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

就我个人而言,我并不担心这一点,但乔恩的理由是合理的.但是,我们确实有一个问题.如果您使用私有EventHandler字段来存储您的事件,那么您可能拥有执行此操作的类的内部代码:

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

这很糟糕,因为我们访问相同的私有存储字段而不使用属性使用的相同锁.

如果该类外部的一些代码:

MyControl.Click += MyClickHandler;
Run Code Online (Sandbox Code Playgroud)

通过公共财产的外部代码正在兑现锁定.但你不是,因为你正在触摸私人领域.

变量赋值的一部分clickHandler = _click是原子的,是的,但分配时,该_click字段可能处于过渡状态,一个一个已经由外部类半写的.当您同步对字段的访问时,仅仅同步写访问权限是不够的,您还必须同步读取访问权限:

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

UPDATE

事实证明,围绕评论的一些对话实际上是正确的,正如OP的更新所证明的那样.这不是扩展方法本身的问题,它是委托具有值类型语义并在赋值时被复制的事实.即使您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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这个版本有效,因为我们实际上并没有传递它EventHandler,只是对它的引用(注意ref方法签名中).遗憾的是,您不能在扩展方法中使用refthis,因此它必须保持纯静态方法.

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