apr_thread使用内存之谜

问题起因

问题的起因是因为使用了一个apr的服务,产生了巨大的virtual memory,具体的表现是,在top中可以看到该进程的VIRT和RES,VIRT比实际上使用的要大很多。

在google上找到如下文章

https://lists.apache.org/thread/yvwxmssr90y2nnozj949w5gsg9gpxn1p

怕有人访问不了,直接把原文贴在这里

I have noticed that my multithreaded APR program consumes a *very*
large amount of virtual memory per thread and I can't think of a
reason why. I am running Debian 3.1 (latest kernel 2.6.8-3-686) and I
tried the test program below with both the APR 0.9.x that comes with
Debian and with the latest APR 1.2.x version.
我注意到多线程的APR程序每一个线程会消耗很大的virtual memory,我没有想明白是为什么。
我使用的是Debian 3.1的系统,基于apr版本 APR 0.9.x和 APR 1.2.x

In both cases I end up with around 800 MB of virtual RAM for 100 threads:
20385 ivanr 16 0 802m 956 1800 S 0.0 0.4 0:00.02 test
在如下两个例子中,运行100个线程大致使用了800MBvirtual RAM

Am I doing something wrong or is this a bug? Any help is greatly appreciated!

void* APR_THREAD_FUNC thread_worker(apr_thread_t *thread, void *data) {
apr_thread_exit(thread, 0);
}

int main(int argc, const char * const argv[]) {
apr_pool_t *pool;
int i;

apr_app_initialize(&argc, &argv, NULL);
atexit(apr_terminate);

apr_pool_create(&pool, NULL);

for(i = 0; i < 100; i++) {
apr_thread_t *thread = NULL;
apr_thread_create(&thread, NULL, thread_worker, NULL, pool);
}

apr_sleep(1000 * 1000 * 1000);
}

VIRT & RES & SHR

如果你执行top看一下进程的内存占用,会得到如下几列

  • VIRT
    它是程序“需要”内存的总和,包含了加载的动态库内存,与其他进程共享的内存,以及分配给他的内存;
    但是如果程序申请了100m但是仅仅使用了10m,这里还是按照100m计算统计

  • RES
    它是程序正在使用内存的总和,一个主要区别是如果程序申请了100m但是仅仅使用了10m,这里是按照10m计算统计

  • SHR
    加载的库的内存,如果仅仅是使用库里的一些函数,但是整个库还是会加载入内存。

一个“空白”程序,如下,它会占用多少VIRT,RES,SHR?

int main(int argc, const char * const argv[]) {

    return 0;
}

在这里插入图片描述
它占用了4164kB的VIRT, 368KB的RES, 268KB的SHR。

SHR基本上为动态库加载。一个“什么都没有的"c程序,基础的链接是这样的
在这里插入图片描述

那么程序”自身“多大呢? RES-SHR = 368- 268 = 100KB

程序总共”需要“的大小 VIRT = 4164kB

如果我们申请1m内存再释放它,是否可以跟上述程序暂用内存一致呢?
来试一下

int main(int argc, const char * const argv[]) {
    char *p = (char *)calloc(1048576, sizeof(char)); 
    free(p); // break 1
    return 0; // break 2
}

这里我们增加2个断点,来观察内存使用的变化;

break1

在这里插入图片描述
与上面”空白“程序相比,此时RES是增加1k; 但是VIRT也增长了1k

break2在这里插入图片描述

与上面”空白“程序相比,VIRT是回到了原点。 RES仍有所增加,但是可以理解,毕竟多了几行代码,code也是要占用空间的;
SHR也增加了一点(这个是为什么?)

换成malloc呢?

int main(int argc, const char * const argv[]) {
    char *p;
    for (int i=0; i<1048576; i++)
        p = malloc(sizeof(char));
    //free(p);
    return 0;
}

在不释放内存的情况下,RES可以涨到33M;VIRT涨到37M
在这里插入图片描述

在释放后,内存情况大体也相当; 说明多次调用malloc还是有一定开销的。(这里换成calloc也一样)

int main(int argc, const char * const argv[]) {
    char *p;
    for (int i=0; i<1048576; i++) {
        p = malloc(sizeof(char));
        free(p);
    }
    return 0;
}

在这里插入图片描述

apr_thread测试

1. 使用样例代码(不做thread_join)

上面的帖子跟遇到的情况有些类似,所以我把他的样例程序跑了一下

#include <apr-1/apr.h>
#include <apr-1/apr_pools.h>
#include <apr-1/apr_thread_proc.h>

#define THREAD_NUM 100

