【问题标题】:Does lock(){} lock a resource, or does it lock a piece of code?lock(){} 是锁定一个资源,还是锁定一段代码?
【发布时间】:2009-04-15 18:54:02
【问题描述】:

我仍然很困惑...当我们写这样的东西时:

Object o = new Object();
var resource = new Dictionary<int , SomeclassReference>();

...并且有两个代码块在访问resource 时锁定o...

//Code one
lock(o)
{
  // read from resource    
}

//Code two
lock(o)
{
  // write to resource
}

现在,如果我有两个线程,其中一个线程执行从 resource 读取的代码,另一个执行写入它的代码,我想锁定 resource,这样当它被读取时,作者将不得不等待(反之亦然 - 如果正在写入,读者将不得不等待)。锁结构会帮助我吗? ...或者我应该使用其他东西吗?

(我在这个例子中使用Dictionary,但可以是任何东西)

我特别关注两种情况:

  1. 两个线程试图执行同一行代码
  2. 两个线程试图在同一个资源上工作

lock 会在这两种情况下提供帮助吗?

【问题讨论】:

  • @SilverHorse:很抱歉踩到你的编辑 - 回去添加标签,但没有意识到你同时编辑了!

标签: c# multithreading concurrency


【解决方案1】:

大多数其他答案都针对您的代码示例,因此我将尝试在标题中回答您的问题。

锁实际上只是一个令牌。可以说,拥有令牌的人可以上台。因此,您锁定的对象与您尝试同步的资源没有显式连接。只要所有读者/作者都同意同一个令牌,它就可以是任何东西。

当试图锁定一个对象时(即通过在一个对象上调用Monitor.Enter),运行时检查锁是否已经被一个线程持有。如果是这种情况,则试图锁定的线程被挂起,否则它获取锁并继续执行。

当持有锁的线程退出锁作用域(即调用Monitor.Exit)时,锁被释放,任何等待的线程现在都可以获取锁。

关于锁的最后几点要记住:

  • 根据需要锁定,但不再锁定。
  • 如果您使用Monitor.Enter/Exit 而不是lock 关键字,请确保将对Exit 的调用放在finally 块中,这样即使在出现异常的情况下也会释放锁。
  • 公开要锁定的对象会使了解锁定对象和时间变得更加困难。理想的同步操作应该被封装。

【讨论】:

  • 不明白这一点“将对象公开以锁定使得更难了解谁在锁定以及何时锁定。理想情况下应该封装同步操作”谢谢
  • 如果用于锁定的对象可以在类型之外访问,则更难识别谁在锁定。另一方面,如果所有的锁定都被封装了,你可以很容易地获得锁定的概览。
  • 不清楚。你的意思是我用过的对象“o”应该严格地在一个类型之内吧?
【解决方案2】:

是的,使用锁是正确的方法。您可以锁定任何对象,但正如其他答案中提到的,锁定您的资源本身可能是最简单和最安全的。

但是,您可能希望使用读/写锁对而不是单个锁,以减少并发开销。

这样做的基本原理是,如果您只有一个线程写入,但有多个线程读取,则您不希望读取操作阻塞另一个读取操作,而只有读取阻塞写入,反之亦然。

现在,我更像是一个 java 人,所以你必须更改语法并挖掘一些文档以在 C# 中应用它,但 rw-locks 是 Java 中标准并发包的一部分,所以你可以写一些东西喜欢:

public class ThreadSafeResource<T> implements Resource<T> {
    private final Lock rlock;
    private final Lock wlock;
    private final Resource res;

    public ThreadSafeResource(Resource<T> res) {
        this.res = res;
        ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        this.rlock = rwl.readLock();
        this.wlock = rwl.writeLock();
    }

    public T read() {
        rlock.lock();
        try { return res.read(); }
        finally { rlock.unlock(); }
    }

    public T write(T t) {
        wlock.lock();
        try { return res.write(t); }
        finally { wlock.unlock(); }
    }
}

如果有人能想出一个 C# 代码示例...

【讨论】:

  • 如果该资源在他的内部代码之外可见,则锁定该资源不是一个好主意。这可能导致死锁或竞争条件。建议您锁定私有对象以防止出现这种情况。
  • 好吧,在这种情况下,您确实希望防止其他并发感知代码在您处理资源时弄乱您的(非线程安全的)资源,所以你确实想锁定它。
【解决方案3】:

两个代码块都被锁定在这里。如果线程一锁定了第一个块,而线程二试图进入第二个块,它将不得不等待。

【讨论】:

  • 有两种情况... 1) 两个线程试图执行同一行代码 2) 两个线程试图在同一资源上工作将在两种情况下锁定帮助
【解决方案4】:

lock (o) { ... } 语句编译成这样:

Monitor.Enter(o)
try { ... }
finally { Monitor.Exit(o) }

如果另一个线程已经调用了 Monitor.Enter(),则该调用将阻塞该线程。只有在其他线程在对象上调用 Monitor.Exit() 后才会解除阻塞。

【讨论】:

    【解决方案5】:

    在这两种情况下都会锁定帮助吗? 是的。

    lock(){} 是否锁定资源,或者是否 它锁定了一段代码?

    lock(o)
    {
      // read from resource    
    }
    

    是语法糖

    Monitor.Enter(o);
    try
    {
      // read from resource 
    }
    finally
    {
      Monitor.Exit(o);
    }
    

    Monitor 类包含您用来同步访问代码块的对象集合。 对于每个同步对象,Monitor 保持:

    1. 对当前持有同步对象锁的线程的引用;即轮到该线程执行。
    2. “就绪”队列 - 在获得此同步对象的锁之前一直处于阻塞状态的线程列表。
    3. “等待”队列 - 在 Monitor.Pulse()Monitor.PulseAll() 将它们移动到“就绪”队列之前阻塞的线程列表>.

    所以,当一个线程调用 lock(o) 时,它会被放入 o 的就绪队列中,直到它获得 o 上的锁,此时它会继续执行其代码。

    【讨论】:

    • 应该是'finally'而不是'catch'。很抱歉吹毛求疵:)
    • @Andy - 是的。更新了答案。
    【解决方案6】:

    假设您只涉及一个进程,这应该可以工作。如果您希望它在多个进程中工作,您将需要使用“互斥锁”。

    哦,“o”对象应该是一个单例对象,或者在需要锁定的任何地方都具有作用域,因为真正被锁定的是该对象,如果您创建一个新对象,则该新对象将不会被锁定然而。

    【讨论】:

      【解决方案7】:

      您实现它的方式是一种可接受的方式来做您需要做的事情。一种改进方法是在字典本身上使用 lock(),而不是用于同步字典的第二个对象。这样,资源本身就不会传递一个额外的对象,而是跟踪它自己的监视器上是否有锁。

      在某些情况下使用单独的对象可能很有用,例如同步对外部资源的访问,但在这种情况下,它会产生开销。

      【讨论】:

        猜你喜欢
        • 2016-07-26
        • 1970-01-01
        • 1970-01-01
        • 2017-10-23
        • 1970-01-01
        • 1970-01-01
        • 2019-05-13
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多