2013-10-27 35 views
2

为什么,如果我有这样的语句:并发线程可以同时检查相同的对象锁吗?

private int sharedValue = 0; 

public void SomeMethodOne() 
{ 
    lock(this){sharedValue++;} 
} 

public void SomeMethodTwo() 
{ 
    lock(this){sharedValue--;} 
} 

因此,对于一个线程进入一个锁如果另一个线程操作它,必须先检查。如果不是,它可以进入并且必须将某些内容写入内存,但这肯定不能是原子的,因为它需要读取和写入。

那么为什么一个线程不可能读取锁,而另一个线程正在将它的所有权写入它呢?

简化为什么两个线程不能同时进入锁定?

+1

查看另一个类似的问题在这里:http://stackoverflow.com/questions/14758088/how-are-atomic-operations-implemented-at-a-hardware-level – blt

+1

他们是原子..具体来说,他们是一个原子更新的同步块索引..这只是一个整数。 –

+1

这是锁定的关键 - 以便您可以编写安全的并发线程使用的代码。 – Enigmativity

回答

2

它看起来像你基本上是问如何锁工作。锁怎样才能以原子的方式保持内部状态而不必建立锁?它似乎是一个chicken and egg problem起初不是吗?

这一切都是因为compare-and-swap(CAS)的操作而发生的。 CAS操作是一个硬件级别的指令,它执行两个重要的事情。

  • 它生成一个内存屏障,以便指令重新排序受到约束。
  • 它将内存地址的内容与另一个值进行比较,如果它们相等,则将原始值替换为新值。它以原子的方式完成所有这些。

从最基本的层面来看,这就是技巧的完成。并非所有其他线程都在阅读时被阻止,而另一线程正在写入。这完全是错误的思考方式。实际发生的情况是所有线程都同时作为编写者。该策略比悲观更乐观。每个线程都试图通过执行称为CAS的特殊类型的写入来获取锁。您实际上可以通过Interlocked.CompareExchange(ICX)方法在.NET中访问CAS操作。每一个同步原语都可以从这个单独的操作中建立。

如果我要在C#中完全从头开始编写类似Monitor的类(即关键字lock在幕后使用的),我可以使用Interlocked.CompareExchange方法来完成。这是一个过度简化的实现。请记住,这当然是而不是 .NET Framework如何做到这一点。 我提供下面的代码的原因是向您展示如何在纯粹的C#代码中完成而不需要幕后的CLR魔术,以及它可能让您考虑微软如何实施它。

public class SimpleMonitor 
{ 
    private int m_LockState = 0; 

    public void Enter() 
    { 
     int iterations = 0; 
     while (!TryEnter()) 
     { 
      if (iterations < 10) Thread.SpinWait(4 << iterations); 
      else if (iterations % 20 == 0) Thread.Sleep(1); 
      else if (iterations % 5 == 0) Thread.Sleep(0); 
      else Thread.Yield(); 
      iterations++; 
     } 
    } 

    public void Exit() 
    { 
     if (!TryExit()) 
     { 
      throw new SynchronizationLockException(); 
     } 
    } 

    public bool TryEnter() 
    { 
     return Interlocked.CompareExchange(ref m_LockState, 1, 0) == 0; 
    } 

    public bool TryExit() 
    { 
     return Interlocked.CompareExchange(ref m_LockState, 0, 1) == 1; 
    } 
} 

该实现演示了一些重要的事情。

  • 它显示了如何使用ICX操作来自动读取和写入锁定状态。
  • 它显示了可能发生的等待情况。

注意我是如何使用Thread.SpinWaitThread.Sleep(0)Thread.Sleep(1)Thread.Yield而锁等待被收购。等待策略过于简化,但它的确已经接近real life algorithm implemented in the BCL。我故意在上面的Enter方法中保持代码简单,以便更容易发现关键位。这不是我通常会如此实施的方式,但我希望它能够将重点放在首位。

另请注意,我的SimpleMonitor上面有很多问题。这只是少数。

  • 它不处理嵌套锁定。
  • 它不提供WaitPulse方法像真正的Monitor类。他们真的很难做对。

的CLR将实际使用的存储器的特殊块上的每个引用类型存在。这块内存被称为“同步块”。 Monitor将操纵这块内存中的位来获取和释放锁。这个动作可能需要一个内核事件对象。你可以阅读关于Joe Duffy's blog的更多信息。

+0

嗨。感谢您的回应。我正在阅读Duffy(Windows上的Concurrent Programming)一书,它讨论了Dekker和Dijkstra的算法,以及你上面提到的内容。 – William

+0

@ user1743962:这是一本好书。请务必阅读关于旋转等待的部分,从页码开始。这是我在回答我的等待策略时所使用的另一个参考。 –

1

lock在C#中用于创建实际用于锁定的对象Monitor

您可以在这里阅读更多关于Monitorhttp://msdn.microsoft.com/en-us/library/system.threading.monitor.aspx。所述MonitorEnter方法确保只有一个线程可以在时间进入临界区:

获取用于一个对象的锁。这一行动也标志着关键部分的开始。除非使用不同的锁定对象执行关键部分中的指令,否则没有其他线程可以进入关键部分。

顺便说一句,你应该避免锁定thislock(this))。您应该在类(静态或非静态)上使用私有变量来保护关键部分。你可以阅读更多在上面提供的相同的链接,但其原因是:

当选择在其上同步的对象,你应该只在私有或内部对象锁定。锁定外部对象可能会导致死锁,因为不相关的代码可以选择相同的对象来锁定以达到不同的目的。

相关问题