Category 活动日历

前言 在 Linux 多线程开发中,“线程控制” 是贯穿始终的核心技能 —— 从线程的创建、终止,到等待、分离,每一步操作都直接影响程序的性能、稳定性和资源利用率。而要熟练掌握线程控制,首先必须理清一个关键问题:进程和线程究竟哪些资源共享、哪些资源独占?这是理解线程控制逻辑的底层基石。

很多开发者在编写多线程程序时,常会陷入这样的困境:明明调用了pthread_create却创建失败,线程退出后出现资源泄漏,用pthread_join等待线程却始终阻塞,甚至因误操作导致整个进程崩溃。这些问题的根源,往往是对线程与进程的资源关系理解不深,或是对 POSIX 线程库的控制接口使用不当。

本文将从 “进程与线程的资源划分” 入手,层层递进讲解 Linux 线程的完整控制流程 —— 包括 POSIX 线程库的使用、线程创建、终止、等待、分离等核心操作,全程结合实战代码和底层原理,用通俗的语言拆解复杂概念,让你不仅 “会用” 线程控制接口,更能 “懂原理”,轻松避开多线程开发的各种坑。下面就让我们正式开始吧!

一、先搞懂核心:Linux 进程与线程的资源共享与独占 要理解线程控制的本质,首先要明确:线程是进程内部的执行流,进程是资源分配的基本单位,线程是调度的基本单位。这一定位决定了线程与进程的资源关系 —— 线程共享进程的大部分资源,同时拥有少量私有资源。

1.1 进程与线程的核心定位 在 Linux 系统中,进程和线程的核心区别可以用一句话概括:

进程:负责 “占有资源”,是操作系统分配资源(虚拟地址空间、文件描述符、用户 ID 等)的最小单位,进程之间相互独立,资源不共享。线程:负责 “执行任务”,是 CPU 调度和执行的最小单位,线程不能独立占有资源,而是共享所属进程的全部资源。 举个通俗的例子:如果把计算机系统比作一个大型工厂,进程就是工厂里的一个个车间 —— 每个车间都有自己的场地、设备、工具(对应进程的资源);而线程就是车间里的工人 —— 工人共享车间的所有资源,同时各自执行不同的任务,协同完成车间的整体目标。一个车间至少有一个工人(单线程进程),也可以有多个工人(多线程进程)。

1.2 线程的私有资源:独属于 “工人” 的工具 虽然线程共享进程的大部分资源,但为了保证线程的独立执行,每个线程都拥有自己的 “私有财产”,这些资源不会被其他线程共享:

线程 ID(TID):线程的唯一标识,分为两种 —— 内核级线程 ID(LWP,Light Weight Process ID)和用户级线程 ID(pthread_t)。内核级 TID 是系统全局唯一的,用户级 TID 是进程内唯一的,由 POSIX 线程库维护。线程上下文数据:包括一组寄存器的值、程序计数器(PC)、栈指针(SP)等,用于保存线程的执行状态。当线程切换时,内核会保存这些上下文,切换回来时恢复,保证线程能继续执行。线程私有栈:每个线程都有自己的栈空间,用于存储局部变量、函数调用参数和返回值。主线程的栈在虚拟地址空间的栈区,而子线程的栈通常在共享区(mmap 区域),通过mmap系统调用分配,默认大小一般为 8MB,且不能动态增长。errno 变量:用于存储线程执行系统调用时的错误码。每个线程都有自己的errno副本,避免多个线程同时修改导致错误码混乱。信号屏蔽字:线程可以设置自己的信号屏蔽字,决定哪些信号会被阻塞,不影响其他线程的信号处理。调度优先级:线程可以拥有自己的调度优先级,内核会根据优先级调度线程执行,不同线程的优先级可以不同。1.3 线程共享的进程资源:车间里的 “公共设施” 同一个进程的所有线程,共享进程的全部核心资源,这是线程高效协作的基础:

