如果在另一个线程中被阻塞,为什么线程可以更改实例数据?

Why can threads change instance data if it is blocked in another thread?

我开始研究锁,马上就出现了一个问题

这里docs.microsoft说:

The lock statement acquires the mutual-exclusion lock for a given object, executes a statement block, and then releases the lock. While a lock is held, the thread that holds the lock can again acquire and release the lock. Any other thread is blocked from acquiring the lock and waits until the lock is released.

我举了一个简单的例子来证明,另一个线程的方法没有 lock 关键字可以很容易地更改实例的数据,而该实例被使用来自第一个线程的锁的方法占用。值得从阻止中删除评论,工作按预期完成。我认为锁会阻止其他线程对实例的访问,即使他们没有在他们的方法中对该实例使用锁。

问题:

  1. 我是否理解正确,在一个线程上锁定实例允许在该实例上修改来自另一个线程的数据,除非另一个线程也使用该实例的锁?如果是这样,那么这种阻塞通常会带来什么,为什么要这样做?

  2. 简单来说这是什么意思? 当持有锁时,持有锁的线程可以再次获取和释放锁


So code formatting works well.


using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class A
    {
        public int a;
    }

    class Program
    {
        static void Main(string[] args)
        {
            A myA = new A();

            void MyMethod1()
            {
                lock (myA)
                {
                    for (int i = 0; i < 10; i++)
                    {
                        Thread.Sleep(500);
                        myA.a += 1;
                        Console.WriteLine($"Work MyMethod1 a = {myA.a}");
                    }
                }                               
            }

            void MyMethod2()
            {
                //lock (myA)
                {
                    for (int i = 0; i < 10; i++)
                    {
                        Thread.Sleep(500);
                        myA.a += 100;
                        Console.WriteLine($"Work MyMethod2 a = {myA.a}");
                    }
                }                
            }

            Task t1 = Task.Run(MyMethod1);
            Thread.Sleep(100);
            Task t2 = Task.Run(MyMethod2);

            Task.WaitAll(t1, t2);

        }
    }
}

锁是合作的,它依赖于可以更改数据的所有各方在尝试更改数据之前合作并获取锁。请注意,锁不关心 什么 你在锁内改变。在保护某些数据结构时,使用代理锁对象是相当普遍的。即

private object myLockObject = new object();
private int a;
private int b;

public void TransferMonety(int amount){
    lock(myLockObject){
         if(a > amount){
             a-=amount;
             b+=amount;
        }
    }
}

因为这个锁非常灵活,你可以保护任何一种操作,但你需要正确编写你的代码。

因此,使用锁时一定要小心。锁最好是私有的,以避免任何不相关的代码获取锁。锁内的代码应该相当短,并且不应调用 class 之外的任何代码。这样做是为了避免死锁,如果任意代码是 运行 它可能会做一些事情,比如获取其他锁或等待事件。

虽然锁非常有用,但根据您的用例,还可以使用其他同步原语。

What does this mean in simpler terms? "While a lock is held, the thread that holds the lock can again acquire and release the lock."

这意味着你可以这样做:

lock (locker)
{
    lock (locker)
    {
        lock (locker)
        {
            // Do something while holding the lock
        }
    }
}

你可以多次获取锁,然后等量释放。这叫做reentrancy. The lock statement is reentrant, because the underlying Monitor class is reentrant by design. Other synchronization primitives, like the SemaphoreSlim, .