锁定C#
我还是不清楚什么时候应该在一些代码周围包装一个锁。我的一般经验法则是在读取或者写入静态变量时将操作包装在锁中。但是,当仅读取静态变量时(例如,它是在类型初始化期间设置的只读),则不需要将其包装在lock语句中,对吗?我最近看到了一些类似于以下示例的代码,这使我认为我的多线程知识可能存在一些不足:
class Foo { private static readonly string bar = "O_o"; private bool TrySomething() { string bar; lock(Foo.objectToLockOn) { bar = Foo.bar; } // Do something with bar } }
但这对我来说毫无意义-为什么读寄存器会出现并发问题?
同样,此示例提出了另一个问题。其中之一比另一个更好吗? (例如,示例二持有锁的时间更少了?)我想我可以拆卸MSIL了……
class Foo { private static string joke = "yo momma"; private string GetJoke() { lock(Foo.objectToLockOn) { return Foo.joke; } } }
与
class Foo { private static string joke = "yo momma"; private string GetJoke() { string joke; lock(Foo.objectToLockOn) { joke = Foo.joke; } return joke; } }
解决方案
如果我们只是将值写入指针,则无需锁定,因为该操作是原子的。通常,我们应该在需要进行至少涉及两个原子操作(读或者写)的事务时锁定,该事务取决于状态在开始和结束之间不发生变化。
就是说,我来自Java领域,那里所有变量的读取和写入都是原子操作。这里的其他答案表明.NET是不同的。
脏读?
读取或者写入32位或者更小的字段是C#中的原子操作。据我所知,我们无需锁定所提供的代码。
我认为,我们应该尽力不要将静态变量放在需要从不同线程读取/写入静态变量的位置。在这种情况下,它们实际上是所有全局变量都可以自由使用的变量,而全局变量几乎总是一件坏事。
话虽如此,如果我们确实将静态变量放在这样的位置,则可能需要在读取期间锁定,以防万一,请记住,另一个线程可能在读取期间突然进入并更改了值,如果这样做,我们可能会导致数据损坏。除非我们通过锁定确保读取操作,否则不一定是原子操作。与写入相同,它们也不总是原子操作。
编辑:
正如Mark所指出的,对于Creads中的某些原语来说,它们总是原子的。但是请注意其他数据类型。
至于"哪个更好"的问题,它们是相同的,因为函数作用域未用于其他任何用途。
在我看来,在第一种情况下,不需要锁。确保使用静态初始化程序初始化bar是线程安全的。由于我们只读取过该值,因此无需锁定它。如果价值永远不会改变,就不会有任何争执,为什么要锁定呢?
由于我们编写的代码均未在初始化后修改静态字段,因此无需进行任何锁定。仅用新值替换字符串也不需要同步,除非新值取决于读取旧值的结果。
静态字段不是唯一需要同步的事物,任何可以修改的共享引用都容易受到同步问题的影响。
class Foo { private int count = 0; public void TrySomething() { count++; } }
我们可能假设执行TrySomething方法的两个线程会很好。但事实并非如此。
- 线程A将计数(0)的值读入寄存器,以便可以递增。
- 上下文切换!线程调度程序确定线程A有足够的执行时间。接下来的是线程B。
- 线程B将计数(0)的值读入寄存器。
- 线程B增加寄存器。
- 线程B保存结果(1)进行计数。
- 上下文切换回A。
- 线程A将保存在其堆栈中的count(0)值重新加载到寄存器中。
- 线程A增加寄存器。
- 线程A保存结果(1)进行计数。
因此,即使我们两次调用count ++,count的值也才刚刚从0变为1. 让我们使代码成为线程安全的:
class Foo { private int count = 0; private readonly object sync = new object(); public void TrySomething() { lock(sync) count++; } }
现在,当线程A被中断时,线程B不会打乱计数,因为它将打到lock语句,然后阻塞直到线程A释放同步。
顺便说一句,还有另一种方法可以使递增的Int32和Int64成为线程安全的:
class Foo { private int count = 0; public void TrySomething() { System.Threading.Interlocked.Increment(ref count); } }
关于问题的第二部分,我想我会选择比较容易理解的那个,任何性能差异都可以忽略不计。早期的优化是万恶之源,等等。
为什么穿线很难