C语言陷阱与技巧第48节,创建的线程函数占用的资源就是不释放?可以自己创建线程解决

在之前的章节里,我们讨论了C语言程序开发中的多线程开发的应用场景,以及相关注意事项。其实在C语言程序开发中,何时该使用,以及如何使用多线程编程是不值一谈的,大多数C语言初学者甚至看一眼相关 demo 就能了解,真正需要小心的,是开发中的一些细节。

“资源残留”

最容易遇到,也是最容易被忽略的细节就是多线程编程后的“资源残留”问题,这个问题本专栏前面的文章已经较为详细的讨论,粗略概括来说,就是C语言程序中的线程函数执行完毕并返回后,它向系统申请的资源并没有被系统回收。

如果程序员不够敏锐,忽略了该问题,那么C语言程序每创建一个线程,系统就会有一部分资源被占用,且在C语言程序结束之前,这部分被占用的资源不会再被系统回收再利用(有些程序员称这种现象为“内存泄漏”)。而系统的资源总是有限的,迟早会被消耗完,一旦如此,程序就会崩溃,甚至还会损毁数据。

如果C语言程序内存泄漏的比较少,在桌面系统中使用也许不会表现出明显的问题,因为程序占用的资源随着系统重启就被强制回收了。但是,对于嵌入式系统而言,程序的生命周期常常是没有上限的,有些设备从第一次开启后,就永远不会关闭了,除非有断电等意外发生,否则它应该一直工作到超过设计年限(直到报废)。

另外,嵌入式设备的资源本身就比较匮乏,这种情况下,C语言程序哪怕只有一点内存泄漏,在超长工作时间的积累下,也是不可接受的。所以合格的嵌入式C语言程序员,应该避免内存泄漏的发生。

看过本专栏之前文章的读者应该知道,线程函数退出后,资源不被系统回收的原因是:系统不知道是否仍然有子程序关心该线程函数的运行结果,如果有子程序需要,系统却把运行结果释放回收了,整个程序就不安全了。

所以为避免C语言程序中的线程函数退出时,占用资源不被回收,一般有两种处理方法:告诉系统没有人关心线程函数处理结果,或者编写接收线程函数处理结果的C语言代码。

操作系统:要么告诉我没人关心它,要么来人来处理它,反正我不敢随便释放回收。

具体来说,在 Linux 中开发C语言程序,可以调用 pthread_detach() 函数告诉系统没人关心线程函数的处理结果,或者调用 pthread_join() 函数等待线程函数完成,并接收其处理结果。这样一来,线程函数就不会再有资源残留了。

顽固的“资源残留”

可能有读者已经注意到了,上述两种方法只能避免C语言程序多次创建线程函数造成的资源占用累加。只要创建线程函数,程序就会占用至少一次创建线程函数所消耗的资源。请看例子,相关C语言代码如下:

#include <stdio.h>
#include <pthread.h>

void *thread(void *p)
{
    pthread_detach(pthread_self());

    printf("thread running...\n");
    printf("thread exit\n");

    return NULL;
}

int main()
{
    pthread_t pid;

    pthread_create(&pid, NULL, thread, NULL);
    while(1);

    return 0;
}   

为了让C语言代码尽量简洁,便于讨论主题,这里没有做错误处理。

这段C语言代码很简单,线程函数打印两句话后退出,main() 函数创建线程函数后,进入 while(1) 死循环。编译这段C语言代码并使用 GDB 工具单步运行:

# gcc t.c -lpthread -g
# gdb ./a.out 
(gdb) tb main
Temporary breakpoint 1 at 0x400729: file t.c, line 18.
(gdb) r
Temporary breakpoint 1, main () at t.c:18
18      pthread_create(&pid, NULL, thread, NULL);
(gdb)

在 main() 函数下断点后,输入 run 命令让C语言程序运行起来,此时程序会停在创建线程函数之前,我们观察此时程序占用的系统资源:

可见,创建线程函数之前,C语言程序占用的资源很少。现在再在 thread() 线程函数中下断点,并输出 continue 命令继续执行程序:

