C#中锁定语句的困惑

20
这是来自 MSDN 的内容: lock 关键字确保在一个线程进入“临界区”时,另一个线程不会进入相同的“临界区”。
“a critical section” 必须与“the critical section”相同吗?
还是它意味着: lock 关键字确保在一个线程进入由对象保护的“任何临界区”时,另一个线程不会进入由相同对象保护的“任何临界区”?
    class Program
{
    static void Main(string[] args)
    {
        TestDifferentCriticalSections();

        Console.ReadLine();
    }

    private static void TestDifferentCriticalSections()
    {
        Test lo = new Test();

        Thread t1 = new Thread(() =>
        {
            lo.MethodA();
        });
        t1.Start();

        Thread t2 = new Thread(() =>
        {
            lo.MethodB();
        });
        t2.Start();
    }
}

public class Test
{
    private object obj = new object();

    public Test()
    { }

    public void MethodA()
    {
        lock (obj)
        {
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(500);
                Console.WriteLine("A");
            }
        }
    }

    public void MethodB()
    {
        lock (obj)
        {
            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(500);
                Console.WriteLine("B");
            }
        }
    }
}

3
这段话更多地涉及语法而非C#。"the"表示特定的,而"a"是不确定的,可能指代代码的任何部分。这里有一个相关的英文网站链接:http://www.englishclub.com/grammar/adjectives-determiners-the-a-an.htm - Lloyd
6个回答

64

问题的措辞令人困惑,目前的答案也不是特别清晰。让我将问题重新语言成几个问题:

(1) lock语句是否确保在任何时候最多只有一个线程在lock语句体内部?

。例如:

static readonly object lock1 = new object();
static readonly object lock2 = new object();
static int counter = 0;
static object M()
{
    int c = Interlocked.Increment(ref counter);
    return c % 2 == 0 ? lock1 : lock2;
}

...
lock(M()) { Critical(); }

有可能两个线程同时在锁语句的主体中,因为锁语句锁定了两个不同的对象。线程 Alpha 可以调用 M() 并获取 lock1,然后线程 Beta 可以调用 M() 并获取 lock2。

(2) 假设我的锁语句总是锁定相同的对象,那么锁语句是否确保在任何时候只有一个“活动”的线程在锁的主体内?

是的。如果您有:

static readonly object lock1 = new object();
...
lock(lock1) { Critical(); }

那么线程 Alpha 可以获取锁,而线程 Beta 将会被 阻塞 直到锁可用后再进入锁体。

(3) 假设我有两个锁语句,并且每次都在同一个对象上加锁,那么锁语句是否确保任何时候在任一锁的主体中最多只有一个“活动”线程?

是的。如果你有:

static readonly object lock1 = new object();
...
static void X() 
{
    lock(lock1) { CriticalX(); }
}
static void Y() 
{
    lock(lock1) { CriticalY(); }
}

如果线程Alpha在X中并获取了锁,而线程Beta在Y中,则线程Beta将会阻塞,直到锁可用再进入锁体。

(4) 为什么你在“引号”中使用“active”?

这是为了强调一个等待的线程也可以在锁体内。您可以使用Monitor.Wait方法来“暂停”处于锁体内的线程,并允许已阻塞的线程变为活动状态并进入该锁体(或锁定同一对象的其他锁体)。等待线程将保持其“等待”状态,直到被唤醒。在唤醒后的某个时间点,它将重新加入“准备就绪”队列并阻塞,直到锁中没有“活动”线程。然后它将在离开时的点继续执行。


3
一如既往,您的回答在全面性和准确性方面远超过我的。因此,我会给您点赞。 - Stefan H
1
再次感谢您的回答,我从中受益匪浅。谢谢Eric。 - MoonKnight
1
+1,因为在“活跃”中加上引号真的很恐怖。 ;-)这里非常需要它。 - Nawaz

5
您在一个对象上放置了一把锁。如果另一个线程尝试同时访问由该对象标记的关键部分,它将阻塞直到锁被移除/完成。
示例:
public static object DatabaseLck= new object();

