0

0

ReaderWriterLockSlim的LockRecursionException怎么避免?

月夜之吻

月夜之吻

发布时间:2025-08-16 09:14:01

|

825人浏览过

|

来源于php中文网

原创

lockrecursionexception的根源是线程在持有锁时重复获取同类型锁,因readerwriterlockslim默认非递归;2. 解决方法包括使用enterupgradeablereadlock()实现安全升级、严格遵循try/finally释放锁;3. 避免在嵌套调用中隐式重入,需重构代码以明确锁边界;4. 非递归设计旨在提升性能并防止死锁,强制开发者清晰管理锁生命周期;5. 定位异常需分析堆栈、审查代码、添加日志及编写并发测试;6. 虽无内置递归读写锁,但可通过重构、缩小锁范围或使用monitor/mutex等替代方案应对,自定义递归锁风险高不推荐。应将该异常视为设计警示而非单纯技术问题,通过优化并发结构从根本上解决。

ReaderWriterLockSlim的LockRecursionException怎么避免?

LockRecursionException
在使用
ReaderWriterLockSlim
时,通常是因为线程试图重复获取它已经持有的锁,而
ReaderWriterLockSlim
默认是非递归的。避免它的核心在于理解其非递归特性,并严格遵循正确的锁获取、释放及升级降级模式。

解决

LockRecursionException
的关键在于对
ReaderWriterLockSlim
的工作机制有深刻的理解,特别是它的非递归特性和特有的锁升级/降级路径。

我个人在项目中遇到这玩意儿,大部分时候都是因为“想当然”地认为某个方法里拿了读锁,然后调用的另一个方法里又去拿读锁,或者更常见的,在读锁内部想直接升级到写锁,结果就炸了。

最直接的办法是:

  1. 认识到

    ReaderWriterLockSlim
    默认是非递归的: 这意味着一个线程不能在持有某个锁(无论是读锁还是写锁)的情况下,再次尝试获取同类型的锁。如果你在持有读锁时再次调用
    EnterReadLock()
    ,或者在持有写锁时再次调用
    EnterWriteLock()
    ,都会抛出异常。

  2. 利用

    EnterUpgradeableReadLock()
    进行锁升级: 这是最容易出错的地方。如果你在持有普通读锁(
    EnterReadLock()
    )的情况下,试图通过
    EnterWriteLock()
    来获取写锁,那肯定会失败。正确的方式是先获取一个“可升级的读锁”:
    EnterUpgradeableReadLock()

    • 在持有可升级读锁期间,你可以安全地获取普通读锁(
      EnterReadLock()
      )进行读操作。
    • 当你需要进行写操作时,可以从可升级读锁升级到写锁:
      EnterWriteLock()
    • 完成写操作后,先释放写锁(
      ExitWriteLock()
      ),再释放可升级读锁(
      ExitUpgradeableReadLock()
      )。
    • 记住,可升级读锁在同一时刻只能被一个线程持有,这保证了升级到写锁时的独占性。
  3. 严格遵循

    try/finally
    模式: 任何锁的获取都必须搭配相应的释放。这是并发编程的基本原则,能有效防止因异常导致锁无法释放,进而引发死锁或资源耗尽。

    var rwLock = new ReaderWriterLockSlim();
    
    // 读操作示例
    rwLock.EnterReadLock();
    try
    {
        // 安全地读取共享资源
    }
    finally
    {
        rwLock.ExitReadLock();
    }
    
    // 写操作示例
    rwLock.EnterWriteLock();
    try
    {
        // 安全地修改共享资源
    }
    finally
    {
        rwLock.ExitWriteLock();
    }
    
    // 升级场景示例:先读后写
    rwLock.EnterUpgradeableReadLock(); // 关键一步!
    try
    {
        // 在这里可以进行读操作
        // 如果需要修改,则升级
        if (someConditionRequiresWrite)
        {
            rwLock.EnterWriteLock();
            try
            {
                // 执行写操作
            }
            finally
            {
                rwLock.ExitWriteLock();
            }
        }
    }
    finally
    {
        rwLock.ExitUpgradeableReadLock();
    }
  4. 避免嵌套调用中的隐式重入: 有时候,你可能在一个方法A中获取了锁,然后A又调用了方法B,而B中也尝试获取了同一个锁。这种情况下,如果锁是非递归的,就会抛出异常。这时你需要审视你的设计:是方法B不应该获取锁?还是方法A在调用B之前就应该释放锁?或者,考虑将共享资源的操作封装得更细粒度,让锁的范围更小。