虚拟地址空间:包括代码段(Text Segment)、数据段(Data Segment)、堆区(Heap)、共享区(mmap 区域)等。线程可以直接访问进程的全局变量、静态变量、堆内存,无需额外的通信机制。文件描述符表:进程打开的所有文件、socket、管道等资源,对应的文件描述符会被所有线程共享。一个线程关闭文件描述符,会影响其他线程对该文件的访问。信号处理方式:进程中设置的信号处理函数(如SIG_IGN忽略、SIG_DFL默认处理、自定义处理函数)对所有线程有效。线程可以修改信号处理方式,但修改后会作用于整个进程。当前工作目录:进程的当前工作目录由所有线程共享,一个线程调用chdir()修改工作目录,其他线程的工作目录也会随之改变。用户 ID 和组 ID:进程的所有者用户 ID(UID)和组 ID(GID)对所有线程共享,线程无法单独修改自己的 UID/GID。其他进程资源:包括进程的 PID、进程组 ID(PGID)、会话 ID(SID)、打开的共享内存、消息队列、信号量等,均为线程共享。1.4 关键问题:单进程本质是 “单线程进程” 很多开发者会疑惑:之前学习的单进程程序,和线程有什么关系?

答案很简单:单进程本质上是 “只有一个线程执行流的进程”。这个线程就是主线程(main 线程),它拥有进程的全部资源,独自执行main函数中的代码。当我们创建新的线程后,进程就变成了多线程进程,多个线程共享进程资源,协同执行不同的任务。

理解这一点很重要:线程控制的所有操作,本质上都是在 “进程的资源框架内” 对执行流进行管理,不会改变进程的资源分配状态。

二、POSIX 线程库:Linux 线程控制的 “工具箱” Linux 系统中,线程控制的接口主要由POSIX 线程库(pthread 库) 提供,这是一套标准的线程操作 API,绝大多数函数都以pthread_为前缀。要使用这些函数,必须掌握其编译链接方式、错误处理规则,这是线程控制的基础。

2.1 编译与链接:必须链接 pthread 库 POSIX 线程库不是 Linux 系统的默认库,因此在编译多线程程序时,必须通过-lpthread选项明确链接该库,否则会出现 “未定义引用” 错误。

编译命令示例:

代码语言:javascript复制# 编译单个文件

gcc thread_demo.c -o thread_demo -lpthread

# 编译多个文件

gcc thread1.c thread2.c -o thread_app -lpthread2.2 头文件:包含 所有 pthread 库的函数声明、数据类型(如pthread_t、pthread_attr_t)都定义在头文件中,使用时必须包含:

代码语言:javascript复制#include 2.3 错误处理:与传统系统调用不同 传统的 Linux 系统调用(如open、read、fork)通常返回 - 1 表示失败,并设置全局变量errno来指示错误原因。但 pthread 库的函数错误处理方式不同:

函数执行成功时返回 0。执行失败时,不设置全局errno,而是直接返回错误码(非 0 值)。可以使用strerror()函数将错误码转换为人类可读的错误信息。错误处理示例:

代码语言:javascript复制#include

#include

#include // 包含strerror函数

int main() {

pthread_t tid;

// 故意传递错误的参数(如NULL线程函数),触发创建失败

int ret = pthread_create(&tid, NULL, NULL, NULL);

if (ret != 0) {

// 用strerror将错误码转换为错误信息

fprintf(stderr, "创建线程失败:%s\n", strerror(ret));

return 1;

}

return 0;

}运行结果:

代码语言:javascript复制创建线程失败:Invalid argument 注意:pthread 库也提供了线程内的errno副本,以兼容依赖errno的代码,但对于 pthread 函数的错误,建议直接通过返回值判断,效率更高。

三、线程创建:启动一个新的执行流 线程创建是线程控制的第一步,通过pthread_create函数创建新线程,新线程会执行指定的函数,与主线程并行运行。

3.1 函数原型与参数解析代码语言:javascript复制int pthread_create(pthread_t *thread, const pthread_attr_t *attr,

void *(*start_routine)(void*), void *arg);参数详解:

thread:输出参数,指向pthread_t类型的变量,用于存储新创建线程的用户级 ID(进程内唯一)。后续对该线程的操作(如终止、等待)都需要通过这个 ID。attr:线程属性设置,指定线程的栈大小、调度优先级、分离状态等。若为NULL,表示使用默认属性。start_routine:函数指针,指向线程启动后要执行的函数。该函数的返回值类型为void*,参数类型也为void*,这是 POSIX 标准规定的线程函数签名,便于传递任意类型的数据。arg:传递给start_routine函数的参数,可以是任意类型的指针。若需要传递多个参数,可封装为结构体,将结构体指针作为arg传入。返回值:

成功:返回 0。失败:返回非 0 错误码,常见错误码包括EINVAL(参数无效,如start_routine为 NULL)、EAGAIN(系统资源不足,无法创建新线程)等。3.2 线程创建实战示例示例 1:创建简单线程,执行无参数函数代码语言:javascript复制#include

#include

#include // 包含sleep函数

// 线程执行函数

void *thread_func(void *arg) {

// 循环打印线程信息

for (int i = 0; i < 5; i++) {

printf("子线程:我正在运行,线程ID = %lu\n", (unsigned long)pthread_self());

sleep(1); // 休眠1秒

}

return NULL; // 线程退出,返回NULL

}

int main() {

pthread_t tid;

// 创建线程

int ret = pthread_create(&tid, NULL, thread_func, NULL);

if (ret != 0) {

fprintf(stderr, "创建线程失败:%s\n", strerror(ret));

return 1;

}

printf("主线程:成功创建子线程,子线程ID = %lu\n", (unsigned long)tid);

// 主线程循环打印信息

for (int i = 0; i < 5; i++) {

printf("主线程:我正在运行,线程ID = %lu\n", (unsigned long)pthread_self());

sleep(1);

}

// 等待子线程结束(后续详细讲解)

pthread_join(tid, NULL);

printf("主线程:子线程已退出,程序结束\n");

return 0;

}编译运行:

代码语言:javascript复制gcc thread_simple.c -o thread_simple -lpthread

./thread_simple运行结果(主线程和子线程交替执行):

代码语言:javascript复制主线程:成功创建子线程,子线程ID = 140703347508992

主线程:我正在运行,线程ID = 140703355896640

子线程:我正在运行,线程ID = 140703347508992

主线程:我正在运行,线程ID = 140703355896640

子线程:我正在运行,线程ID = 140703347508992

主线程:我正在运行,线程ID = 140703355896640

子线程:我正在运行,线程ID = 140703347508992

主线程:我正在运行,线程ID = 140703355896640

子线程:我正在运行,线程ID = 140703347508992

主线程:我正在运行,线程ID = 140703355896640

子线程:我正在运行,线程ID = 140703347508992

主线程:子线程已退出,程序结束示例 2:传递参数给线程函数(单个参数)代码语言:javascript复制#include

#include

#include

// 线程执行函数,接收一个整数参数

void *thread_with_arg(void *arg) {

// 将void*类型转换为int类型

int num = *(int*)arg;

for (int i = 0; i < num; i++) {

printf("子线程:第%d次运行,线程ID = %lu\n", i+1, (unsigned long)pthread_self());

sleep(1);

}

return NULL;

}

int main() {

pthread_t tid;

int run_count = 3; // 要传递给线程的参数

// 创建线程,传递run_count的地址

int ret = pthread_create(&tid, NULL, thread_with_arg, &run_count);

if (ret != 0) {

fprintf(stderr, "创建线程失败:%s\n", strerror(ret));

return 1;

}

// 等待子线程结束

pthread_join(tid, NULL);

printf("主线程:子线程执行完毕,程序结束\n");

return 0;

}示例 3:传递多个参数给线程函数(结构体封装)代码语言:javascript复制#include

#include

#include

#include

// 定义要传递给线程的参数结构体

typedef struct {

char name[20]; // 线程名称

int age; // 自定义整数参数

float score; // 自定义浮点数参数

} ThreadArgs;

// 线程执行函数,接收结构体参数

void *thread_with_struct_arg(void *arg) {

ThreadArgs *args = (ThreadArgs*)arg;

printf("子线程:线程名称 = %s,年龄 = %d,分数 = %.2f\n",

args->name, args->age, args->score);

// 模拟业务逻辑

sleep(2);

printf("子线程:执行完毕\n");

return NULL;

}

int main() {

pthread_t tid;

// 初始化参数结构体

ThreadArgs args = {"Worker-1", 25, 98.5};

// 创建线程,传递结构体指针

int ret = pthread_create(&tid, NULL, thread_with_struct_arg, &args);

if (ret != 0) {

fprintf(stderr, "创建线程失败:%s\n", strerror(ret));

return 1;

}

// 等待子线程结束

pthread_join(tid, NULL);

printf("主线程:程序结束\n");

return 0;

}3.3 线程 ID 的两种类型与获取方式 在 Linux 中,线程有两种 ID,用途不同,必须区分清楚:

用户级线程 ID(pthread_t):