lock (DatabaseLck) {
        results = db.Query<T>(query).ToList();
     }

或者
lock (DatabaseLck) {
       results = db.Query<T>(string.Format(query, args)).ToList();
  }

这两个代码块不能同时运行,因为它们使用相同的锁对象。如果你为每个代码块使用不同的锁对象,它们就可以同时运行。


如果另一个线程尝试同时访问该对象,它将被阻塞。-- 你的意思是:"如果另一个线程尝试同时访问由该对象标记的关键部分,它将被阻塞"? - Cui Pengfei 崔鹏飞

3

它是同一个关键部分。

lock (synclock)
{
  // the critical section protected by the lock statement
  // Only one thread can access this at any one time
}

请参考MSDN上的lock语句
使用lock关键字可以将一个语句块标记为临界区,通过获取给定对象的互斥锁、执行语句,然后释放锁来实现。
或者说:lock关键字确保一个线程在进入代码中任何临界区之前,先等待其他线程退出该临界区。
不是这样的。它指的是由该锁和该锁独自保护的临界区。
更新,以下是代码示例:
如果您使用单个对象进行锁定,则会锁定所有临界区,导致其他线程阻塞直到释放。 在您的代码示例中,一旦进入MethodA中的锁定,所有到达MethodB的锁定以及该锁定的其他线程都将阻塞,直到锁定被释放(这是因为您在两种方法中都锁定了相同的对象)。

1

这并不意味着任何,尽管您可以使用相同的对象锁定两个代码块,以防止多个线程同时进入它们。这是一种常见的范例--您可能希望在清除和写入时都锁定您的集合。


0

不,这意味着另一个线程不会进入由此锁语句保护的关键部分。

关键部分仅由程序员定义,在这种情况下,您可以将其替换为:由锁保护的部分

因此翻译: lock 关键字确保一个线程不会进入由 lock 保护的代码段,同时另一个线程正在该代码段中(由 lock 保护)。


2
这难道不意味着它是由相同锁对象保护的任何部分吗? - erict
不行,因为你在锁定中使用的对象仅用于保证锁定。 - squelos
1
@squelos:任何锁定同一对象的部分都会被该对象上的锁定所阻塞。 - Stefan H

0

所谓的关键部分是由锁定语句保护的部分。

任何锁定在相同对象上的关键部分都将被阻止访问。

同样重要的是,您的锁定对象必须是静态的,因为锁定需要锁定(或尝试锁定)在锁定对象的相同实例上。


5
除非您在使用适当的锁定时发现了性能问题,请勿使用双重检查锁定。有大约一百万种方法可以错误地实现双重检查锁定,只有一种方法可以正确地实现它;如果不小心使用,这是一种极其危险的模式。如果您认为应该使用双重检查锁定,请三思而后行。您可能可以通过以下方式解决问题:(1)常规锁定,(2)互锁交换,(3)Lazy<T>类,(4)利用静态类初始化程序锁定语义。 - Eric Lippert
@EricLippert:我一直以为双重检查锁定是必要的。感谢澄清。我已经从我的答案中删除了该评论,但我认为其余部分仍然经得起审查。 - Stefan H
@EricLippert 那么对于单例模式的锁定技巧是什么呢?您会检查对象是否为空,然后锁定它,再次检查对象是否为空,最后实例化吗?这仍然算双重检查锁定吗?为了使其正常工作,是否需要内存屏障? - Stefan H
@StefanH:检查空值两次的技术被恰当地称为“双重检查锁定”模式。至于你关于内存屏障的问题:请更精确地提出你的问题。是否需要内存屏障才能使其正常工作?显然,锁引入了内存屏障,但该技术的全部意义在于避免锁 - Eric Lippert
1
关于双重检查锁定的危险性,可以参考以下链接:https://dev59.com/Am025IYBdhLWcg3wwYz4#5821201。 - Eric Lippert
显示剩余3条评论

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