销毁 Linux 线程的正确方式

 原创    2023-06-20

在 Linux 系统中,线程是轻量级的执行单元,正确销毁线程可避免内存泄漏等问题。Linux 线程的有 joinable 和 detached 两种属性。

在之前的文章《如何正确地获取线程ID》中,我介绍了Linux线程的一些历史背景以及NPTL线程模型。在本文中,将以上述知识点为背景,讲解销毁Linux线程的正确方式。

在Linux系统中,线程是轻量级的执行单元,正确销毁线程可避免内存泄漏等问题。写这篇文章的起因是线上报告一些内存泄漏的情况,排查后发现是因为线程没有正确销毁导致的:有一处开启线程的逻辑,在正常测试时仅会出发一次,但是到线上后,用户可能很长时间都不会关闭App,这个时间足够长,以至于积少成多,最终造成了比较明显的内存泄漏。

回顾下Linux线程生命周期相关的有几个函数:

int pthread_create(pthread_t _Nullable * _Nonnull __restrict,
		const pthread_attr_t * _Nullable __restrict,
		void * _Nullable (* _Nonnull)(void * _Nullable),
		void * _Nullable __restrict);
		
void pthread_exit(void * _Nullable);
int pthread_cancel(pthread_t);

int pthread_detach(pthread_t);
int pthread_join(pthread_t , void * _Nullable * _Nullable);

退出线程的几种方式

在线程结束时,有几种方式可以使线程退出:

直接返回

当线程函数执行到末尾或者遇到 return 语句时,线程会自动退出。这是最常用的线程退出方式。

pthread_exit

调用 pthread_exit 会立即终止 当前线程 的执行,pthread_exit 函数接收一个指针参数,作为线程的返回值传递给 pthread_join 该线程的其他线程。

与 直接返回 不同的是, pthread_exit 可以在线程逻辑任意需要的地方调用,不仅仅局限于线程的入口函数。如果在主函数中调用了 pthread_exit,将导致进程退出。

pthread_cancel

调用 pthread_cancel 函数会向指定的线程发送取消请求。与 pthread_exit 只能终止本线程不同。pthread_cancel 可以由其他线程调用以显式地向目标线程发起取消请求,且 pthread_cancel 不是强制的,是一种协商机制,目标线程可以选择是否响应这次取消操作。

与 pthread_cancel 相关的几个API:

int pthread_setcancelstate(int , int * _Nullable);

设置当前线程的“可取消性”状态,可选状态值分别为:PTHREAD_CANCEL_ENABLEPTHREAD_CANCEL_DISABLE

int pthread_setcanceltype(int , int * _Nullable);

设置当前线程的接收取消时的处理。可选值为:PTHREAD_CANCEL_DEFERRED(继续运行至「可取消点」时终止线程) 和 PTHREAD_CANCEL_ASYNCHRONOUS(立即终止)。

void pthread_testcancel(void);

创建一个「可取消点」。只有可取消状态为PTHREAD_CANCEL_ENABLE,且取消处理为PTHREAD_CANCEL_DEFERRED时才有意义,将终止线程。

线程的 joinable 和 detached

Linux线程的有两种属性:joinable 和 detached。这些属性决定了线程退出后如何处理其资源。

joinable

joinable 是线程的默认属性。joinable 线程在退出后,它的资源(主要是线程相关的内存结构、线程描述符等)不会立即释放,直到另一个线程通过调用 pthread_join 函数来等待并回收该线程的资源。如果不调用 pthread_join 函数,那么 joinable 线程将在退出后成为"僵尸线程(zombie thread)",造成内存泄漏等问题。

下面是 joinable 属性线程的使用示例:

pthread_t pth;
int ret = pthread_create(&pth, NULL, chuanFunc, NULL);
if (ret == 0) {
    void* thread_result;
    //阻塞当前线程,等待子线程退出并获取退出状态。
    pthread_join(pth, &thread_result);
}

detached

当线程被设置为 detached 属性时,它的退出状态和资源会被系统自动回收,无需其他线程调用 pthread_join 函数等待线程退出。detached 线程适用于不需要关心线程退出状态或进行清理工作的情况,可以提高程序的效率和简化代码逻辑。

下面是 detached 属性线程的使用示例:

pthread_t pth;
int ret = pthread_create(&pth, NULL, chuanFunc, NULL);
if (ret == 0) {
    pthread_detach(pth);//调用会立即返回,不会等待子线程执行结束
}

也可以在线程创建时,配置 detached 属性,如下:

#include <pthread.h>

pthread_t thread;
pthread_attr_t attr;
pthread_attr_init(&attr);
//pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);//设置detached属性
pthread_create(&thread, &attr, chuanFunc, args);

只有在线程创建之前设置属性才有效,一旦线程创建,无法更改其属性。

最佳实践

  • 无论是 joinable 还是 detached 线程,在线程函数中确保分配的内存、文件描述符在函数结束时正确释放、关闭,是良好的编程习惯;
  • pthread_create 后,根据需要选择使用 pthread_join 或 pthread_detach,确保线程结构等的正确释放。
文章最后修改于 2023-06-27

相关文章:

Linux线程局部存储 Thread Local Storage
如何正确地获取线程ID?
perl: warning: Setting locale failed.
阿里云云盘扩容笔记

发表留言

您的电子邮箱地址不会被公开,必填项已用*标注。发布的留言可能不会立即公开展示,请耐心等待审核通过。