内存泄漏、多线程Debug技巧总结
- 1. 稳定复现(单线程)
- 2. 变量跟踪(单线程)
- 3. 序列跟踪(单/多线程)
- 4. 死锁问题(单/多线程)
- 5. 内存泄漏、越界(单/多线程)
- 6. 总结
写传统C/C++/Java等程序时经常会遇到内存泄漏、多线程BUG等问题,这些问题时常难以定位(复现)令人头疼,本文介绍几个博主在进行Debug的常常用到的心得体会,供自己回顾、也供人参考
1. 稳定复现(单线程)
在单线程程序中,遇到BUG,首先需要去掉程序中所有不确定因素,
例如:随机数生成、根据CPU号执行不同逻辑等。这样才能够帮助我们分析BUG的起因。该方法适于寻找任何单线程的任何BUG。我们以随机数生成的一段代码为例(假设process在random为12034时出现BUG):
while (True):
random = rand()
process(random)
在调试过程中,我们可以通过两种方式去掉这里的随机因素:
- 设置随机种子
该方法适用于大多数用户态程序,在网络上搜索相应随机数算法如何生成种子即可。 - 固定生成序列
该方法适用于那些不可调节随机种子的随机算法,这里我们可以将上述程序修改如下:
iter = 0
while (True):
iter += 1
random = iter + 1
process(random)
如此一来,我们便能够生成一个固定的序列:2到∞
2. 变量跟踪(单线程)
当能够稳定复现问题之后,便需要了解问题的成因。在输入数据量较大的情况下,单步调试可能很难触发错误。而通过跟踪出错变量可以很好地解决这个问题。
假设我们将上述程序变换为固定生成序列,并且发现random = 12034
时能够稳定复现。于是,我们对添加如下代码做变量跟踪工作:
iter = 0
while (True):
iter += 1
random = iter + 1
# 添加的代码
if (random == 12034):
print("打个断点")
process(random)
利用GDB等调试程序我们可以将断点打在print("打个断点")
处,这样一来就可以通过进入 process函数查看问题。
3. 序列跟踪(单/多线程)
在很多情况下,某一变量可能经过多种操作处理,在时间先后上也有差异,这导致对于单变量的跟踪效率降低。对于多线程的程序该情况更为明显。以下述代码为例:
# 第一次预处理
while (True):
random = rand()
ret = preprocess1(random)
if ret == 0:
break
# 第二次预处理
while (True):
random = rand()
ret = preprocess2(random)
if ret == 0:
break
...
# 第N次预处理
while (True):
random = rand()
ret = preprocessN(random)
if ret == 0:
break
# 正式处理
while (True):
random = rand()
ret = process(random)
if ret == 0:
break
这里我们假设在正式处理阶段出现BUG,并且通过稳定复现变换稳定定位process在random为12034时出现BUG。由于random在process之前被预处理了N次,在N非常大的情况下,变量挨个跟踪的效率是非常低下的。
事实上,我们主要关注的是random被处理后程序的状态发生了什么变化,例如preprocess(random)
是变换某个全局变量的值。因此我们只需要在每次preprocess(12034)
后查看该值是否符合预期即可。这里假设每次处理独立,即preprocess(12033)不影响preprocess(12034)的结果。在这种情况下,我们为程序加入如下跟踪信息:
# 第一次预处理
seed(1) # 种子设置法
while (True):
random = rand()
ret = preprocess1(random)
if (random == 12304):
print(global_state(random))
if ret == 0:
break
# 第二次预处理
while (True):
random = rand()
ret = preprocess2(random)
if (random == 12304):
print(global_state(random))
if ret == 0:
break
...
# 第N次预处理
while (True):
random = rand()
ret = preprocessN(random)
if (random == 12304):
print(global_state(random))
if ret == 0:
break
# 正式处理
while (True):
random = rand()
ret = process(random)
if ret == 0:
break
运行程序后,我们可以将输出重定向到一个文件中(下述命令对Windows与Linux均适用),例如:
program > log
接着打开log
文件你便会得到关于random == 12304
的执行序列结果,详细分析执行序列结果便大概率能找到问题所在。
该方法同样可以作用于多线程,这样便能够屏蔽很多其他杂乱的信息,聚焦于错误点进行针对性调试。
4. 死锁问题(单/多线程)
死锁是代码编写时常常会遇到的问题。不限于多线程,单线程也可能遇上死锁的情况,例如忘记unlock
。
对于单线程的死锁问题,分析起来比较简单,找到使代码卡住的lock
,分析该lock
的锁与解锁对即可。
对于多线程的死锁问题,分析起来较为困难,博主只能介绍个人的经验:
- 单锁问题
如果我们只有一个锁lock
,那么只要保证锁的上锁与解锁成对出现即可。 - 在不同线程中要保证多个锁的上锁与解锁顺序一致
假设我们有lock1
和lock2
两个锁,那么每个线程的锁顺序保持一致,例如,所有线程进入临界区均遵循下述原则:
lock(lock1)
lock(lock2)
...
unlock(lock2)
unlock(lock1)
如果有些线程出现下述代码情况,那么很可能发生死锁。例如线程A(使用上述代码)请求lock1
,线程B(使用下述代码)请求lock2
,接下来,线程A又会请求lock2
,线程B请求lock1
,形成循环等待,死锁。
lock(lock2)
lock(lock1)
...
unlock(lock1)
unlock(lock2)
上述问题是博主经常遇到的死锁问题,其他死锁问题也可以把所有锁的情况写出来,针对问题分析资源等待请求情况以及死锁原因。
5. 内存泄漏、越界(单/多线程)
这里首推valgrind
工具,相关介绍参考其官网。
6. 总结
本文分享了博主在进行程序开发时遇到的一系列问题以及一些解决与定位方法。本博文将持续更新,OK,不多说了,起飞🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫🛫