为啥在 lock 语句中必须指定同步对象
Posted
技术标签:
【中文标题】为啥在 lock 语句中必须指定同步对象【英文标题】:Why is specifying a synchronization object in the lock statement mandatory为什么在 lock 语句中必须指定同步对象 【发布时间】:2012-12-14 16:30:20 【问题描述】:我正在努力思考lock statement 中究竟发生了什么。
如果我理解正确的话,lock 语句是语法糖和以下...
Object _lock = new Object();
lock (_lock)
// Critical code section
...被翻译成大致如下:
Object _lock = new Object();
Monitor.Enter(_lock);
try
// Critical code section
finally Monitor.Exit (_lock);
我用过几次lock语句,总是创建一个私有字段_lock
,作为专用的同步对象。我明白你为什么不应该锁定公共变量或类型。
但是为什么编译器不创建那个实例字段呢?我觉得实际上可能存在开发人员想要指定锁定什么的情况,但根据我的经验,在大多数情况下,这完全没有兴趣,你只想要那个锁!那么为什么没有无参锁重载呢?
lock()
// First critical code section
lock()
// Second critical code section
将被翻译成(或类似的):
[DebuggerHidden]
private readonly object _lock1 = new object()
[DebuggerHidden]
private readonly object _lock2 = new object()
Monitor.Enter(_lock1);
try
// First critical code section
finally Monitor.Exit(_lock1);
Monitor.Enter(_lock2);
try
// Second critical code section
finally Monitor.Exit(_lock2);
编辑:我显然不清楚多个锁定语句。更新了问题以包含两个锁定语句。
【问题讨论】:
您的建议似乎等同于 lock(this) 这不是一个好主意。 @empi: 不,它不等同于lock(this)
,因为_lock 是一个成员
@mousio:它在功能上是等效的,因为类实例会有一个锁对象(_lock)
@empi:您刚刚解释了为什么它不等效;需要锁的静态方法呢?
@adabyron 你已经有一个属性可以应用于这个[MethodImpl(MethodImplOptions.Synchronized)]
的方法
【参考方案1】:
需要存储锁的状态。是否输入。这样可以阻塞另一个试图进入同一个锁的线程。
这需要一个变量。一个很简单的,一个普通的对象就够了。
对这种变量的一个硬性要求是它是在任何锁定语句使用它之前创建的。尝试按照您的建议即时创建它会产生一个新问题,现在需要使用锁来安全地创建变量,以便只有进入锁的第一个线程创建它,而其他线程试图进入锁在创建之前被阻塞。这需要一个变量。等等,一个无法解决的先有鸡还是先有蛋的问题。
【讨论】:
感谢您的回答。但是,是什么阻止了为编译器生成的变量存储状态? 我不确定这是否能回答问题。 可以使用不同的对象进行更细粒度的锁定这一事实。编译器无法合理推断出哪个对象应该持有锁。 怎么会有先有鸡还是先有蛋的问题?如果字段在字段初始化器中初始化,则它由调用构造函数的线程创建。当然只能有 1 个线程调用给定对象的构造函数。 现在您正在对锁定变量的范围做出危险的假设。假设它总是可以是一个非静态变量当然是不合适的。这不会保护具有静态范围的共享资源,很多都可以。【参考方案2】:在某些情况下,您可能需要两个不同的lock
,它们彼此独立。这意味着当代码的一个“可锁定”部分被锁定时,其他“可锁定”部分不应被锁定。这就是为什么可以提供锁对象的原因——你可以为几个独立的lock
s 拥有其中的几个
【讨论】:
每个 lock 语句当然会创建一个单独的编译器生成的变量(必须生成名称,例如匿名类型获取名称的方式)。但我理解你的观点已经转变:有时你想锁定同一个变量,这就是为什么无参数版本必须是重载的原因之一。 @adabyron 那么您将如何对 2 个方法进行同步锁定?您似乎只看一种锁方法的退化情况?一个变量。很多时候,您有多个方法使用相同的锁定语句(计数、放置、获取锁定列表)。 @TomTom:在这种情况下(存在,正如我在我的问题中已经允许的那样),使用 overloadlock(_userspecifiedvariable)
,到目前为止。
@adabyron - 他们没有进行无参数重载,因为它没有真正的好处,而是编译器团队开发的巨大成本:) 你可以认为它是“很高兴拥有”。跨度>
【参考方案3】:
为了使无变量的事情起作用,您必须:
每个锁块有一个自动生成的锁变量(您所做的,这意味着您不能让两个不同的锁块锁定同一个变量) 对同一类中的所有锁块使用相同的锁变量(这意味着您不能保护两个独立的事物)此外,您还会遇到决定这些是实例级还是静态的问题。
最后,我猜语言设计者认为在一个特定情况下的简化并不值得在阅读代码时引入歧义。线程代码(这是使用锁的原因)已经很难正确编写和验证。让它变得更难不是一件好事。
【讨论】:
锁定同一个变量需要现有的 lock(var) 重载(请参阅 Christopher Harris 和 SergeyS 的答案中的 cmets)。实例/静态变量确实是一个点,在某种程度上解决了 Hans Passants 的答案。我认为包含 lock 语句的方法的 static 修饰符将决定变量 static 修饰符。 我并没有真正看到歧义。 lock() 会立即告诉您,无需进一步调查,该锁未共享。编写代码会更容易,因为您不必声明该变量(在非共享情况下)。关于同步变量是保护目标的误解会减少——在我的书中,所有的好事。但是,我现在同意这些好处可能不值得付出努力。【参考方案4】:允许隐式锁定对象可能会鼓励使用单个锁定对象,这被认为是不好的做法。通过强制使用显式锁定对象,该语言鼓励您将锁定命名为有用的名称,例如“countIncementLock”。
因此命名的变量不会鼓励开发人员在执行完全独立的操作时使用相同的锁对象,例如写入某种流。
因此,对象可以在一个线程上写入流,同时在另一个线程上增加计数器,并且两个线程都不会相互干扰。
该语言不这样做的唯一原因是因为它看起来像是一种好的做法,但实际上隐藏了一种不好的做法。
编辑:
也许 C# 的设计者不想要隐式锁定变量,因为他们认为这可能会助长不良行为。
也许设计者根本没有考虑隐式锁变量,因为他们首先要考虑其他更重要的事情。
如果每个 C# 开发人员在编写 lock()
时都知道发生了什么,并且他们知道其中的含义,那么它就没有理由不存在,也没有理由不能按照您的建议工作.
【讨论】:
我认为您的观点与 SergeyS 相同。在我的建议中,每个锁语句都有一个同步变量。开发人员将无法锁定同一个变量。我可能需要更新问题以明确这一点。 @ababyron,我的观点与@SergeyS' 不完全相同。我试图回答问题why does the compiler not create that instance field as well?
,但没有解决答案本身的有效性。例如,仅仅因为编译器不这样做,并不意味着它不应该这样做。对于您的建议,也许有一个完全有效的用例。我的答案假设是,“如果不应该有隐式锁变量怎么办?”
我从不想替换 lock(var)。这只是一个无参数的重载。
对。我完全理解这一点。之前,我假设您想要一个锁变量的单个实例,用于一个类中的所有lock()
。然后你说每个人都有自己的实例。这更有意义。尽管如此,编译器团队仍会花费大量时间和精力来实现此功能,而且我确信这个想法刚刚被推迟,转而支持其他功能。以上是关于为啥在 lock 语句中必须指定同步对象的主要内容,如果未能解决你的问题,请参考以下文章