由 POSIX 线程库维护,是进程内唯一的标识,仅在当前进程中有效。通过pthread_create的thread参数获取,或通过pthread_self()函数获取当前线程的 ID。类型pthread_t的具体实现可能是整数、指针等,不建议直接用%d打印,推荐用%lu(转换为unsigned long)。 内核级线程 ID(LWP,Light Weight Process ID):

由 Linux 内核维护,是系统全局唯一的标识,内核调度线程时使用。可以通过ps -aL命令查看,LWP 列显示的就是内核级线程 ID。主线程的 LWP 与进程 ID(PID)相同。查看线程信息的 bash 命令:

代码语言:javascript复制# 编译运行线程程序后,查看线程信息

ps -aL | grep 程序名示例:运行示例 1 的thread_simple程序后,执行命令:

代码语言:javascript复制ps -aL | grep thread_simple输出结果:

代码语言:javascript复制12345 12345 pts/0 00:00:00 thread_simple # 主线程:PID=12345,LWP=12345

12345 12346 pts/0 00:00:00 thread_simple # 子线程:PID=12345,LWP=123463.4 线程创建的注意事项

线程函数的返回值:线程函数的返回值类型为void*,可以返回任意类型的数据,但返回的内存必须是全局变量或堆内存(malloc分配),不能是局部变量(线程退出后局部变量会被释放,导致野指针)。参数传递的安全性:传递给线程的参数如果是局部变量,要确保线程执行期间该变量不被销毁。如果主线程在子线程启动前就退出,局部变量会被释放,子线程访问时会出现未定义行为。线程创建后的执行顺序:线程创建后,主线程和子线程的执行顺序由 CPU 调度器决定,无法预测。不要假设子线程会在主线程之前执行,或反之。系统线程数量限制:Linux 系统对单个进程的线程数量有默认限制(通常为 1024),超出限制会导致pthread_create返回EAGAIN错误。可以通过修改/etc/security/limits.conf文件调整限制。四、线程终止:优雅结束线程执行 线程终止是指线程停止执行,释放自己的私有资源(如栈、寄存器上下文等),但进程的共享资源不会被释放。Linux 提供了三种合法的线程终止方式,以及一种不推荐的强制终止方式。

4.1 线程终止的三种合法方式方式 1:从线程函数 return 返回(推荐) 线程函数执行完毕后,通过return语句返回,线程正常终止。这种方式最优雅,可以自然地清理线程内的局部变量,并且可以返回线程的执行结果。

示例:

代码语言:javascript复制#include

#include

#include

// 线程函数返回一个整数结果

void *thread_return(void *arg) {

int *result = (int*)malloc(sizeof(int));

*result = 100; // 线程执行结果

printf("子线程:执行完毕,返回结果 = %d\n", *result);

return (void*)result; // 返回堆内存的指针

}

int main() {

pthread_t tid;

pthread_create(&tid, NULL, thread_return, NULL);

// 等待线程结束,获取返回值

void *ret;

pthread_join(tid, &ret);

printf("主线程:获取子线程返回值 = %d\n", *(int*)ret);

free(ret); // 释放子线程分配的堆内存

return 0;

}方式 2:调用 pthread_exit 函数终止自己 线程可以通过调用pthread_exit函数主动终止自己,该函数不会返回,相当于线程的 “自杀” 函数。pthread_exit的参数可以传递线程的退出状态,供其他线程通过pthread_join获取。

函数原型:

代码语言:javascript复制void pthread_exit(void *value_ptr);

参数value_ptr:指向线程退出状态的指针,与线程函数return的返回值作用相同。该指针指向的内存必须是全局变量或堆内存,不能是局部变量。返回值:无返回值,线程调用后直接终止。示例:

代码语言:javascript复制#include

#include

#include

void *thread_exit_demo(void *arg) {

int *result = (int*)malloc(sizeof(int));

*result = 200;

printf("子线程:调用pthread_exit终止\n");

pthread_exit((void*)result); // 终止线程,传递结果

// 以下代码不会执行

printf("子线程:这行代码永远不会被执行\n");

return NULL;

}

int main() {

pthread_t tid;

pthread_create(&tid, NULL, thread_exit_demo, NULL);

void *ret;

pthread_join(tid, &ret);

printf("主线程:获取子线程退出状态 = %d\n", *(int*)ret);

free(ret);

return 0;

}方式 3:主线程正常退出(main函数 return) 主线程执行完main函数的代码后return,相当于调用exit函数,会终止整个进程,进程内的所有线程都会随之退出。

