zoukankan      html  css  js  c++  java
  • 死锁检测<转载msdn>


    代码下载位置: NETMatters2007_10.exe (156 KB)
    Browse the Code Online

     
    问:我在应用程序中使用锁来同步一组线程上的工作。不幸的是,由于我的不正确操作,我的线程有时似乎会停止工作。我想我是遇上了死锁,但我不清楚如何找到它们。有没有办法通过编程来找到它们?我希望遇到死锁时能引发异常。
    问:我在应用程序中使用锁来同步一组线程上的工作。不幸的是,由于我的不正确操作,我的线程有时似乎会停止工作。我想我是遇上了死锁,但我不清楚如何找到它们。有没有办法通过编程来找到它们?我希望遇到死锁时能引发异常。
    首先,重要的是要理解什么是线程中的死锁,以及导致死锁的条件。线程出现死锁的条件是互相等待释放某些资源,但是随着执行该阻塞等待,他们不会释放其他线程需要用来解除阻塞的资源。在资源得到释放之前,线程不会有任何进度,但是因为它们没有进度,资源就永远不会被释放,于是线程就锁住了,形成了“死锁”。许多操作系统课本都会引用死锁产生的四个必备条件:
    首先,重要的是要理解什么是线程中的死锁,以及导致死锁的条件。线程出现死锁的条件是互相等待释放某些资源,但是随着执行该阻塞等待,他们不会释放其他线程需要用来解除阻塞的资源。在资源得到释放之前,线程不会有任何进度,但是因为它们没有进度,资源就永远不会被释放,于是线程就锁住了,形成了“死锁”。许多操作系统课本都会引用死锁产生的四个必备条件:
    • 数量有限的特定资源。如果是 C# 中的监视器(在使用 lock 关键字时使用),此限定数量为一,因为监视器是互斥锁(意味着一次只有一个线程能占有监视器)。
    • 占用一个资源再请求另一个资源的能力。在 C# 中,这类似于锁定一个对象,然后在释放第一个锁前再锁定另一个,例如:
    lock(a)
    {
        lock(b)
        {
            ...
        }
    }
    
    • 没有抢占的能力。在 C# 中,这表示一个线程不能迫使其他线程释放锁。
    • 循环等待的条件。这表示在线程中存在一个循环,每个线程都在等待下一个线程释放资源才能继续。
    如果不符合这些条件的任意一条,死锁就不可能发生。第一个条件是监视器固有的,因此只要您使用监视器,这一条件是不可更改的。只要确保一次只锁定一个对象,第二个条件就可以避免,但是在大型软件项目中此要求经常是不可行的。第三个条件在 Microsoft® .NET Framework 中有可能避免,只要中止或打断占用您的线程所需资源的线程即可,但是 a) 这需要了解是哪个线程占有了资源,b) 这是一个有固有危险性的操作(要了解存在的众多原因,请参阅 msdn.microsoft.com/msdnmag/issues/05/10/Reliability)。因此,避免死锁的方法是避免(或阻止)第四个条件。
    在 2006 年 4 月期的文章(请访问 msdn.microsoft.com/msdnmag/issues/06/04/Deadlocks)中,Joe Duffy 讨论了一些避免和检测死锁的技术,其中一个称为锁级别。在锁级别中,锁被赋予数值,线程只能获得其数值大于已获得锁的数值的那些锁。这防止了出现循环的可能。在目前典型的软件应用程序中,这通常也很难做得很好,一旦在每个锁的获得上没有遵循锁级别,即会引发死锁。
    许多系统不是大规模地预防死锁,而是试图检测死锁,并且一旦发现立即清除。例如,SQL Server® 能在产生死锁时检测到它们,并通过中止循环中的任务之一将其删除。在文章中,Joe 构建了公共语言运行库 (CLR) 主机,它能在使用监视器的 .NET 应用程序中实现这种形式的死锁检测,这是一个非常棒的功能。遗憾的是,使用自定义 CLR 主机对于许多 .NET 应用程序来说并不总是实际的,包括那些已有自定义主机的应用程序,比如 ASP.NET 应用程序。因此,如能在无需自定义 CLR 主机的前提下利用相似的死锁检测功能,这将会非常有益,这样就能在运行中检测出这些类型的死锁。这对于在开发和调试阶段识别误用锁的代码错误非常有益。同时,它也可以在生产中用于随时检测和消除产生的死锁(阻止线程尝试进入产生循环所需的关键等待,从而防止其引起死锁),但是典型的死锁检测算法开销非常大,可能会由于性能原因而不适合在生产系统中使用。(我会在本专栏的结尾处就性能做一些评论。)
    为了满足此需求,我为 .NET System.Threading.Monitor 类构建了一个示例包装。如同 Monitor,我的 DdMonitor 类提供 Enter 和 Exit 方法,并在后台委托给 Monitor 上的等效方法。不过,它还跟踪监视器的使用情况,当试图获得锁将结束循环(导致死锁)时引发异常。在专栏的其余部分,我将更详细地介绍此死锁监视器类的实现,并就其用法及其部署的优点和缺点提供一些附加信息。
    DdMonitor 类的概要如图 1 所示。请注意,从一开始 DdMonitor 并没有完全模拟 System.Threading.Monitor 的公共接口。它提供了相同的公共静态 TryEnter、Enter 和 Exit 方法,但是它没有提供其对应体的 Wait、Pulse 和 PulseAll 静态公共方法。
    class DdMonitor
    {
        public static IDisposable Lock(object monitor) { ... }
    
        public static void Enter(object monitor) { ... }
    
        public static bool TryEnter(object monitor) { ... }
        public static bool TryEnter(
            object monitor, TimeSpan timeout) { ... }
        public static bool TryEnter(
            object monitor, int millisecondsTimeout) { ... }
    
        public static void Exit(object monitor) { ... }
    }
    
    
    DdMonitor 还提供了一个公共静态 Lock 方法。因为 C# lock 关键字对 Monitor 提供了一个很好的抽象,所以为 DdMonitor 提供一个相似的抽象也会有用。我们没有能力修改 C# 语言,因此就无法得到完全相同的语法,但是我们能做到非常接近:
    // with Monitor
    lock(obj) { ... }
    
    // with DdMonitor
    using(DdMonitor.Lock(obj)) { ... }
    
    此语法功能是通过实现 Lock 来完成的,如图 2 所示。Lock 方法委托 DdMonitor.Enter 获得锁。但是,它还实例化一个实现 IDisposable 的 DdMonitorCookie 对象。此对象的 Dispose 方法将调用 DdMonitor.Exit 来释放锁,这使得开发人员能够如之前所示的那样,将 DdMonitor.Lock 封装到一个 C# 的 using 语句中(Visual Basic® 中的 Using 或 C++/CLI 中的堆栈分配)。
    public static IDisposable Lock(object monitor)
    {
        if (monitor == null) throw new ArgumentNullException("monitor");
        IDisposable cookie = new DdMonitorCookie(monitor);
        Enter(monitor);
        return cookie;
    }
    
    private class DdMonitorCookie : IDisposable
    {
        private object _monitor;
    
        public DdMonitorCookie(object obj) { _monitor = obj; }
    
        public void Dispose()
        {
            if (_monitor != null)
            {
                DdMonitor.Exit(_monitor);
                _monitor = null;
            }
        }
    }
    
    
    Enter 方法和三个 TryEnter 方法中的两个仅仅是第三个 TryEnter 方法的包装,如图 3 所示。这些实现的目的是为了遵循 Monitor 的 Enter 和 TryEnter 方法所实现的相同规范。调用 Enter 会阻塞,直到获得锁(我们在设计上略有不同,即如果检测到死锁,我们的 Enter 方法会引发异常而不是永远阻塞),从而委托给 TryEnter 并带有一个 Timeout.Infinite 超时。请注意,Timeout.Infinite 等于 -1,这是一个特殊值,目的是通知 TryEnter 在获得锁之前阻塞,而不是在特定时间后失败。相比较而言,不接受时间值的 TryEnter 重载默认情况下使用 0 作为超时,这意味着如果不能立即获得锁,它会返回 false(同样,如果获得锁会导致死锁,我们的实现也将引发异常;但是这个与 TryEnter 有关的决定具有争议性,因此您可以选择相应地修改该实现)。请注意,如果已获得锁,TryEnter 会返回 true,反之返回 false。TryEnter 的第二个重载仅仅将所提供的 TimeSpan 超时值转换为一个毫秒值、验证参数并委托给最终的 TryEnter 重载,所有真正的工作都在此发生。
    public static void Enter(object monitor)
    {
        TryEnter(monitor, Timeout.Infinite);
    }
    
    public static bool TryEnter(object monitor)
    {
        return TryEnter(monitor, 0);
    }
    
    public static bool TryEnter(object monitor, TimeSpan timeout)
    {
        long totalMilliseconds = (long)timeout.TotalMilliseconds;
        if (totalMilliseconds < -1 ||
            totalMilliseconds > Int32.MaxValue) 
                throw new ArgumentOutOfRangeException("timeout");
        return TryEnter(monitor, (int)totalMilliseconds);
    }
    
    
    我们的设计相对简单,尽管它确实包含一些有趣的实现细节。执行实现前先验证所提供的参数,确保真正提供了要锁定的对象并提供了有效的超时(意味着 -1 或者非零的毫秒数)。
    此时,DdMonitor 需要访问一些共享状态。因为没有方法可以支持 DdMonitor 向 CLR 询问关于监视器的信息(包括哪些线程可能占有监视器以及哪些可能在等待),DdMonitor 必须手动跟踪这些信息。它达成此目的的方法是存储一个静态数据结构表,其中包含与系统中已获得监视器相关的所有信息,每次在监视器上执行 Enter 或 Exit 操作时,它便更新此共享状态。
    请注意,这里有几个含义。首先,虽然我们看到 DdMonitor 最终确实将用户提供的同一监视器对象委托给 Monitor,但是 DdMonitor 对直接调用 Monitor 的方法一无所知,因此它无法根据任何此类已进行的调用更新其内部状态信息。这意味着如果在应用程序中混合使用 DdMonitor 和 Monitor(或者 lock 关键字),死锁检测将无法正常工作,从而导致“假阴性”并且可能无法预防一些死锁。
    此实现的另一个含义是,此静态表是特定于 AppDomain 的。这应该不是问题,因为您锁定的对象应只存在于一个 AppDomain 中。但是,有一些特殊类型的对象(例如 Type 和 String)能够穿越 AppDomain 的边界,如果用来自多个域的 DdMonitor 锁定这些“领域敏捷”对象中的一个,DdMonitor 在各个域中维护的静态表将只会包含相关信息的子集,同样可能会导致“假阴性”。
    这个方法的第三个问题与可靠性相关。C# 中的 lock 语句扩展到对 Monitor.Enter 的调用,后面紧随 try 块,该块的主体与 lock 语句的主体相同,其 finally 块则释放监视器。通过一点 JIT hackery,CLR 可确保在调用 Monitor.Enter 成功的情况下进入 try 块;这确保了在 Monitor.Enter 成功的情况进入 finally 块,否则在调用 Monitor.Enter 和进入 try 块之间就可能产生异步异常,就此而言这相当重要(有关更多信息,请参阅 msdn.microsoft.com/msdnmag/issues/05/10/Reliabilitywww.bluebytesoftware.com/blog/2007/01/30/MonitorEnterThreadAbortsAndOrphanedLocks.aspx)。但是,正如您即将在我们的 DdMonitor 实现中要看到的,在对 Monitor.Enter 的实际底层调用之后以及调用 DdMonitor.Enter 之后的 try 块之前,有代码存在;因此我们的实现中将失去这些可靠性保证。
    您应该牢记所有这些问题,以防止对您的方案形成任何危险。
    回到我们的 TryEnter 实现,如图 4 所示,它下一步是为此共享状态获取其内部锁。一旦获得,它将访问词典来查找所进入的监视器上之前的任何数据。如果不存在这样的数据,就表示目前没有线程占有或等待监视器。在这种情况下,TryEnter 会初始化一个新的 MonitorState 对象来跟踪此监视器,并将其放回表中。
    private static Dictionary<object, MonitorState> _monitorStates = 
        new Dictionary<object, MonitorState>();
    
    public static bool TryEnter(object monitor, int millisecondsTimeout)
    {
        if (monitor == null) throw new ArgumentNullException("monitor");
        if (millisecondsTimeout < 0 && 
            millisecondsTimeout != Timeout.Infinite) 
                throw new ArgumentOutOfRangeException("millisecondsTimeout");
    
        bool thisThreadOwnsMonitor = false;
        MonitorState ms = null;
        try
        {
            lock (_globalLock)
            {
                if (!_monitorStates.TryGetValue(monitor, out ms))
                {
                    _monitorStates[monitor] = ms = new MonitorState(monitor);
                }
    
                if (ms.OwningThread != Thread.CurrentThread)
                {
                    ms.WaitingThreads.Add(Thread.CurrentThread);
                    ThrowIfDeadlockDetected(ms);
                }
            }
    
            thisThreadOwnsMonitor = Monitor.TryEnter(
                monitor, millisecondsTimeout);
        }
        finally
        {
            lock (_globalLock)
            {
                if (ms != null)
                {
                    ms.WaitingThreads.Remove(Thread.CurrentThread);
    
                    if (thisThreadOwnsMonitor)
                    {
                        if (ms.OwningThread != Thread.CurrentThread) 
                            ms.OwningThread = Thread.CurrentThread;
                        else ms.ReentranceCount++;
                    }
                }
            }
        }
    
        return thisThreadOwnsMonitor;
    }
    
    
    private class MonitorState
    {
        public MonitorState(object monitor) { MonitorObject = monitor; }
        public object MonitorObject;
        public Thread OwningThread;
        public int ReentranceCount;
        public List<Thread> WaitingThreads = new List<Thread>();
    }
    
    MonitorState 存储了一些关于监视器的信息。首先,它存储监视器本身;系统使用它仅仅是为了在检测到死锁时提供诊断信息(因为了解哪些监视器陷入死锁循环对程序调试很有帮助)。MonitorState 还为目前占有监视器的任意线程存储 Thread 对象(如果目前没有获得,此值为空)、表明已进入监视器次数的 ReentranceCount(因为占有监视器的线程能安全地重复进入监视器),以及表明目前正在尝试获得此监视器的等待线程的列表。
    现在有了此监视器的 MonitorState 跟踪对象之后,TryEnter 将当前的 Thread 添加到 MonitorState 的 WaitingThreads 列表中,从而注册该线程等待对象的意图。一旦此意图得到注册,TryEnter 就会调用私有 ThrowIfDeadlockDetected 方法,该方法的名称就是其能够实现的功能(我们稍后将会看一下此方法的实现)。如果 ThrowIfDeadlockDetected 检测到获得此锁将导致死锁循环,它会引发异常来防止获得该锁。同时请注意,在执行此操作时,包装此调用的 finally 块将会从等待线程列表中删除当前线程。
    如果没有检测到死锁,此线程试图获得监视器将是安全的,TryEnter 仅仅委托 Moni- tor.TryEnter 进入实际的监视器。不论是否获得锁,随后都需要共享状态的锁。请注意,在这里我有自己的锁级别方案来防止 DdMonitor 引起死锁(从本来目的就是为了防止死锁的工具而言,这真是非常糟糕的事) 当在用户提供的监视器上占用锁时,获得 globalLock 是可以的,但是反之则不行。这就是我可以在 finally 块(其中我可能已经占有了用户提供的监视器)中得到 _globalLock 上的锁的原因,但是当占用 _globalLock 时就不能试图获得用户提供的监视器上的锁,这就是我在调用 Monitor.TryEnter 前先退出 _globalLock 的原因。
    此时,不论用户提供的监视器是否被成功获得,我都从监视器的等待列表中删除当前线程(它要么占有监视器要么等待失败,因此不论何种情况,都不需要继续等待)。接下来,如果监视器被获得,MonitorState 中的相关字段会得到更新。
    图 5 所示,退出锁更加简单。除了参数验证之外,整个主体都封装到 _globalLock 监视器上的一个锁中。用户提供的监视器上的 MonitorState 将被检索(如果它不存在,说明此监视器没有锁定,并引发异常),而且会检查它以确定哪个线程占有它。如果无人占有它,或占有者不是当前线程,说明调用 Exit 出现错误,并会引发异常。否则,如果当前线程已经多次进入此监视器,ReentranceCount 即会减少,如果没有进入,OwningThread 将被设为空,以表示在监视器上释放此线程的锁。此外,如果没有线程在等待此监视器,MonitorState 会从表中删除,这样就可对它进行垃圾收集处理(如果我们不这样做的话,表会随着时间的推移无限增大)。最后,方法委托 Monitor.Exit 释放实际的监视器。
    public static void Exit(object monitor)
    {
        if (monitor == null) throw new ArgumentNullException("monitor");
    
        lock (_globalLock)
        {
            MonitorState ms;
            if (!_monitorStates.TryGetValue(monitor, out ms)) 
                throw new SynchronizationLockException();
    
            if (ms.OwningThread != Thread.CurrentThread)
            {
                throw new SynchronizationLockException();
            }
            else if (ms.ReentranceCount > 0)
            {
                ms.ReentranceCount--;
            }
            else
            {
                ms.OwningThread = null;
                if (ms.WaitingThreads.Count == 0)
                    monitorStates.Remove(monitor);
            }
    
            Monitor.Exit(monitor);
        }
    }
    
    
    接下来的内容很有意思。除了实际的死锁检测算法之外,我们已经看到了完整的实现,其核心已在 ThrowIfDeadlockDetected 中实现(请参见图 6),之前我们已经在从 DdMonitor.Enter 的调用中看到。
    private static void ThrowIfDeadlockDetected(MonitorState targetMs)
    {
        if (targetMs.OwningThread == null) return;
    
        Dictionary<Thread, List<MonitorState>> locksHeldByThreads;
        Dictionary<MonitorState, List<Thread>> threadsWaitingOnLocks;
        CreateThreadAndLockTables(
            out locksHeldByThreads, out threadsWaitingOnLocks);
    
        Queue<CycleComponentNode> threadsToFollow = 
            new Queue<CycleComponentNode>(locksHeldByThreads.Count);
    
        threadsToFollow.Enqueue(new CycleComponentNode(
            Thread.CurrentThread, targetMs, null));
    
        while (threadsToFollow.Count > 0)
        {
            CycleComponentNode currentChain = threadsToFollow.Dequeue();
            Thread currentThread = currentChain.Thread;
    
            List<MonitorState> locksHeldByThread;
            if (!locksHeldByThreads.TryGetValue(
                currentThread, out locksHeldByThread)) continue;
    
            foreach (MonitorState ms in locksHeldByThread)
            {
                List<Thread> nextThreads;
                if (!threadsWaitingOnLocks.TryGetValue(
                    ms, out nextThreads)) continue;
    
                foreach (Thread nextThread in nextThreads)
                {
                    if (currentChain.ContainsThread(nextThread)) 
                        throw new SynchronizationLockException(
                            CreateDeadlockDescription(
                                currentChain, locksHeldByThreads));
    
                    threadsToFollow.Enqueue(new CycleComponentNode(
                        nextThread, ms, currentChain));
                }
            }
        }
    }
    
    
    ThrowIfDeadlockDetected 方法接受 MonitorState 实例作为参数,该实例代表了将要等待的监视器。要出现死锁,此监视器(当前线程即将等待它,我们已经注册了该意图)需要由直接或间接占用它的线程占用,而该线程则正在等待当前线程占用的某个监视器。我说“直接或间接占用”的意思是,要么它已获得监视器上的锁,要么它正在等待由直接或间接占用此监视器的线程占用的某个监视器(对不起,我喜欢饶舌)。换句话说,我们在寻找一个循环,从此监视器开始,到占用它的线程,到该线程正在等待的监视器,到占有该监视器的线程,等等,直到我们回到最初的监视器和占有它的线程。如果我们能找到这样的循环,就引发代表死锁的异常。
    实现遵循该方法。首先,我们检查以确保目标 MonitorState 真正由线程占有,如果不是,等待它就没有问题。接着,我们根据当前拥有的所有信息(有关锁占有的对象以及在监视器上等待的对象)生成查找表(所有这些信息都在 _monitorState 表中,所以我们只需将该信息重构为两个 Dictionary 数据结构,即可使其余的算法变得更容易实现)。
    此时,我们需要开始检查线程。我们从当前的线程开始,我们知道它正在等待所提供的 MonitorState。为了捕获这一配对,我创建了一个简单的帮助器容器 CycleComponentNode,除了存储此数据之外,还同时维护对在识别出的循环中下一个节点的引用。开始算法时,我们唯一了解的节点是初始线程和 MonitorState,因此 Next 值为空。
    下一个将要检查的线程(或者更确切地说是存储该线程的 CycleComponentNode)会从队列中检索到,直到没有线程可检查为止。使用之前创建的表,我们能检索到此该线程占用的所有锁,如果它没有占用任何锁,它就无法成为死锁的一部分(请记住死锁的必备条件,特别是那条关于占用一个资源并请求另一个的条件)。这些锁中的每一个都得到检查,再次使用之前创建的表,我们可以查找哪些线程正在等待这些锁。如果任何这些线程存在于我们构建的当前等待链中(由 CycleComponentNode 以列表的形式表示),我们就找到了死锁! CreateDeadlockDescription 方法枚举了链中的所有 CycleComponentNode 实例并生成一个描述,如下所示(接着就引发异常):
    Thread 24 waiting on lockP (39C82F0) 
        while holding lockH (36BEF47), lockK (3B3D5E3)
    Thread 30 waiting on lockH (36BEF47) 
        while holding lockB (2CF7029), lockN (349C40A
    Thread 29 waiting on lockB (2CF7029) 
        while holding lockP (39C82F0)
    
    如果线程不存在于当前等待链中,我们仍需检查它。因此,我们围绕它及其正在等待的 MonitorState 创建一个 CycleComponentNode,它的 Next 值将设置为当前等待链。按此方式,当最终检查此线程和所有在其占用的监视器上等待的线程时,我们可以正确地搜索那些线程的等待链。
    这就是解决方案。如果它不能满足您的所有需求,您当然可以根据需要修改它。例如,当阻塞时,您可以将当前线程的堆栈跟踪存储到 MonitorState 中,接着,当发现死锁时,将所有相关线程的堆栈跟踪都包含在引发异常的消息中。或者您可以扩展该解决方案来支持更多的同步原语,例如 System.Threading.Mutex。
    同时需要注意,如果您编写的是本机代码并且锁定在 Windows 同步原语上,那么就可以利用新的 Wait Chain Traversal API 来执行类似并更强大的分析。有关这方面的更多信息,请参阅 John Robbins 在 2007 年 7 月期的《MSDN® 杂志》上的 Bugslayer 专栏,地址为 msdn.microsoft.com/msdnmag/issues/07/07/Bugslayer。
    最后,还有关于此实现的性能方面的一些看法。我在这里实现的死锁检测代码开销大、清楚且简单。正因为如此,您可以在调试版本而非零售版本中使用它。要实现这个目的有一些方法,但是最简单的就是用一个静态属性扩展 DdMonitor 的实现。
    private static bool _useDeadlockDetection = false;
    
    public static bool UseDeadlockDetection
    {
        get { return _useDeadlockDetection; }
        set { _useDeadlockDetection = value; }
    }
    
    这样的话,您可以扩展 Enter、TryEnter 和 Exit 的实现以检查此属性,并用它来确定是要实现完备的死锁检测算法,还是简单地将其委托给 Monitor 上的相应方法:
    public static void TryEnter(object monitor, int millisecondsTimeout)
    {
        if (UseDeadlockDetection)
        {
             ... // existing logic here
        }
        else TryEnter(monitor, millisecondsTimeout);
    }
    
    要在代码中设置 UseDeadlockDetection,可从配置文件中读取一个值,或者如果您想要根据生成配置设置该值,可使用如下代码:
    public static void Main(string [] args)
    {
        DdMonitor.UseDeadlockDetection =
    #if DEBUG
            true;
    #else
            false;
    #endif
    
        ... // the rest of your app here
    }
    
    将 UseDeadlockDetection 设置为 false 并在整个基本代码中使用 DdMonitor 不会提供与仅使用 Monitor 等同的行为和性能,但是应该非常相近。除了在程序开头,确保您不会再更改 UseDeadlockDetection 的值;如果您确实在其他地方更改它了,那么您最终将会是使用死锁检测来获得一些监视器而非其他目的,这其中的危险我们已经讨论过。Joe Duffy 建议了一个替代办法,就是更改 TryEnter 的实现,首先避免死锁检测代码,然后通过一些可配置的超时委托给 Monitor.TryEnter,超时可以是 500ms(或用户提供的更小的超时)。如果该调用成功,就不需要执行死锁检测,因为监视器已经成功被获得。如果该调用超时,且选择的超时小于用户提供的超时,那么就可以运行完备的死锁检测算法。通过这个办法,已部署程序中的死锁仍然可以发现,但是在第一个 Monitor 获取超时之前,您不用承担检测算法的成本。而且如果您选择了一个合理的超时,那么它可能始终都会因为死锁而超时。
  • 相关阅读:
    使用matplotlib绘制常用图表(3)其他图表绘制
    python简单爬虫
    使用matplotlib绘制常用图表(2)常用图标设置
    一个GISer的使命
    SQL2008″Unable to read the list of previously registered servers on this system”
    Eclipse语言包安装
    eclipse查看JDK源码
    Eclipse中设置javadoc中文帮助文档
    GDI+处理图像时出现内存不足的问题
    利用github搭建私人maven仓库
  • 原文地址:https://www.cnblogs.com/yongbufangqi1988/p/1876874.html
Copyright © 2011-2022 走看看