【问题标题】:Identify the thread which holds the lock识别持有锁的线程
【发布时间】:2010-08-19 15:44:20
【问题描述】:

我的应用程序中的一个线程在下面的 lock 语句中被阻塞并导致死锁

void ExecuteCommand()
{
    lock(this._lockinstance)
    {
        // do some operation
    }
}

是否可以轻松识别当前持有锁的线程?.. 我的应用程序有超过 50 个线程,这使得使用 Visual Studio 遍历每个调用堆栈来定位持有锁的线程变得困难

【问题讨论】:

    标签: c#


    【解决方案1】:

    一些示例代码可以试用:

    class Test {
        private object locker = new object();
        public void Run() {
            lock (locker) {  // <== breakpoint here
                Console.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId);
            }
        }
    }
    

    在指定的行上设置断点。当它中断时,使用 Debug + Windows + Memory + Memory 1. 右键单击​​窗口并选择“4-byte Integer”。在地址框中,键入 &locker。第二个字是拥有锁的线程的线程 ID。跳过 lock 语句以查看它的变化。

    请注意,该数字是托管线程 ID,而不是您在“调试 + Windows + 线程”窗口中看到的操作系统线程 ID。这有点糟糕,你可能应该在你的程序中添加一些日志来转储 ManagedThreadId 的值,这样你就有办法将值与线程匹配。更新:在更高的 VS 版本中已修复,Debug > Windows > Threads 调试器窗口现在显示 ManagedThreadId。

    【讨论】:

    • 确保您有调试 > 选项和设置 > 启用地址级调试,以便出现内存窗口
    • &amp;locker 似乎不起作用。只是给出“无法评估表达式”。 locker 有效,但这不允许我看到您描述的线程 ID。 (VS2010)
    • 对我也不起作用 - 尝试 64 位构建,在内存监视器中找不到预期的托管 ID
    • 在 VS2013 上重新测试,它仍然可以正常工作。它是 64 位进程的第 4 个 4 字节整数。通过调试器的 Debug + Windows + Threads 窗口进一步改进,现在显示托管线程 ID。十进制。非常重要的是,锁对象只用于锁中,例如,如果代码曾经在其上调用 GetHashCode(),它就无法工作。可能有一个工具+选项,调试设置很重要,我无法想象它可能是哪个。
    【解决方案2】:

    最近我试图确定哪个函数持有锁,发现以下非常有用,并且在以前的任何地方都没有见过。我把它作为答案放在这里,以防其他人也觉得它有用。

    之前发布的许多其他解决方案需要编写一个新类,然后将所有 lock(blah) 转换为 BetterLock(blah),这需要大量的调试工作,并且您可能不希望在生产/交付版本你的代码。其他人则需要附加调试器,这会改变代码的时序并可能掩盖问题。

    请尝试以下方法...

    原码:

    object obj = new object();
    lock(obj)
    {
        // Do stuff
    }
    

    用于调试的修改代码:

    object _obj = new object();
    object obj
    {
        get
        {
            System.Diagnostics.StackFrame frame = new System.Diagnostics.StackFrame(1);
            System.Diagnostics.Trace.WriteLine(String.Format("Lock acquired by: {0} on thread {1}", frame.GetMethod().Name, System.Threading.Thread.CurrentThread.ManagedThreadId));
            return _obj;
        }
    }
    // Note that the code within lock(obj) and the lock itself remain unchanged.
    lock(obj)
    {
        // Do stuff
    }
    

    通过将obj 公开为属性,至少是暂时的,通过非常 最少的代码更改,您可以确定哪个函数最后获得了锁以及在哪个线程上 - 只需查看跟踪输出最后一个条目。当然,您也可以输出任何其他可能对 getter 有用的信息。

    不,这不会让您确定何时释放锁,但如果它及时释放,那么您实际上并没有首先遇到锁争用问题.

    【讨论】:

    • 跟踪应该是"Attempting to acquire lock"。在高竞争下,跟踪中消息的顺序可能与线程获取锁的实际顺序不同。
    【解决方案3】:

    您可以实现一个监视器包装器,在输入时保存堆栈跟踪和线程名称。

    老办法:

    private object myLock = new object();
    
    ...
    lock(myLock)
    {
        DoSomething();
    }
    ...
    

    代码如下:

    private SmartLock myLock = new SmartLock();
    
    ...
    myLock.Lock( () =>
    {
        DoSomething();
    }
    );
    ...
    

    来源:

    public class SmartLock
    {
        private object LockObject = new object();
        private string HoldingTrace = "";
    
        private static int WARN_TIMEOUT_MS = 5000; //5 secs
    
    
        public void Lock(Action action)
        {
            try
            {
                Enter();
                action.Invoke();
            }
            catch (Exception ex)
            {
                Globals.Error("SmartLock Lock action", ex);
            }
            finally
            {
                Exit();
            }
    
        }
    
        private void Enter()
        {
            try
            {
                bool locked = false;
                int timeoutMS = 0;
                while (!locked)
                {
                    //keep trying to get the lock, and warn if not accessible after timeout
                    locked = Monitor.TryEnter(LockObject, WARN_TIMEOUT_MS);
                    if (!locked)
                    {
                        timeoutMS += WARN_TIMEOUT_MS;
                        Globals.Warn("Lock held: " + (timeoutMS / 1000) + " secs by " + HoldingTrace + " requested by " + GetStackTrace());
                    }
                }
    
                //save a stack trace for the code that is holding the lock
                HoldingTrace = GetStackTrace();
            }
            catch (Exception ex)
            {
                Globals.Error("SmartLock Enter", ex);
            }
        }
    
        private string GetStackTrace()
        {
            StackTrace trace = new StackTrace();
            string threadID = Thread.CurrentThread.Name ?? "";
            return "[" + threadID + "]" + trace.ToString().Replace('\n', '|').Replace("\r", "");
        }
    
        private void Exit()
        {
            try
            {
                Monitor.Exit(LockObject);
                HoldingTrace = "";
            }
            catch (Exception ex)
            {
                Globals.Error("SmartLock Exit", ex);
            }
        }
    }
    

    【讨论】:

      【解决方案4】:

      是的,您可以在 VS 中使用“线程”视图。在应用程序中的任意位置中断(或单击“全部中断”按钮),然后您可以选择每个线程并查看谁拥有锁定(如果有人)。

      要添加它,请转到调试 > Windows > 线程 (Ctrl+D,T)

      【讨论】:

      • +1 是的。将鼠标悬停在线程上以查看带有堆栈跟踪的工具提示。五十个线程,这是你必须学会​​避免的事情。
      • 谢谢。我知道这种技术。但是如果线程数非常多,则非常耗时
      • @Hans Passant:在win32(c++)中,可以通过检查临界区对象的属性来获取临界区的线程id。 c#中是否有类似的技术可用?
      • @Maanu:是的,你可以找到。我会发布它作为答案。
      • @HansPassant:当我将鼠标悬停在 Visual Studio 的“线程”窗口中的线程上时,工具提示会显示调用堆栈,但不显示父线程 (id)。
      【解决方案5】:

      旧帖子已过时。

      但我想我可能会给出一个我认为对于尝试追踪死锁和其他锁定问题非常有用的解决方案。

      我为我的锁使用一次性类 - 我喜欢 Monitor,但可以使用任何锁定机制。

      public class MonitorLock : IDisposable
      {
          public static MonitorLock CreateLock(object value)
          {
              return new MonitorLock(value);
          }
      
          private readonly object _l;
      
          protected MonitorLock(object l)
          {
              _l = l;
              
              Console.WriteLine("Lock {0} attempt by {1}", _l, Thread.CurrentThread.ManagedThreadId);
              
              Monitor.Enter(_l);
      
              Console.WriteLine("Lock {0} held by {1}" , _l, Thread.CurrentThread.ManagedThreadId);
          }
      
          public void Dispose()
          {
              Monitor.Exit(_l);
      
              Console.WriteLine("Lock {0} released by {1}", _l, Thread.CurrentThread.ManagedThreadId);
          }
      }
      

      我使用了一个带有名称的锁对象,这样我就可以清楚地知道我正在尝试获取哪个锁。

      public class LockObject
      {
          public string Name { get; set; }
      
          public LockObject(string name)
          {
              Name = name;
          }
      
          public override string ToString()
          {
              return Name;
          }
      }
      

      最后创建一个锁对象,然后在一个 using 块中持有该对象。

      //create an object to lock on
      private readonly object _requestLock = new LockObject("_requestLock");
      
      using (MonitorLock.CreateLock(_requestLock))
      {
          //do some work
      }
      

      输出应该类似于

      Lock _requestLock attempt by 92
      Lock _requestLock held by 92
      Lock _requestLock attempt by 19
      Lock _requestLock released by 92
      Lock _requestLock held by 19
      Lock _requestLock released by 19
      

      希望有人觉得这很有用:)

      【讨论】:

      • 这很整洁。我会为更大的应用程序做的一项性能优化是将MonitorLock 设置为struct。编译器足够聪明,不会在 using 块中使用时将结构装箱,并且您将为每个占用的锁节省分配空间。
      • 可以考虑使用 Monitor.IsEntered 的可重入代码以获得更多有用的跟踪。
      【解决方案6】:

      http://mse.codeplex.com/http://www.microsoft.com/downloadS/details.aspx?FamilyID=80cf81f7-d710-47e3-8b95-5a6555a230c2&displaylang=en 的托管堆栈资源管理器在这种情况下非常出色。

      它连接到正在运行的托管代码(需要适当的权限),包括实时代码,并获取正在运行的线程列表。您可以双击它们中的任何一个,或者(在这种情况下更有用)选择批次并按 Enter 以快速相对非侵入性地进行(显然它会消耗资源,但它会尽可能快地进出) 转储不同线程的当前堆栈。非常适合发现死锁、无限循环、近乎无限循环(当您的应用程序意外地依赖于天文学家对地球将持续多长时间才能完成的时候感到悲观)和其他此类情况。

      【讨论】:

      • 这个工具在我尝试的时候根本不起作用(Windows 7 x64)
      • @蒂姆。他们有问题处理程序吗? (我已经有一段时间没有看了)。
      • 他们有一个 codeplex 页面,但它看起来自 2012 年以来就没有被触及过。我认为它已经过时了,并且多年来并没有真正跟上 .NET 和 Windows 的变化.
      • 它只是没有显示任何可连接的进程。
      • @Tim 您是否尝试以管理员身份运行它?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-09-14
      • 1970-01-01
      • 1970-01-01
      • 2013-09-28
      • 2013-10-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多