跳到主要内容
Linux 多线程开发:线程创建、终止、等待与分离实战 | 极客日志
C
Linux 多线程开发:线程创建、终止、等待与分离实战 Linux 多线程开发核心在于资源管理与生命周期控制。本文详解 POSIX 线程库(pthread)的创建、终止、等待及分离机制,剖析进程与线程的资源共享与独占关系,通过实战代码演示常见错误与正确用法,帮助开发者避免资源泄漏与同步问题。
引言
在 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**选项明确链接该库,否则会出现'未定义引用'错误。
gcc thread_demo.c -o thread_demo -lpthread
gcc thread1.c thread2.c -o thread_app -lpthread
2.2 头文件:包含 <pthread.h> 所有pthread 库的函数声明、数据类型(如 pthread_t、pthread_attr_t)都定义在**<pthread.h>**头文件中,使用时必须包含:
2.3 错误处理:与传统系统调用不同 传统的 Linux 系统调用(如 open、read、fork)通常返回 -1 表示失败,并设置全局变量 errno 来指示错误原因。但 pthread 库的函数错误处理方式不同:
函数执行成功时返回 0。执行失败时,不设置全局**errno,而是直接返回错误码(非 0 值)。可以使用 strerror()**函数将错误码转换为人类可读的错误信息。
#include <stdio.h>
#include <pthread.h>
#include <string.h>
int main () {
pthread_t tid;
int ret = pthread_create(&tid, NULL , NULL , NULL );
if (ret != 0 ) {
fprintf (stderr , "创建线程失败:%s\n" , strerror(ret));
return 1 ;
}
return 0 ;
}
注意:pthread 库也提供了线程内的 errno 副本,以兼容依赖 errno 的代码,但对于 pthread 函数的错误,建议直接通过返回值判断,效率更高。
三、线程创建:启动一个新的执行流 线程创建是线程控制的第一步,通过**pthread_create**函数创建新线程,新线程会执行指定的函数,与主线程并行运行。
3.1 函数原型与参数解析 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:创建简单线程,执行无参数函数 #include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *thread_func (void *arg) {
for (int i = 0 ; i < 5 ; i++) {
printf ("子线程:我正在运行,线程 ID = %lu\n" , (unsigned long )pthread_self());
sleep(1 );
}
return 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 ;
}
gcc thread_simple.c -o thread_simple -lpthread
./thread_simple
主线程:成功创建子线程,子线程 ID = 140703347508992
主线程:我正在运行,线程 ID = 140703355896640
子线程:我正在运行,线程 ID = 140703347508992
主线程:我正在运行,线程 ID = 140703355896640
子线程:我正在运行,线程 ID = 140703347508992
主线程:我正在运行,线程 ID = 140703355896640
子线程:我正在运行,线程 ID = 140703347508992
主线程:我正在运行,线程 ID = 140703355896640
子线程:我正在运行,线程 ID = 140703347508992
主线程:我正在运行,线程 ID = 140703355896640
子线程:我正在运行,线程 ID = 140703347508992
主线程:子线程已退出,程序结束
示例 2:传递参数给线程函数(单个参数) #include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *thread_with_arg (void *arg) {
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 ;
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:传递多个参数给线程函数(结构体封装) #include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
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(LWP,Light Weight Process ID) :
由 Linux 内核维护,是系统全局唯一的标识,内核调度线程时使用。可以通过**ps -aL**命令查看,LWP 列显示的就是内核级线程 ID。主线程的 LWP 与进程 ID(PID)相同。
由 POSIX 线程库维护,是进程内唯一的标识,仅在当前进程中有效。通过**pthread_create的 thread参数获取,或通过 pthread_self()函数获取当前线程的 ID。类型 pthread_t**的具体实现可能是整数、指针等,不建议直接用 %d 打印,推荐用 %lu(转换为 unsigned long)。
示例 :运行示例 1 的 thread_simple 程序后,执行命令:
ps -aL | grep thread_simple
12345 12345 pts/0 00:00:00 thread_simple
12345 12346 pts/0 00:00:00 thread_simple
3.4 线程创建的注意事项
线程函数的返回值 :线程函数的返回值类型为**void*,可以返回任意类型的数据,但返回的内存必须是全局变量或堆内存(malloc 分配),不能是局部变量(线程退出后局部变量会被释放,导致野指针)。
参数传递的安全性 :传递给线程的参数如果是局部变量,要确保线程执行期间该变量不被销毁。如果主线程在子线程启动前就退出,局部变量会被释放,子线程访问时会出现未定义行为。
线程创建后的执行顺序 :线程创建后,主线程和子线程的执行顺序由 CPU 调度器决定,无法预测。不要假设子线程会在主线程之前执行,或反之。
系统线程数量限制 :Linux 系统对单个进程的线程数量有默认限制(通常为 1024),超出限制会导致 pthread_create返回 EAGAIN**错误。可以通过修改 /etc/security/limits.conf 文件调整限制。
四、线程终止:优雅结束线程执行 线程终止是指线程停止执行,释放自己的私有资源(如栈、寄存器上下文等),但进程的共享资源不会被释放。Linux 提供了三种合法的线程终止方式,以及一种不推荐的强制终止方式。
4.1 线程终止的三种合法方式
方式 1:从线程函数 return 返回(推荐) 线程函数执行完毕后,通过**return**语句返回,线程正常终止。这种方式最优雅,可以自然地清理线程内的局部变量,并且可以返回线程的执行结果。
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
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**获取。
void pthread_exit (void *value_ptr) ;
参数 value_ptr :指向线程退出状态的指针,与线程函数**return**的返回值作用相同。该指针指向的内存必须是全局变量或堆内存,不能是局部变量。
返回值 :无返回值,线程调用后直接终止。
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
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**函数,会终止整个进程,进程内的所有线程都会随之退出。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
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 );
sleep(3 );
printf ("主线程:退出,所有子线程将被终止\n" );
return 0 ;
}
4.2 线程的强制终止:调用 pthread_cancel 函数 一个线程可以调用**pthread_cancel**函数,强制终止同一进程中的另一个线程。这种方式相当于'杀死'其他线程,使用时需要谨慎,因为可能导致资源泄漏(如线程持有互斥锁未释放)。
int pthread_cancel (pthread_t thread) ;
参数 thread :要终止的线程的用户级 ID(pthread_t 类型)。
返回值 :成功返回 0,失败返回非 0 错误码。
**pthread_cancel只是向目标线程发送一个'取消请求',目标线程不会立即终止,而是在到达'取消点'时才会响应请求。取消点是线程执行过程中检查是否有取消请求的位置,常见的取消点包括: sleep、read、write、pthread_join等系统调用和库函数。线程可以通过 pthread_setcancelstate函数设置自己的取消状态(启用或禁用),通过 pthread_setcanceltype**函数设置取消类型(立即取消或延迟取消)。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *thread_to_cancel (void *arg) {
int i = 0 ;
while (1 ) {
printf ("子线程:第%d次运行,等待取消...\n" , ++i);
sleep(1 );
}
return NULL ;
}
int main () {
pthread_t tid;
pthread_create(&tid, NULL , thread_to_cancel, NULL );
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 ;
}
子线程:第 1 次运行,等待取消...
子线程:第 2 次运行,等待取消...
子线程:第 3 次运行,等待取消...
主线程:发送取消请求
主线程:子线程已被成功取消
4.3 线程终止的禁忌:避免使用 exit 函数 线程中绝对不要调用 exit 函数 !exit函数的作用是终止整个进程,而不是当前线程。一个线程调用 exit ,会导致进程内的所有线程(包括主线程)都被终止,这通常不是我们想要的结果。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
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**函数实现线程等待,该函数会阻塞调用线程(通常是主线程),直到目标线程终止。
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 返回的线程,获取返回值 #include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
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 终止的线程,获取退出状态 #include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
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:等待被取消的线程,判断取消状态 #include <stdio.h>
#include <pthread.h>
#include <unistd.h>
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:不关心退出状态,仅回收资源 #include <stdio.h>
#include <pthread.h>
#include <unistd.h>
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 );
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**函数可以将线程设置为分离状态,该函数可以由其他线程调用,也可以由线程自身调用。
int pthread_detach (pthread_t thread) ;
参数 thread :要设置为分离状态的线程的用户级 ID。
返回值 :成功返回 0,失败返回非 0 错误码。
6.3 线程分离的两种方式
方式 1:线程自身调用 pthread_detach(推荐) 线程在启动后,主动调用**pthread_detach(pthread_self())**,将自己设置为分离状态。这种方式最灵活,线程可以自主决定是否分离。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
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 );
sleep(3 );
printf ("主线程:程序结束,无需调用 pthread_join\n" );
return 0 ;
}
方式 2:其他线程调用 pthread_detach 主线程(或其他线程)在创建目标线程后,调用 pthread_detach 将其设置为分离状态。这种方式需要确保在目标线程终止前完成分离设置,否则可能导致资源泄漏。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
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 错误示例:等待分离状态的线程 #include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
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 ;
}
等待分离线程失败:Invalid argument
总结 线程控制是 Linux 多线程开发的基础,掌握这些操作后,才能进一步学习线程同步(互斥锁、条件变量、信号量)、线程安全等高级主题。后续将继续讲解线程同步、线程安全、线程池等高级主题。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online