多线程环境下无锁哈希表remove函数挂死问题求助
看起来你的无锁哈希表单线程逻辑是通顺的,但多线程场景下的挂死大概率是因为遍历过程中没处理链表被其他线程并发修改的情况,导致线程陷入无限循环或者访问了无效的节点状态。我来帮你梳理几个可能的问题点和修复思路:
首先先明确你的场景:你实现的是基于链表的无锁哈希表,删除时暂时不释放节点(留到哈希表销毁时统一处理内存),单线程下remove功能正常,但多线程调用时总会挂死。你的remove函数用了原子加载和CAS来保证并发安全,但逻辑上存在几个关键漏洞。
问题1:遍历过程中prev节点的有效性未被验证
你的代码里,当遍历到prev = current后,直接基于这个prev去操作它的m_next,但这里有个致命隐患:在你把prev更新为current的瞬间,其他线程可能已经把current节点从链表中删除了!
举个具体的并发场景:
- 线程A正在处理
prev = nodeA,current = nodeB(nodeA的next是nodeB),发现nodeB的val不是目标值,于是把prev设为nodeB,准备加载nodeB的next。 - 与此同时,线程B找到了nodeB的前驱节点,通过CAS把nodeB从链表中彻底删除了。
- 这时候线程A的
prev指向的是已经被“踢出”链表的nodeB,后续基于这个无效节点的操作,要么会访问到错误的m_next指针,要么会在CAS时永远失败,导致线程A无限循环,最终表现为挂死。
问题2:CAS失败后的遍历逻辑可能导致活锁
当CAS失败时,你直接continue回到循环开头重新遍历,这在高并发场景下会导致线程反复从头开始——如果多个线程同时修改同一条链表,会出现频繁的CAS失败,线程一直在重复遍历,看起来就像程序挂死了。
问题3:未处理遍历中途链表被截断的情况
假设在你遍历到某个节点时,其他线程把当前节点的前驱到当前节点的链接断开了,你的代码没有检测这种情况,会继续基于旧的链表结构往下走,最终可能陷入死循环。
修复思路与修改后的代码
核心修复点是在遍历的每一步都验证链表的链接关系是否有效,也就是确保prev->m_next仍然指向current,避免基于无效的节点关系操作。同时优化CAS失败后的逻辑,减少不必要的全量重试。
这里是修改后的remove函数参考:
int remove_item(HM *hm, long val) { if (!hm) { return 1; } size_t index = hash_function(val) % hm->n_buckets; while (1) { Node_HM* prev = hm->buckets[index]->sentinel; Node_HM* current = __atomic_load_n(&prev->m_next, __ATOMIC_ACQUIRE); while (current != NULL) { // 关键:先验证prev和current的链接是否未被其他线程修改 Node_HM* prev_next = __atomic_load_n(&prev->m_next, __ATOMIC_ACQUIRE); if (prev_next != current) { break; // 链接被篡改,重新从sentinel开始遍历 } long current_val = __atomic_load_n(¤t->m_val, __ATOMIC_ACQUIRE); if (current_val == val) { Node_HM* next = __atomic_load_n(¤t->m_next, __ATOMIC_ACQUIRE); // 尝试CAS删除current节点 if (__atomic_compare_exchange_n(&prev->m_next, ¤t, next, 0, __ATOMIC_RELEASE, __ATOMIC_RELAXED)) { // 保持你的逻辑:暂时不释放节点 return 0; } else { break; // CAS失败,重新遍历 } } else { prev = current; current = __atomic_load_n(¤t->m_next, __ATOMIC_ACQUIRE); } } // 遍历到链表末尾都没找到目标值,返回未找到 if (current == NULL) { return 1; } } }
额外的调试建议
如果修改后还是有问题,你可以在多线程环境下加一些安全的调试输出(比如用带互斥锁的fprintf,避免输出混乱),打印每次循环的prev、current指针地址,以及它们的m_val和m_next值,这样能直观看到挂死时线程卡在了哪个步骤——是一直在CAS失败重试,还是访问了无效节点。
另外,你也可以检查一下原子内存序的使用:__ATOMIC_ACQUIRE和__ATOMIC_RELEASE的配对是正确的,但在遍历current->m_next时,用__ATOMIC_RELAXED可能就足够了,不过这不是挂死的直接原因。
内容来源于stack exchange

