锁定对象

5
我经常看到像这样的代码,在此处显示,即分配一个对象,然后将其用作“锁对象”。
我认为你可以使用任何对象来实现这个功能,包括事件本身作为锁对象。为什么要分配一个没有作用的新对象?我的理解是,在对象上调用lock()并不会实际改变对象本身,也不会实际锁定它以防止使用,它只是作为多个锁语句的占位符。
public class Shape : IDrawingObject, IShape
{
    // Create an event for each interface event
    event EventHandler PreDrawEvent;
    event EventHandler PostDrawEvent;

    object objectLock = new Object();

    // Explicit interface implementation required.
    // Associate IDrawingObject's event with
    // PreDrawEvent
    event EventHandler IDrawingObject.OnDraw
    {
        add
        {
            lock (objectLock)
            {
                PreDrawEvent += value;
            }
        }
        remove
        {
            lock (objectLock)
            {
                PreDrawEvent -= value;
            }
        }
    }
}

所以我的问题是,这真的是一个好主意吗?

3个回答

7
包括事件本身。
不可以这样做。一个“事件”实际上只是一些访问器方法。假设您指的是后备委托,那将非常糟糕——委托是不可变的:每次添加/删除订阅者,都会得到一个不同的委托。
实际上,4.0编译器现在使用无锁代码和Interlocked来实现这一点——可能值得采用这种方法。
在您的示例中,objectLock确保所有调用者(对该实例)都针对相同的对象进行锁定,这很重要——但没有锁定this的丑陋(这是C#编译器以前的工作方式)。
更新:您的示例显示在C# 4.0之前必须的代码,访问类似字段的事件内部直接与字段通信:正常的字段式事件锁定没有被尊重。这在C# 4.0中已更改;现在您可以(在C# 4.0中)安全地将其重写为:
public class Shape : IDrawingObject, IShape
{
    // Create an event for each interface event
    event EventHandler PreDrawEvent;
    event EventHandler PostDrawEvent;

    event EventHandler IDrawingObject.OnDraw
    {
        add { PreDrawEvent += value; }
        remove { PreDrawEvent -= value; }
    }
}

所有正确的行为都会被遵循。

@Mystere Man - 更糟糕的是:未订阅的事件是“null”,因此它将完全失败。您的假设是不正确的。 - Marc Gravell
1
在锁定方面做出假设是一个非常糟糕的想法。了解锁定的工作原理至关重要。 - Rusty
@Rusty - 确实;一般的方法是:要么保持非常简单(锁定某些私有和只读内容),要么非常熟练地掌握锁定的细节。 - Marc Gravell
1
关于您的更新...具有讽刺意味的是,我从.NET 4 MSDN获取了那段代码,猜想它还没有被更新;)不知道是否有可能在显式接口中使用时,.net 4已经不需要手动添加委托了? - Erik Funkenbusch
你的意思是这段代码能够工作吗?event EventHandler IFoo.Bar; 不行,它不能工作。 - Marc Gravell
显示剩余3条评论

2
任何私有的引用类型成员都可以胜任此任务。只要它是私有的且永不被重新分配。这使得委托对象失效,您绝对不想看到一个锁无法正常工作,只因为您无法控制的客户代码分配了事件处理程序。这种情况非常难以调试。
使用执行其他工作的私有成员并不是可扩展的方法。如果在重构或调试时发现需要锁定另一段代码区域,则需要找到另一个私有成员。这就是问题迅速恶化的地方:您可能会再次选择相同的私有成员,而死锁的危险就悄然而至。
如果将锁对象专用于需要保护的特定共享变量集,则不会发生这种情况。允许您给它起一个好名字。

这就是为什么我建议使用事件,因为那是你正在操作的“对象”。但是,如果我正确理解了Marc的评论,那么使用事件作为锁定对象是非常糟糕的事情... - Erik Funkenbusch

1

建议锁定私有静态字段,因为这可以确保多个尝试并行访问锁的线程将被阻塞。锁定类本身的实例(lock(this))或某个实例字段可能会有问题,因为如果两个线程在对象的两个不同实例上调用该方法,则它们将能够同时进入锁语句。


是的,但那不是我说的。我指的是将私有事件成员本身用作锁定变量(即PreDrawEvent)。我还应该注意,示例未使用静态变量。此外,如果在类中有多个锁定语句,锁定此对象会有问题。 - Erik Funkenbusch
同样的事情:在this实例字段上进行锁定可能不好,因为实例字段将根据对象实例而改变,而静态只读字段永远不会改变。 - Darin Dimitrov
我真的不明白问题出在哪里。整个重点是锁定变量的实例,以便多个线程不能更改该实例。为什么要防止同时修改两个不同的实例呢? - Erik Funkenbusch
在这种情况下,使用对象实例作为监视器是没有问题的,因为关键部分仅改变实例自己的数据结构。两个单独的线程实例同时进入此监视器是完全安全的。但如果两个执行线程同时进入/此/实例的监视器,则不安全。因此,在这种情况下,实例变量甚至对象本身都可以作为监视器。 - Jason Coco
在这种情况下是可以的,但请想象一下,如果该方法修改了一些共享数据(例如静态变量),会怎样。 - Darin Dimitrov
好的。那么为什么要分配对象呢?当你可以锁定事件本身时,它似乎是多余的。我在这里假设事件是值类型,或者最坏的情况下是编译器控制的自动引用类型,以便将锁放置在事件本身上,而不是被分配的事件(我真的不确定事件类型是什么,但我认为这是一个安全的假设)。我不得不想知道有多少人从MSDN复制和粘贴,并遭受这种冗余,甚至没有意识到它...或更糟。 - Erik Funkenbusch

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