为什么
ReaderWriterLockSlim
默认是非递归的?这种设计有什么考量?

这个问题挺有意思的,也是我一开始用的时候百思不得其解的地方。

ReaderWriterLockSlim
之所以默认是非递归的,主要是出于性能和避免死锁的考量。

你想啊,如果一个锁允许递归,那么一个线程可以反复进入同一个锁。这听起来很方便,但它会带来额外的开销。每次进入和退出都需要记录锁的重入计数,这无疑增加了锁的内部复杂度和性能损耗。对于一个旨在提供高性能读写分离的锁来说,这种开销是需要权衡的。

更深层次的原因在于,递归锁往往会掩盖潜在的设计问题。当一个线程可以递归地获取锁时,开发者可能会不经意间写出复杂的、相互依赖的锁定逻辑,这大大增加了死锁的风险。想象一下,线程A持有锁L1,然后尝试获取L2;同时线程B持有L2,然后尝试获取L1。这就是经典的死锁。如果L1和L2都是递归锁,情况会变得更复杂,因为一个线程可能在持有L1的情况下又递归地获取了L1,然后才尝试获取L2。非递归锁强制你清晰地规划锁的边界和生命周期,让死锁更容易被发现和避免。它迫使你思考:“我现在持有这个锁,接下来我调用的代码会不会也需要这个锁?如果会,那是不是我的设计有问题?”这种“不方便”恰恰是一种设计上的约束,旨在引导开发者写出更健壮、更清晰的并发代码。

所以,非递归是默认的选择,因为它简单、高效,并且能有效避免一些常见的并发陷阱。如果你真的需要递归锁,.NET提供了

Monitor
Mutex
,它们默认就是递归的,但它们的性能特性和适用场景与
ReaderWriterLockSlim
不同。

绿色大气茶叶网站源码下载1.0
绿色大气茶叶网站源码下载1.0

PHPWEB绿色大气茶叶网站源码下载,源码为PHPWEB 2.05 的商业版。本来是为某人制作的网站,在制作之前,问及什么要求。说是没要求,然后按照某某网站来做即可。(即这套程序的1.X的版本)。我再三确认是否有别的要求。都说没有,然后在发给他看的时候又说不满意,完全和那边的站点一样。哎哟我的妈,当初要求就这样,我不按照这个来做怎么做?现在免费发布出来给大家吧!

下载

如何诊断和定位
LockRecursionException
的发生位置?

诊断这种异常,其实和诊断其他运行时异常没什么太大区别,关键在于看堆栈信息。当

LockRecursionException
抛出时,异常信息会告诉你哪个线程尝试了非法的重入。

我通常会这样做:

  1. 查看异常堆栈: 这是最重要的信息源。堆栈会清晰地显示从哪里开始,一步步调用到哪个方法,最终导致了

    EnterReadLock()
    EnterWriteLock()
    EnterUpgradeableReadLock()
    的第二次(或不合法的)调用。通常,你会看到异常是在
    ReaderWriterLockSlim
    的内部方法中抛出的,但你需要往上追溯,找到你自己的代码中导致这个调用的那一层。

  2. 代码审查: 拿到堆栈信息后,回到代码中,顺着调用链看。特别关注那些在锁内部调用了其他方法的情况。比如:

    public void MethodA()
    {
        _rwLock.EnterReadLock();
        try
        {
            MethodB(); // 如果MethodB内部也尝试获取_rwLock,就可能出问题
        }
        finally
        {
            _rwLock.ExitReadLock();
        }
    }
    
    public void MethodB()
    {
        _rwLock.EnterReadLock(); // 这里的重入就会导致异常
        try { /* ... */ }
        finally { _rwLock.ExitReadLock(); }
    }

    或者更隐蔽的,

    MethodB
    可能没有直接获取锁,但它调用的
    MethodC
    获取了,这就需要你一层层剥开看。

  3. 日志记录: 在复杂的系统中,如果异常难以复现,可以在

    EnterXLock()
    ExitXLock()
    的前后加入详细的日志,记录当前线程ID、锁的状态以及尝试获取/释放的锁类型。这能帮助你在生产环境中追踪问题。当然,这会引入一些性能开销,所以通常只在调试阶段或特定问题复现时启用。

  4. 单元测试/集成测试: 针对并发代码编写测试用例是必不可少的。模拟多线程竞争和特定操作序列,可以帮助你在开发阶段就发现这类问题。例如,测试一个线程先获取读锁,再尝试获取写锁(没有

    UpgradeableReadLock
    ),看它是否按预期抛出异常。

定位这类问题,很多时候考验的是你对整个模块甚至系统锁粒度的理解。它不是一个简单的语法错误,而是一个并发逻辑的设计问题。

在什么情况下,我可能需要一个递归的读写锁,或者说有没有替代方案?

嗯,有时候你就是会觉得,哎呀,如果这个锁能递归多好啊,省得我改那么多地方。这种想法通常出现在你有一个复杂的、多层调用的方法体系,并且这些方法在不同层级都需要访问或修改共享资源。

确实,如果你的设计模式就是这样,或者说重构代价太大,你可能会渴望一个递归的读写锁。但遗憾的是,.NET标准库中并没有直接提供一个

ReaderWriterLockSlim
的递归版本。前面提到了,
Monitor
Mutex
是递归的,但它们是独占锁,无法提供读写分离的并发优势。

那么,替代方案或者说应对策略有哪些呢?

  1. 重构代码,消除递归需求: 这是最推荐也最根本的解决方案。如果一个方法在持有锁的情况下又去调用另一个也需要这个锁的方法,这通常意味着你的锁粒度过大,或者职责划分不清。
    • 缩小锁的范围: 让锁只保护真正需要保护的那一小段代码,而不是整个方法。
    • 传递锁状态: 如果一个内部方法确实需要知道外部是否已经持有锁,可以考虑将锁的持有状态作为参数传递进去,或者让内部方法在外部锁的保护下执行,而不必自己再次获取锁。
    • 将共享资源操作封装成原子单元: 确保每个操作都是独立的,不需要依赖于外部的锁状态。
  2. 使用
    Monitor
    Mutex
    (如果读写分离不是核心需求):
    如果你的并发瓶颈主要不在于读多写少,而在于简单的资源独占,那么
    Monitor
    lock
    关键字的底层)或
    Mutex
    可能更适合。它们是递归的,使用起来相对简单,但并发性能不如
    ReaderWriterLockSlim
  3. 自定义递归锁(不推荐,但理论可行): 这是一个高级且风险很高的选项。你可以基于
    ReaderWriterLockSlim
    或其他同步原语,自己实现一个带有重入计数功能的递归锁。但这会引入巨大的复杂性,包括正确处理线程ID、重入计数、死锁预防等等。我个人强烈不建议在生产环境尝试这种方案,除非你对并发编程有极其深厚的理解,并且有充分的测试来验证其正确性。通常,这种“解决方案”反而会引入更多难以调试的并发问题。

总的来说,当你遇到

LockRecursionException
时,第一反应不应该是寻找一个递归的读写锁,而是应该反思你的并发设计。它是一个信号,告诉你当前的代码结构可能在并发环境下存在隐患,需要更精细的同步控制。很多时候,通过调整方法调用链、细化锁粒度,这个问题就能迎刃而解。

相关专题

更多
堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

389

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

572

2023.08.10

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

389

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

572

2023.08.10

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

480

2023.08.10

Python 多线程与异步编程实战
Python 多线程与异步编程实战

本专题系统讲解 Python 多线程与异步编程的核心概念与实战技巧,包括 threading 模块基础、线程同步机制、GIL 原理、asyncio 异步任务管理、协程与事件循环、任务调度与异常处理。通过实战示例,帮助学习者掌握 如何构建高性能、多任务并发的 Python 应用。

143

2025.12.24

C++ 单元测试与代码质量保障
C++ 单元测试与代码质量保障

本专题系统讲解 C++ 在单元测试与代码质量保障方面的实战方法,包括测试驱动开发理念、Google Test/Google Mock 的使用、测试用例设计、边界条件验证、持续集成中的自动化测试流程,以及常见代码质量问题的发现与修复。通过工程化示例,帮助开发者建立 可测试、可维护、高质量的 C++ 项目体系。

6

2026.01.16

java数据库连接教程大全
java数据库连接教程大全

本专题整合了java数据库连接相关教程,阅读专题下面的文章了解更多详细内容。

28

2026.01.15

Java音频处理教程汇总
Java音频处理教程汇总

本专题整合了java音频处理教程大全,阅读专题下面的文章了解更多详细内容。

12

2026.01.15

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号