void* APR_THREAD_FUNC thread_worker(apr_thread_t *thread, void *data) {
apr_thread_exit(thread, 0);
}

int main(int argc, const char * const argv[]) {
    apr_pool_t *pool;
    int i;
    apr_status_t status = 0;

    apr_app_initialize(&argc, &argv, NULL);
    atexit(apr_terminate);

    apr_pool_create(&pool, NULL);

    apr_thread_t *thread[THREAD_NUM];
    for(i = 0; i < THREAD_NUM; i++) {
        apr_thread_create(&thread[i], NULL, thread_worker, NULL, pool);
    }

    apr_sleep(1000 * 1000 * 1000);
}

样例中运行100个apr_thread

这个是样例代码的结果。

在这里插入图片描述
可以看到RES仅仅1.7M,但是VIRT确实906M,将近一个G了。这个差距也太大了。确实有文章里说的问题。

2. 执行thread_join

如果将所有线程join,如下

#define THREAD_NUM 100

void* APR_THREAD_FUNC thread_worker(apr_thread_t *thread, void *data) {
apr_thread_exit(thread, 0);
}

int main(int argc, const char * const argv[]) {
    apr_pool_t *pool;
    int i;
    apr_status_t status = 0;

    apr_app_initialize(&argc, &argv, NULL);
    atexit(apr_terminate);

    apr_pool_create(&pool, NULL);

    apr_thread_t *thread[THREAD_NUM];
    for(i = 0; i < THREAD_NUM; i++) {
        apr_thread_create(&thread[i], NULL, thread_worker, NULL, pool);
    }

    // 
    for(i = 0; i < THREAD_NUM; i++) {
        apr_thread_join(&status, thread[i]);
    }

    apr_sleep(1000 * 1000 * 1000);

    return 0;
}

RES1.3M,几乎差不多;VIRT120M,比不join少了很多;但是仍有近100倍差距
在这里插入图片描述

3. 再做apr_pool_clear

如果在thread_join后面再调用apr_pool_clear(pool),主动clear一下pool呢?

在这里插入图片描述
几乎没有变化。

4. 移除apr_thread_exit

既然最后都有thread_join,把线程中的apr_thread_exit移除试试呢?

代码如下

#define THREAD_NUM 100

void* APR_THREAD_FUNC thread_worker(apr_thread_t *thread, void *data) {
//apr_thread_exit(thread, 0);
return NULL;
}

int main(int argc, const char * const argv[]) {
    apr_pool_t *pool;
    int i;
    apr_status_t status = 0;

    apr_app_initialize(&argc, &argv, NULL);
    atexit(apr_terminate);

    apr_pool_create(&pool, NULL);

    apr_thread_t *thread[THREAD_NUM];
    for(i = 0; i < THREAD_NUM; i++) {
        apr_thread_create(&thread[i], NULL, thread_worker, NULL, pool);
    }

    for(i = 0; i < THREAD_NUM; i++) {
        apr_thread_join(&status, thread[i]);
    }

    apr_sleep(1000 * 1000 * 1000);

    return 0;
}

对比发现;RES和SHR几乎没有变化;但是VIRT居然只有52M,比之前少了一半还多。

在这里插入图片描述

这么看来,如果最后有join的动作,还是不要执行apr_thread_exit的比较节省内存。

5. 将线程detach运行

#define THREAD_NUM 100

void* APR_THREAD_FUNC thread_worker(apr_thread_t *thread, void *data) {
//apr_thread_exit(thread, 0);
return NULL;
}

int main(int argc, const char * const argv[]) {
    apr_pool_t *pool;
    int i;
    apr_status_t status = 0;
    apr_threadattr_t *thread_attr;

    apr_app_initialize(&argc, &argv, NULL);
    atexit(apr_terminate);

    apr_pool_create(&pool, NULL);
    apr_threadattr_create(&thread_attr, pool);
    apr_threadattr_detach_set(thread_attr, 1);

    apr_thread_t *thread[THREAD_NUM];
    for(i = 0; i < THREAD_NUM; i++) {
        assert(apr_thread_create(&thread[i], thread_attr, thread_worker, NULL, pool)==APR_SUCCESS);
    }

    apr_sleep(1000 * 1000 * 1000);

    return 0;
}

与joinable的thread几乎一样。
在这里插入图片描述
如果还原apr_thread_exit,也跟joinable的thread几乎一样

在这里插入图片描述
所以detach对内存几乎没有影响。

6. 限制thread的栈大小

#define THREAD_NUM 100

void* APR_THREAD_FUNC thread_worker(apr_thread_t *thread, void *data) {
//apr_thread_exit(thread, 0);
return NULL;
}