示例:

代码语言:javascript复制#include

#include

#include

void *thread_func(void *arg) {

while (1) {

printf("子线程:正在运行...\n");

sleep(1);

}

return NULL;

}

int main() {

pthread_t tid;

pthread_create(&tid, NULL, thread_func, NULL);

// 主线程休眠3秒后退出

sleep(3);

printf("主线程:退出,所有子线程将被终止\n");

return 0; // 主线程退出,子线程也会随之终止

}4.2 线程的强制终止:调用 pthread_cancel 函数 一个线程可以调用pthread_cancel函数,强制终止同一进程中的另一个线程。这种方式相当于 “杀死” 其他线程,使用时需要谨慎,因为可能导致资源泄漏(如线程持有互斥锁未释放)。

函数原型:

代码语言:javascript复制int pthread_cancel(pthread_t thread);

参数thread:要终止的线程的用户级 ID(pthread_t类型)。返回值:成功返回 0,失败返回非 0 错误码。注意事项:

pthread_cancel只是向目标线程发送一个 “取消请求”,目标线程不会立即终止,而是在到达 “取消点” 时才会响应请求。取消点是线程执行过程中检查是否有取消请求的位置,常见的取消点包括:sleep、read、write、pthread_join等系统调用和库函数。线程可以通过pthread_setcancelstate函数设置自己的取消状态(启用或禁用),通过pthread_setcanceltype函数设置取消类型(立即取消或延迟取消)。示例:

代码语言:javascript复制#include

#include

#include

void *thread_to_cancel(void *arg) {

int i = 0;

while (1) {

printf("子线程:第%d次运行,等待取消...\n", ++i);

sleep(1); // sleep是取消点

}

return NULL;

}

int main() {

pthread_t tid;

pthread_create(&tid, NULL, thread_to_cancel, NULL);

// 主线程休眠3秒后,强制终止子线程

sleep(3);

printf("主线程:发送取消请求\n");

int ret = pthread_cancel(tid);

if (ret != 0) {

fprintf(stderr, "取消线程失败:%s\n", strerror(ret));

return 1;

}

// 等待子线程终止,获取取消状态

void *cancel_ret;

pthread_join(tid, &cancel_ret);

if (cancel_ret == PTHREAD_CANCELED) {

printf("主线程:子线程已被成功取消\n");

}

return 0;

}运行结果:

代码语言:javascript复制子线程:第1次运行,等待取消...

子线程:第2次运行,等待取消...

子线程:第3次运行,等待取消...

主线程:发送取消请求

主线程:子线程已被成功取消4.3 线程终止的禁忌:避免使用 exit 函数 线程中绝对不要调用exit函数!exit函数的作用是终止整个进程,而不是当前线程。一个线程调用exit,会导致进程内的所有线程(包括主线程)都被终止,这通常不是我们想要的结果。

错误示例:

代码语言:javascript复制#include

#include

#include

#include // 包含exit函数

void *bad_thread(void *arg) {

printf("子线程:调用exit终止\n");

exit(EXIT_FAILURE); // 错误:终止整个进程,主线程也会被终止

return NULL;

}

int main() {

pthread_t tid;

pthread_create(&tid, NULL, bad_thread, NULL);

sleep(1);

printf("主线程:这行代码永远不会被执行\n"); // 不会输出

return 0;

}五、线程等待:等待线程终止,回收资源 线程终止后,其私有资源(如栈、线程控制块 TCB)不会自动释放,仍然占用进程的地址空间。如果主线程不处理这些资源,会导致系统资源泄漏。线程等待就是主线程(或其他线程)等待目标线程终止,回收其资源,并获取其退出状态的过程。

5.1 为什么需要线程等待?

回收资源:线程终止后,其 TCB、栈等资源需要被回收,否则会一直占用内存,长期运行可能导致内存耗尽。获取退出状态:主线程可以通过等待获取子线程的退出状态(如返回值、是否被取消),判断子线程是否正常执行完毕。同步线程执行:主线程可以通过等待,确保子线程执行完毕后再继续执行后续代码,避免出现 “子线程未完成任务,主线程已退出” 的情况。5.2 线程等待函数:pthread_join POSIX 线程库提供pthread_join函数实现线程等待,该函数会阻塞调用线程(通常是主线程),直到目标线程终止。

函数原型:

代码语言:javascript复制int pthread_join(pthread_t thread, void **value_ptr);参数详解:

thread:要等待的目标线程的用户级 ID(pthread_t类型)。value_ptr:二级指针,用于存储目标线程的退出状态。如果不关心退出状态,可以传递NULL。返回值:

成功返回 0,失败返回非 0 错误码(如ESRCH:目标线程不存在,EDEADLK:死锁,如线程等待自己)。不同终止方式对应的退出状态:线程终止方式

value_ptr存储的内容

从线程函数return返回

线程函数的返回值(void*类型)

调用pthread_exit终止

pthread_exit的参数(void*类型)

被pthread_cancel取消

特殊常量PTHREAD_CANCELED

线程异常终止(如段错误)

无定义(进程会随之崩溃,无法通过pthread_join获取)

5.3 线程等待实战示例示例 1:等待 return 返回的线程,获取返回值代码语言:javascript复制#include

#include

#include

void *thread_return_val(void *arg) {

int *ret = (int*)malloc(sizeof(int));

*ret = 300;

printf("子线程:return返回,结果 = %d\n", *ret);

return (void*)ret;

}

int main() {

pthread_t tid;

pthread_create(&tid, NULL, thread_return_val, NULL);

// 等待线程,获取返回值

void *value_ptr;

int ret = pthread_join(tid, &value_ptr);

if (ret != 0) {

fprintf(stderr, "等待线程失败:%s\n", strerror(ret));

return 1;

}

printf("主线程:子线程返回值 = %d\n", *(int*)value_ptr);

free(value_ptr); // 释放子线程分配的内存

return 0;

}示例 2:等待 pthread_exit 终止的线程,获取退出状态代码语言:javascript复制#include

#include

#include

void *thread_exit_val(void *arg) {

char *msg = (char*)malloc(20);

strcpy(msg, "线程执行成功");

printf("子线程:pthread_exit退出\n");

pthread_exit((void*)msg);

}

int main() {

pthread_t tid;

pthread_create(&tid, NULL, thread_exit_val, NULL);

void *value_ptr;

pthread_join(tid, &value_ptr);

printf("主线程:子线程退出状态 = %s\n", (char*)value_ptr);

free(value_ptr);

return 0;

}示例 3:等待被取消的线程,判断取消状态代码语言:javascript复制#include

#include

#include

void *thread_canceled(void *arg) {

while (1) {

printf("子线程:运行中...\n");

sleep(1); // 取消点

}

return NULL;

}

int main() {

pthread_t tid;

pthread_create(&tid, NULL, thread_canceled, NULL);

sleep(2);

pthread_cancel(tid); // 发送取消请求

void *value_ptr;

pthread_join(tid, &value_ptr);

if (value_ptr == PTHREAD_CANCELED) {

printf("主线程:子线程已被取消\n");

} else {

printf("主线程:子线程正常退出\n");

}

return 0;

}示例 4:不关心退出状态,仅回收资源代码语言:javascript复制#include

#include

#include

void *thread_no_val(void *arg) {

printf("子线程:执行任务...\n");

sleep(2);

return NULL;

}

int main() {

pthread_t tid;

pthread_create(&tid, NULL, thread_no_val, NULL);

// 传递NULL给value_ptr,不关心退出状态

pthread_join(tid, NULL);

printf("主线程:子线程已终止,资源已回收\n");

return 0;

}5.4 线程等待的注意事项

只能等待 joinable 状态的线程:默认情况下,线程是joinable状态,必须通过pthread_join等待,否则会资源泄漏。如果线程设置为分离状态(detached),pthread_join会返回EINVAL错误。避免死锁:线程不能等待自己(会导致死锁),也不能重复等待同一个线程(第二次等待会返回ESRCH错误)。等待的阻塞特性:pthread_join是阻塞函数,调用后会暂停当前线程的执行,直到目标线程终止。如果需要非阻塞等待,可以结合pthread_tryjoin_np函数(非标准函数,_np表示 non-portable)。六、线程分离:自动回收资源,无需等待 默认情况下,线程是joinable状态,需要通过pthread_join等待回收资源。但在某些场景下(如不关心线程的退出状态),pthread_join会成为一种负担。这时可以将线程设置为分离状态(detached),线程终止后会自动释放资源,无需其他线程等待。

