在什么条件下线程可以同时进入锁(监控)区域多次?

Edw*_*vey 8 .net c# concurrency locking thread-safety

(问题修改):到目前为止,答案都包括一个线程重新进入锁定区域的线程,通过递归之类的东西,你可以跟踪单个线程进入锁定两次的步骤.但是有可能以某种方式,对于单个线程(可能来自ThreadPool,可能是由于定时器事件或异步事件或线程进入休眠状态并在其他一些代码块中单独唤醒/重用)以某种方式产生两个不同的地方彼此独立,因此,当开发人员通过简单地阅读他们自己的代码而没有想到它时,会遇到锁重入问题?

在ThreadPool类备注(单击此处)中,备注似乎表明睡眠线程应在不使用时重复使用,否则会因睡眠而浪费.

但是在Monitor.Enter参考页面上(点击这里),他们说"同一个线程在没有阻止的情况下不止一次调用Enter是合法的". 所以我认为必须有一些我应该小心避免的东西.它是什么?这怎么可能单个线程输入两次相同的锁定区域?

假设您有一些锁定区域,不幸的是很长时间.这可能是现实的,例如,如果您访问已被分页的内存(或其他内容).锁定区域中的线程可能会进入睡眠状态.同一个线程是否有资格运行更多代码,这可能会意外地进入同一个锁定区域?在我的测试中,以下内容不会使同一个线程的多个实例运行到同一个锁定区域.

那怎么产生问题呢?你究竟需要小心避免什么?

class myClass
{
    private object myLockObject;
    public myClass()
    {
        this.myLockObject = new object();
        int[] myIntArray = new int[100];               // Just create a bunch of things so I may easily launch a bunch of Parallel things
        Array.Clear(myIntArray, 0, myIntArray.Length); // Just create a bunch of things so I may easily launch a bunch of Parallel things
        Parallel.ForEach<int>(myIntArray, i => MyParallelMethod());
    }
    private void MyParallelMethod()
    {
        lock (this.myLockObject)
        {
            Console.Error.WriteLine("ThreadId " + Thread.CurrentThread.ManagedThreadId.ToString() + " starting...");
            Thread.Sleep(100);
            Console.Error.WriteLine("ThreadId " + Thread.CurrentThread.ManagedThreadId.ToString() + " finished.");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Eri*_*ert 12

假设您有一个包含操作的队列:

public static Queue<Action> q = whatever;
Run Code Online (Sandbox Code Playgroud)

假设Queue<T>有一个方法Dequeue返回一个bool,指示队列是否可以成功出列.

假设你有一个循环:

static void Main()
{
    q.Add(M);
    q.Add(M);
    Action action;
    while(q.Dequeue(out action)) 
      action();
}
static object lockObject = new object();
static void M()
{
    Action action;
    lock(lockObject) 
    { 
        if (q.Dequeue(out action))
            action();
    }
}
Run Code Online (Sandbox Code Playgroud)

显然主线程进入M锁定两次; 这段代码是可重入的.也就是说,它通过间接递归进入自身.

这段代码看起来难以置信吗?它不应该.这就是Windows的工作原理.每个窗口都有一个消息队列,当消息队列被"抽取"时,调用与这些消息相对应的方法.单击按钮时,消息将进入消息队列; 当抽取队列时,将调用与该消息对应的单击处理程序.

因此,编写Windows程序是非常常见且非常危险的,其中锁包含对泵送消息循环的方法的调用.如果由于首先处理消息而进入该锁定,并且如果消息在队列中两次,则代码将间接进入自身,这可能导致各种疯狂.

消除这种情况的方法是:(1)在锁内部不要做任何甚至稍微复杂的事情;(2)当你处理消息时,禁用处理程序直到处理消息.


小智 5

如果你有这样的结构,可以重新进入:

Object lockObject = new Object(); 

void Foo(bool recurse) 
{
  lock(lockObject)
   { 
       Console.WriteLine("In Lock"); 
       if (recurse)  { foo(false); }
   }
}
Run Code Online (Sandbox Code Playgroud)

虽然这是一个非常简单的示例,但在许多情况下,您可能存在相互依赖或递归的行为.

例如:

  • ComponentA.Add():锁定一个公共的"ComponentA"对象,将新项添加到ComponentB.
  • ComponentB.OnNewItem():新项触发列表中每个项的数据验证.
  • ComponentA.ValidateItem():锁定一个公共的"ComponentA"对象来验证该项.

需要在同一个锁上重新输入相同的线程,以确保您不会因自己的代码而出现死锁.


Edw*_*vey 1

ThreadPool 线程不能仅仅因为它们进入睡眠状态而在其他地方重用;它们需要在重新使用之前完成。在锁定区域中花费很长时间的线程没有资格在其他某个独立控制点运行更多代码。体验锁重新进入的唯一方法是通过递归或执行锁内重新进入锁的方法或委托。