int main(int argc, const char * const argv[]) {
    apr_pool_t *pool;
    int i;
    apr_status_t status = 0;
    apr_threadattr_t *thread_attr;

    apr_app_initialize(&argc, &argv, NULL);
    atexit(apr_terminate);

    apr_pool_create(&pool, NULL);
    apr_threadattr_create(&thread_attr, pool);
    //apr_threadattr_detach_set(thread_attr, 1);
    apr_threadattr_stacksize_set(thread_attr,10240); //10k

    apr_thread_t *thread[THREAD_NUM];
    for(i = 0; i < THREAD_NUM; i++) {
        assert(apr_thread_create(&thread[i], thread_attr, thread_worker, NULL, pool)==APR_SUCCESS);
    }

    for(i = 0; i < THREAD_NUM; i++) {
        apr_thread_join(&status, thread[i]);
    }

    apr_sleep(1000 * 1000 * 1000);

    return 0;
}

每一个线程的栈大小限制为10k,好像没有什么变化??
在这里插入图片描述

每个thread的栈默认是多少呢? 一般来说,默认为8M,即8,388,608bytes

可以通过ulimit -s或者ulimit -a看一下
在这里插入图片描述
可以通过**apr_threadattr_stacksize_set(thread_attr,8388608);**来还原一下默认值,跑出来结果跟不设置差不多,VIRT为52M;

在这里插入图片描述
好像真没有什么变化。

pthread测试

作为对比,我们同样的使用pthread来做一轮测试

1. 不做pthread_join

#include <pthread.h>
#include <assert.h>
#include <zconf.h>

#define THREAD_NUM 100

static void *thread_worker(void *data) {
    pthread_exit(NULL);
    return NULL;
}

int main(int argc, const char *const argv[]) {
    int i = 0;
    pthread_t thread[THREAD_NUM];
    for (i = 0; i < THREAD_NUM; i++) {
        assert(pthread_create(&thread[i], NULL, thread_worker, NULL) == 0);
    }

    sleep(1000000);
    
    return 0;
}

与apr_thread对比,VIRT893M 基本相当; RES 1.2M比之前的1.7M了一点;SHR 0.4M也小了一点;

在这里插入图片描述

注释掉 pthread_exit的结果如下:
VIRT826M,RES 1.1M,SHR 0.4M
在这里插入图片描述

做pthread_join

static void *thread_worker(void *data) {
    pthread_exit(NULL);
    return NULL;
}

int main(int argc, const char *const argv[]) {
    int i = 0;
    pthread_t thread[THREAD_NUM];
    for (i = 0; i < THREAD_NUM; i++) {
        assert(pthread_create(&thread[i], NULL, thread_worker, NULL) == 0);
    }

    for (i = 0; i < THREAD_NUM; i++) {
        pthread_join(thread[i], NULL);
    }

    sleep(1000000);

    return 0;
}

相比apr_thread,VIRT 106M/ RES 0.7M/SHR 0.5M 都小一些
在这里插入图片描述

3. 去除pthread_exit

#define THREAD_NUM 100

static void *thread_worker(void *data) {
    //pthread_exit(NULL);
    return NULL;
}

int main(int argc, const char *const argv[]) {
    int i = 0;
    pthread_t thread[THREAD_NUM];
    for (i = 0; i < THREAD_NUM; i++) {
        assert(pthread_create(&thread[i], NULL, thread_worker, NULL) == 0);
    }

    for (i = 0; i < THREAD_NUM; i++) {
        pthread_join(thread[i], NULL);
    }

    sleep(1000000);

    return 0;
}

在这里插入图片描述

5. 限制stacksize

#define THREAD_NUM 100

static void *thread_worker(void *data) {
    //pthread_exit(NULL);
    return NULL;
}

int main(int argc, const char *const argv[]) {
    int i = 0;
    int stacksize = 10240;
    pthread_t thread[THREAD_NUM];
    pthread_attr_t  attr;
    pthread_attr_init(&attr);
    pthread_attr_setstacksize(&attr, stacksize);
    for (i = 0; i < THREAD_NUM; i++) {
        assert(pthread_create(&thread[i], &attr, thread_worker, NULL) == 0);
    }

    pthread_attr_destroy(&attr);
    sleep(1000000);

    return 0;
}

相对于不设置大小,内存各项指标基本不变。(同apr_thread结果)
在这里插入图片描述

6. 结论

pthread的内存占用略比apr_thead少,但是整体都出现VIRT比RES大几百倍的情况; 尤其是pthread_exit的使用,似乎对VIRT的参数影响较大。


优化解决方案

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值