6.1 线程分离的作用与原理

作用:线程设置为分离状态后,终止时会自动回收其 TCB、栈等资源,不需要其他线程调用pthread_join,避免资源泄漏。原理:分离状态的线程,其joinid字段会指向自身(pd->joinid = pd),内核会在线程终止时自动清理其资源,无需等待其他线程的pthread_join调用。6.2 线程分离函数:pthread_detach 通过pthread_detach函数可以将线程设置为分离状态,该函数可以由其他线程调用,也可以由线程自身调用。

函数原型:

代码语言:javascript复制int pthread_detach(pthread_t thread);

参数thread:要设置为分离状态的线程的用户级 ID。返回值:成功返回 0,失败返回非 0 错误码。6.3 线程分离的两种方式方式 1:线程自身调用 pthread_detach(推荐) 线程在启动后,主动调用pthread_detach(pthread_self()),将自己设置为分离状态。这种方式最灵活,线程可以自主决定是否分离。

示例:

代码语言:javascript复制#include

#include

#include

void *detached_thread_self(void *arg) {

// 将自己设置为分离状态

int ret = pthread_detach(pthread_self());

if (ret != 0) {

fprintf(stderr, "设置线程分离失败:%s\n", strerror(ret));

return NULL;

}

printf("分离线程:我是分离状态,终止后会自动回收资源\n");

sleep(2);

printf("分离线程:执行完毕,即将终止\n");

return NULL;

}

int main() {

pthread_t tid;

pthread_create(&tid, NULL, detached_thread_self, NULL);

// 主线程休眠3秒,确保子线程执行完毕

sleep(3);

printf("主线程:程序结束,无需调用pthread_join\n");

return 0;

}方式 2:其他线程调用 pthread_detach 主线程(或其他线程)在创建目标线程后,调用pthread_detach将其设置为分离状态。这种方式需要确保在目标线程终止前完成分离设置,否则可能导致资源泄漏。

示例:

代码语言:javascript复制#include

#include

#include

void *detached_thread_other(void *arg) {

printf("分离线程:由主线程设置为分离状态\n");

sleep(2);

printf("分离线程:执行完毕\n");

return NULL;

}

int main() {

pthread_t tid;

pthread_create(&tid, NULL, detached_thread_other, NULL);

// 主线程将子线程设置为分离状态

int ret = pthread_detach(tid);

if (ret != 0) {

fprintf(stderr, "设置线程分离失败:%s\n", strerror(ret));

return 1;

}

sleep(3);

printf("主线程:程序结束\n");

return 0;

}6.4 线程分离的注意事项

joinable 与 detached 状态互斥:一个线程不能同时是joinable和detached状态。一旦设置为detached状态,就不能再通过pthread_join等待,否则会返回EINVAL错误。设置分离状态的时机:必须在线程终止前设置分离状态,否则线程终止后资源无法回收,导致泄漏。无法获取退出状态:分离状态的线程终止后,其退出状态会被自动销毁,无法通过任何方式获取。如果需要获取线程的退出状态,不能使用分离状态。主线程退出的影响:即使线程是分离状态,若主线程提前退出(调用exit或main函数return),所有子线程都会被终止,分离状态仅影响线程终止后的资源回收,不影响线程的生命周期。6.5 错误示例:等待分离状态的线程代码语言:javascript复制#include

#include

#include

#include

void *detached_thread_err(void *arg) {

pthread_detach(pthread_self()); // 设置为分离状态

sleep(2);

return NULL;

}

int main() {

pthread_t tid;

pthread_create(&tid, NULL, detached_thread_err, NULL);

sleep(1); // 确保子线程已设置为分离状态

// 错误:等待分离状态的线程

int ret = pthread_join(tid, NULL);

if (ret != 0) {

fprintf(stderr, "等待分离线程失败:%s\n", strerror(ret));

return 1;

}

return 0;

}运行结果:

代码语言:javascript复制等待分离线程失败:Invalid argument总结 线程控制是 Linux 多线程开发的基础,掌握这些操作后,才能进一步学习线程同步(互斥锁、条件变量、信号量)、线程安全等高级主题。后续将继续讲解线程同步、线程安全、线程池等高级主题,敬请关注!

创作不易,若本文对你有帮助,欢迎点赞、收藏、关注!

Copyright © 2088 星域启程-网游活动专题站 All Rights Reserved.
友情链接