一 业务场景分析
因为多线程在平时是非常常见的,最近有点空,想自己写个线程池而不是用别人写好的。
所以开撸,发现当我在调试一个线程池时,发现使用了一两个小时后,虚拟内存占用得非常高。然后我开始分析,一开始我先写了一个不带调整线程的线程池,发现线程池开启几个小时后,虚拟内存都是很稳定,基本也就正常的几百m。而当我添加了调整线程后,发现同样一个小时多后,虚拟内存变得非常的大,达到20g左右。
注:这里调整线程的作用是:可以根据任务数的大小自动调整线程池中线程的数量,所以就涉及线程的添加和销毁。
1 测试
经过上面发现问题后,肯定想排查问题,于是开启测试。
这是我测试的情况:
-
这是添加调整线程的情况,可以看到经过一两个小时后,虚拟内存都变得非常的大。
下面是C语言写的带调整线程的线程池,情况和上面C++的一样。
-
然后我就把调整线程的代码去掉,发现果然情况变得不一样,经过类似的时间,虚拟内存仍然是一样的,非常稳定。
2 写测试代码
由于线程池添加了调整线程这些内容,测起来不方便,于是就自己写个程序测试。
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void *thread1(void *arg)
{
printf("thread1\n");
return NULL;//创建线程但不回收
}
int main(){
getchar();
printf("ok\n");
pthread_t tid1;
while (getchar())
{
printf("start create thread.\n");
pthread_create(&tid1, NULL, thread1, NULL);
}
return 0;
}
- 首先运行程序
./main
- 然后查看该进程的pid。我的是22651。
ps -ajx | grep main
- 使用top观察虚拟内存。一开始是15428,大概15M左右。
top -p 22651
- 然后查看开辟一个线程所占用的堆栈大小。
ulimit -a //查看stack size那一行
或者
ulimit -s
下面看到是8m。
单位需要从ulimit -a查看,单位是kb。
- 好了,目前我们可以开始调程序观察了。
1)下面看到,由于我不小心按到其它键,创建了两个线程。然后查看VirMem大小,大小为31820,刚好和15428 + 2*8192=31812差不多相等。
2)然后再创建线程,按下enter即可。同样看到VirMem=40016,刚好和31812 + 8192=40004差不多相等。
3)然后再创建线程,按下enter即可。同样看到VirMem=48212,刚好和40004 + 8192=48196差不多相等。
4)然后你不断重复创建线程,情况都和上面一样。所以我们可以分析程序了。
-
程序可以看到,当我们创建线程后,并未对线程进行回收,而是直接return了。所以我们只需要证明调用join回收后,观察虚拟内存的情况,若未增加,就验证了我们的猜想,是没有回收导致的,否则就可能需要从其它方面入手。
-
更改代码,添加一个调整线程去回收退出后的线程,观察情况。
先列出代码:
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <vector>
using namespace std;
vector<pthread_t> garbage;//垃圾回收,记录退出线程的tid,以便join回收
pthread_mutex_t mymutex;//锁住垃圾队列
void *thread1(void *arg)
{
printf("thread1\n");
pthread_t t = pthread_self();
pthread_mutex_lock(&mymutex);
garbage.push_back(t);
pthread_mutex_unlock(&mymutex);
return NULL;//创建线程但不回收
}
//调整线程,回收资源
void *adjust(void *arg)
{
while(true)
{
sleep(2);//定时回收
pthread_mutex_lock(&mymutex);
int size = garbage.size();
vector<pthread_t>::iterator it = garbage.begin();
if(garbage.size() >= 0){
for(it; it != garbage.end(); it++)
{
pthread_join(*it, NULL);
}
garbage.clear();
}
pthread_mutex_unlock(&mymutex);
printf("清理完本次退出的线程,个数为=%d\n", size);
}
pthread_exit(NULL);
}
int main(){
pthread_mutex_init(&mymutex, NULL);
//先创建一个调整线程,用于定时回收资源
pthread_t adjust_tid;
pthread_create(&adjust_tid, NULL, adjust, NULL);
getchar();
printf("ok\n");
pthread_t tid1;
while (getchar())
{
printf("start create thread.\n");
pthread_create(&tid1, NULL, thread1, NULL);
}
pthread_mutex_destroy(&mymutex);
return 0;
}
同理,安装上面一开始的情况,去top -p pid去观察创建线程后的Virt。
我的情况是,当虚拟内存到达一定情况后,一般是几百M,和你程序初始有关,我这里到达187m左右,无论你开辟多少线程,过了2s内被回收后,最终还是变回187m。也就是说,Virt的大小始终保持稳定。
我的测试情况:
1)下图是我创建一定线程并回收后,最终稳定的情况值。
2)然后我们再程序的界面上,不断的按下enter,main主线程就会创建多个线程,此时top的Virt可以看到秒增加变大。
3)然后回收后,立马回到187m。
下图由于比较难截图,只能使用windows自带的截图,比较模糊但是还是能看的,回收的线程数是36。
所以由上面的测试结果可以看到,导致虚拟内存变高的原因是我们线程结束后,未使用join进行回收。
实际上,我们测试时可以使用detach去代替的。情况和上面类似,只不过最终稳定的值比上面低点,大概24m左右。
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <vector>
using namespace std;
void *thread1(void *arg)
{
pthread_detach(pthread_self());
printf("thread1\n");
return NULL;//这里等价于pthread_exit();
}
int main(){
getchar();
printf("ok\n");
pthread_t tid1;
while (getchar())
{
printf("start create thread.\n");
pthread_create(&tid1, NULL, thread1, NULL);
}
return 0;
}
3 总结
虚拟内存非常高,导致的情况可能是非常多的,我这里是因为添加调整线程后,部分退出的线程pthread_exit退出后,未调用join进行回收,导致在虚拟内存占用高,但是在实际的物理内存中,它的值是正常的。
虽然部分场景下,只有物理内存维持正常不爆增,虚拟内存过高还是可以让程序正常进行的,但是程序员必须防范于未然,而且看着也难受,既然找到bug,后续就自己动手改代码咯。
关于其它出现虚拟内存占用高的情况的文章:
为什么linux下多线程程序如此消耗虚拟内存