跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
C算法

Linux 线程控制核心指南:创建、终止与资源管理

综述由AI生成Linux 多线程开发中线程控制是核心技能,涉及进程与线程的资源划分、POSIX 线程库使用及关键操作。文章详解了线程的私有资源与共享资源区别,涵盖 pthread_create 创建、pthread_exit 或 return 终止、pthread_join 等待回收资源以及 pthread_detach 分离状态设置。通过实战代码示例展示了参数传递、错误处理及取消机制,帮助开发者理解线程生命周期并避免资源泄漏与死锁问题。

星辰大海发布于 2026/3/16更新于 2026/4/265 浏览
Linux 线程控制核心指南:创建、终止与资源管理

文章配图


前言

在 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> 头文件中,使用时必须包含:

#include <pthread.h>

2.3 错误处理:与传统系统调用不同

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

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

错误处理示例:

#include <stdio.h>
#include <pthread.h>
#include <string.h> // 包含 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;
}

运行结果:

创建线程失败:Invalid argument

注意: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> // 包含 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;
}

编译运行:

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) {
    // 将 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:传递多个参数给线程函数(结构体封装)
#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)相同。

用户级线程 ID(pthread_t):

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

查看线程信息的 bash 命令:

# 编译运行线程程序后,查看线程信息
ps -aL | grep 程序名

示例:运行示例 1 的 thread_simple 程序后,执行命令:

ps -aL | grep thread_simple

输出结果:

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=12346

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);
    // 主线程休眠 3 秒后退出
    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); // 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;
}

运行结果:

子线程:第 1 次运行,等待取消...
子线程:第 2 次运行,等待取消...
子线程:第 3 次运行,等待取消...
主线程:发送取消请求
主线程:子线程已被成功取消

4.3 线程终止的禁忌:避免使用 exit 函数

线程中绝对不要调用 exit 函数!exit 函数的作用是终止整个进程,而不是当前线程。一个线程调用 exit,会导致进程内的所有线程(包括主线程)都被终止,这通常不是我们想要的结果。

错误示例:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h> // 包含 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 函数实现线程等待,该函数会阻塞调用线程(通常是主线程),直到目标线程终止。

函数原型:

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);
    // 传递 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 函数可以将线程设置为分离状态,该函数可以由其他线程调用,也可以由线程自身调用。

函数原型:

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);
    // 主线程休眠 3 秒,确保子线程执行完毕
    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 多线程开发的基础,掌握这些操作后,才能进一步学习线程同步(互斥锁、条件变量、信号量)、线程安全等高级主题。

目录

  1. 前言
  2. 一、先搞懂核心:Linux 进程与线程的资源共享与独占
  3. 1.1 进程与线程的核心定位
  4. 1.2 线程的私有资源:独属于工人的工具
  5. 1.3 线程共享的进程资源:车间里的公共设施
  6. 1.4 关键问题:单进程本质是单线程进程
  7. 二、POSIX 线程库:Linux 线程控制的工具箱
  8. 2.1 编译与链接:必须链接 pthread 库
  9. 编译单个文件 gcc threaddemo.c -o threaddemo -lpthread
  10. 编译多个文件 gcc thread1.c thread2.c -o thread_app -lpthread
  11. 2.2 头文件:包含 <pthread.h>
  12. 2.3 错误处理:与传统系统调用不同
  13. 三、线程创建:启动一个新的执行流
  14. 3.1 函数原型与参数解析
  15. 参数详解:
  16. 返回值:
  17. 3.2 线程创建实战示例
  18. 示例 1:创建简单线程,执行无参数函数
  19. 示例 2:传递参数给线程函数(单个参数)
  20. 示例 3:传递多个参数给线程函数(结构体封装)
  21. 3.3 线程 ID 的两种类型与获取方式
  22. 编译运行线程程序后,查看线程信息
  23. 3.4 线程创建的注意事项
  24. 四、线程终止:优雅结束线程执行
  25. 4.1 线程终止的三种合法方式
  26. 方式 1:从线程函数 return 返回(推荐)
  27. 方式 2:调用 pthread_exit 函数终止自己
  28. 方式 3:主线程正常退出(main 函数 return)
  29. 4.2 线程的强制终止:调用 pthread_cancel 函数
  30. 4.3 线程终止的禁忌:避免使用 exit 函数
  31. 五、线程等待:等待线程终止,回收资源
  32. 5.1 为什么需要线程等待?
  33. 5.2 线程等待函数:pthread_join
  34. 参数详解:
  35. 返回值:
  36. 不同终止方式对应的退出状态:
  37. 5.3 线程等待实战示例
  38. 示例 1:等待 return 返回的线程,获取返回值
  39. 示例 2:等待 pthread_exit 终止的线程,获取退出状态
  40. 示例 3:等待被取消的线程,判断取消状态
  41. 示例 4:不关心退出状态,仅回收资源
  42. 5.4 线程等待的注意事项
  43. 六、线程分离:自动回收资源,无需等待
  44. 6.1 线程分离的作用与原理
  45. 6.2 线程分离函数:pthread_detach
  46. 6.3 线程分离的两种方式
  47. 方式 1:线程自身调用 pthread_detach(推荐)
  48. 方式 2:其他线程调用 pthread_detach
  49. 6.4 线程分离的注意事项
  50. 6.5 错误示例:等待分离状态的线程
  51. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • 💰 8折买阿里云服务器限时8折购买
  • 🦞 5分钟部署阿里云小龙虾了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 鸿蒙超级终端多设备协同开发
  • 黑客概念内涵的演变与渗透测试基础指南
  • OpenClaw 与 Discord 多 Agent 多频道配置指南
  • DeepSeek 时代:前端开发者的护城河与转型方向
  • 网络安全工程师七年实战经验与技能成长指南
  • CSS 元素显示模式详解:块级、行内与行内块
  • AI Agent 新范式:FastGPT 集成 MCP 协议构建工具增强型智能体
  • RxJava 源码深度解析:订阅流程与线程切换原理
  • 深入理解 Agent:定义、构建模式与最佳实践
  • ChatGLM 实战:基于 LangChain 构建私有知识库
  • 2.5D 等距透视废墟建筑 AIGC 辅助量产工作流
  • Java RESTful 接口开发:Spring Boot 实战指南
  • STM32 HAL 库 UART 的 DMA 双缓冲与半传输中断应用
  • 国内多家大厂大模型岗位面试经验与总结
  • VS Code 运行前端代码教程
  • 国产多模态大模型 InternLM-XComposer 2.5 升级,原生支持 24K 图文上下文
  • 本地部署 Stable Diffusion:零基础搭建 AI 文生图模型
  • GLM-4.6V-Flash-WEB AI 习题解析案例展示
  • OpenAI 发布 GPT-4o 多模态模型及接入方式详解
  • Python 脚本打包成 EXE 应用的 6 种主流方法

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • 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