(gdb) tb thread
Temporary breakpoint 3 at 0x4006f9: file t.c, line 6.
(gdb) c
Continuing.
[New Thread 0x7ffff77f2700 (LWP 18103)]
[Switching to Thread 0x7ffff77f2700 (LWP 18103)]

Temporary breakpoint 3, thread (p=0x0) at t.c:6
6       pthread_detach(pthread_self());

现在C语言程序创建了线程,并且线程函数还没有退出,再观察程序占用的资源:

发现C语言程序占用的资源显著提升了。我们再次输出 continue 命令,让程序将线程函数执行完成:

(gdb) c
Continuing.
thread running...
thread exit
[Thread 0x7ffff77f2700 (LWP 18103) exited]

线程函数的逻辑很简单,被创建后,向终端输出两句话后就直接退出了,程序会停在 main() 函数中的 while(1) 死循环处。我们查看此时C语言程序占用的资源:

发现虽然此时线程函数退出了,但是它占用的资源却并没有被系统回收。乐于动手的读者会发现,调用 pthread_join() 接收线程函数返回值,也是一样的,线程函数消耗的资源并不会被系统回收,这是怎么回事呢?

解析顽固的“资源占用”

示例C语言程序在创建线程之前消耗的内存数大约是 6372,创建线程后消耗的内存数大约是 14700,也就是说C语言程序创建线程消耗内存数大约为 8300,我们查看系统设置的堆栈大小:

# ulimit -s
8192

因此猜测:C语言程序创建线程时使用的堆栈没有释放,仍然残留在程序里。查阅 pthread_create() 原理,发现它是基于 clone() 函数实现的。下面的 demo 模拟了 pthread_create() 函数,请看相关C语言代码:

#define _GNU_SOURCE
#include "stdio.h"
#include "pthread.h"
#include "sched.h"
#include "stdlib.h"
#include "signal.h"

void* thread(void* p)
{
    printf("thread exit\n");
    return NULL;
}
#define STACK_SIZE 8192*1024
int main()
{
    void* pstk = malloc(STACK_SIZE);
    int clonePid = clone((int(*)(void*))thread,  (char *)pstk + STACK_SIZE,
                     CLONE_VM | CLONE_FS  | CLONE_FILES | SIGCHLD, pstk);

    printf("\n------------- getchar --------------\n");
    getchar();

    free(pstk);         // pthread_create 创建线程时,堆栈没有释放

    return 0;
}


请注意上述C语言代码中的 pstk,它模拟了 pthread_create() 函数创建线程时向系统申请的资源。如果C语言程序中的线程函数没有被设置为 detached 或者没有调用 pthread_join() 等待接收其处理结果,则程序每创建一个线程,就会向系统申请一块新的内存资源,造成内存泄漏。

那为什么调用了 pthread_detach() 后,线程函数执行完毕仍然有资源残留呢?其实这块残留的内存资源并没有泄漏,而是被系统缓存起来了,便于下一次快速使用,读者其实不必在意这块内存消耗,事实上,它的存在是为了提升程序效率的。当然了,如果读者真的很在意这块没有被回收的资源,可以参考本节后面的例子,自己实现一个线程创建函数。

小结

在C语言程序开发中,稍微复杂些的项目都离不开多线程编程,本节主要讨论了线程函数的资源占用问题,其实应该明白,以 Linux 为代表的操作系统一般都是具有缓存机制的,只要内存足够使用,有些资源系统并不会立刻回收,这样一来,下一次就可以直接使用,实际上可以提升效率。

反正内存空闲着也没有什么意义,倒不如让操作系统用于缓存数据,提升效率。

读者不必担心操作系统占用内存缓存数据会导致其他程序内存不足,如今的操作系统已经比较成熟,它们会在内存紧张时,将无人使用的缓存占用的内存释放给需要的线程使用。不过,如果读者真的很在意创建的线程函数占用的内存,可以参考本文介绍的基于 clone() 函数的 demo。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif