13

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

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80%E9%99%B7%E9%98%B1%E4%B8%8E%E6%8A%80%E5%B7%A7%E7%AC%AC48%E8%8A%82-%E5%88%9B%E5%BB%BA%E7%9A%84%E7%BA%BF%E7%A8%8B%E5%87%BD%E6%95%B0%E5%8D%A0%E7%94%A8%E7%9A%84%E8%B5%84/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在之前的章节里,我们讨论了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;
}   
d6552aa3d002100a29cf37f9d825c852.png

为了让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;
}

760bab00c4b9ecc62f2e3fa793984b93.png

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

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

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK