C#: 线程安全事件

5
下面的实现是线程安全的吗?如果不是,我错过了什么?我应该在哪里使用volatile关键字?或者在OnProcessingCompleted方法中加锁?如果需要,在哪里加锁?
public abstract class ProcessBase : IProcess
{
    private readonly object completedEventLock = new object();

    private event EventHandler<ProcessCompletedEventArgs> ProcessCompleted;

    event EventHandler<ProcessCompletedEventArgs> IProcess.ProcessCompleted
    {
        add
        {
            lock (completedEventLock)
                ProcessCompleted += value;
        }
        remove
        {
            lock (completedEventLock)
                ProcessCompleted -= value;
        }
    }

    protected void OnProcessingCompleted(ProcessCompletedEventArgs e)
    {
        EventHandler<ProcessCompletedEventArgs> handler = ProcessCompleted;
        if (handler != null)
            handler(this, e);
    }
}

注意: 我有私有事件和显式接口的原因是因为它是一个抽象基类。继承它的类不应该直接操作那个事件。添加类包装器使其更加清晰。

2个回答

6

当获取处理程序时,您需要进行锁定,否则可能无法获得最新的值:

protected void OnProcessingCompleted(ProcessCompletedEventArgs e)
{
    EventHandler<ProcessCompletedEventArgs> handler;
    lock (completedEventLock) 
    {
        handler = ProcessCompleted;
    }
    if (handler != null)
        handler(this, e);
}

请注意,这并不能防止竞态条件的发生。例如,我们已经决定执行一组处理程序,然后有一个处理程序取消了订阅,但它仍然会被调用,因为我们已经将包含它的多路广播委托提取到了“handler”变量中。
除了使处理程序本身知道它不应再被调用之外,您无法做太多事情。
可以认为更好的做法是不要尝试使事件线程安全 - 指定订阅应该仅在将引发事件的线程中更改。

你确定需要锁吗?代理对象是不可变的且赋值是原子操作,因此我认为不需要加锁。 - TcKs
请看我对你的帖子的评论。你绝对需要加锁来使它线程安全。 - Jon Skeet
是的,在“添加”和“删除”中使用锁是必要的。但是,在“OnProcessingCompleted”中使用“锁”有什么好处呢? - TcKs
1
没有锁定,就没有内存屏障,因此不能保证您会看到最新的值。请参见http://pobox.com/~skeet/csharp/threads/volatility.shtml。 - Jon Skeet
6
即使使用锁也不能保证你看到最新的值。这是因为它取决于谁先到达:更改者还是调用者。因此,你只需要在添加/删除周围加锁,因为它们都是内存中的两个步骤:读和写。因此,在读和写之间,另一个更改者可以读取和写入,因此更改者1覆盖了更改者2的更改。但这不会影响调用者。调用者可能在更改者的读取和写入之间读取,但我没有看到任何问题。(如果两者同时运行,无论是否有锁,你都无法确定调用者获取的是哪个值)。 - mmmmmmmm
var handler = Interlocked.CompareExchange(ProcessCompleted, null, null); 这段代码会做什么? - bohdan_trotsenko

4

私有成员ProcessCompleted不需要是一个事件,它可以只是一个字段:private EventHandler<ProcessCompletedEventArgs> ProcessCompleted; - 在类内部它总是直接到达字段,所以event的内容已经丢失了。

您所展示的使用显式锁对象的方法并没有比仅具有类似于字段的事件(即public event EventHandler<ProcessCompletedEventArgs> ProcessCompleted;)更加线程安全。唯一的区别在于您没有锁定“this”(这是一个好事情 - 您应该尽量避免在this上进行锁定)。"处理程序变量"方法是正确的,但仍然存在副作用,您应该注意。


我在问题中添加了为什么使用私有事件处理程序和显式事件内容。这仍然不需要吗?你所说的差异是什么意思?公共事件EventHandler<ProcessCompletedEventArgs> SomeEvent是否会自动锁定此操作? - Svish
2
是的,类似于字段的事件(即没有显式添加/删除的事件)具有内置锁(this);请参见语言规范(MS版本)中的10.8.1节;但是,类内部的代码会绕过此功能-请参见http://marcgravell.blogspot.com/2009/02/fun-with-field-like-events.html;因此,作为*私有*事件,不使用添加/删除(因此也不使用锁)。对于显式接口实现,代码是正确的,您需要自己添加锁定,而您已经这样做了-可以说比“lock(this)”更好。坚持下去 ;-p - Marc Gravell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接