线程(pthread):轻量级的进程,属于某个进程

(1)线程的基本信息:

1.定义:线程是进程内的执行单元,共享进程的内存和资源。同一进程内的多个线程可以并发执行,但线程崩溃可能导致整个进程终止。

2.概念:线程是轻量级的进程

3.目的:实现并发,提高效率,简化编程模型

4.线程的特征:

        ① 轻量级实体

        ②共享进程资源

        ③ 独立调度单位

        ④并发执行

        ⑤线程间协作

(2)主线程和子线程:

1.主线程(Main Thread)

  • 定义:进程启动时系统自动创建的第一个线程,是程序执行的入口点。
  • 特点
    1. 是进程的 “初始线程”,负责启动程序的核心逻辑(如 main 函数的执行)。
    2. 可以创建其他线程(子线程),并对其进行管理(如回收资源、同步协作)。
    3. 主线程的生命周期通常与进程绑定:若主线程退出且未回收子线程,整个进程可能终止(除非子线程已被标记为 “分离状态”)。
  • 示例:C 语言程序中,main 函数的执行线程就是主线程,它可以通过 pthread_create 创建子线程。

2.子线程(Child Thread)

  • 定义:由主线程或其他子线程通过线程创建函数(如 pthread_create)主动创建的线程。
  • 特点
    1. 依赖于创建它的线程(父线程)存在,但父线程退出后,子线程可继续运行(前提是进程未终止)。
    2. 与主线程共享进程的地址空间(全局变量、文件描述符等),但拥有独立的栈空间和程序计数器。
    3. 通常用于执行并行任务(如后台计算、I/O 操作),分担主线程的工作负载。
  • 示例:在之前的代码中,由 main 函数(主线程)通过 pthread_create 创建的 th 线程就是子线程,负责获取用户输入。

3.主线程和子线程的核心关系与区别

维度 主线程(Main Thread) 子线程(Child Thread)
创建方式 进程启动时系统自动创建 由主线程或其他子线程通过 pthread_create 创建
角色 程序入口,负责初始化和管理子线程 执行具体任务,分担主线程工作
生命周期 通常是进程的 “最后一个退出的线程” 可独立于父线程存在,但受进程生命周期限制
资源共享 与所有子线程共享进程资源 与主线程及其他子线程共享进程资源
退出影响 若主线程先退出,可能导致进程终止 子线程退出不影响主线程(除非

(3)进程和线程的区别:(都是为了实现并发)

1.进程线程与系统的关系:

        ①进程是系统最小的支援分配单位

        ②线程是系统最小的执行单位

2.适用场景

        ①进程适合的场景(大任务)
        需要高隔离性的任务,如浏览器多标签页、微服务架构中的独立服务。对安全性要求高的场景,如沙箱环境。

        ②线程适合的场景(大任务中的小任务)
        需要频繁共享数据的任务,如Web服务器处理并发请求。计算密集型任务需利用多核CPU时,可通过线程并行加速。

进程(Process) 线程(Thread)
定义 操作系统进行资源分配和调度的基本单位 进程内的一个执行单元,是操作系统调度的基本单位
资源分配 拥有独立的资源空间(内存、文件描述符、寄存器等) 不拥有独立资源,共享所属进程的全部资源
独立性 高:进程间相互独立,资源不共享一个进程崩溃不影响其他进程 低:线程共享进程资源(除了栈区不共享),一个线程崩溃可能导致整个进程崩溃
调度单位 资源分配的基本单位(非最小调度单位) 操作系统调度的最小单位(CPU 时间片分配给线程)
创建 / 销毁开销 大:需要分配独立的内存空间和资源 小:仅需分配少量私有资源(如栈、寄存器)
切换开销 大:需切换整个进程的资源上下文 小:仅需切换线程私有数据(如 PC、栈指针)
通信方式 需通过进程间通信(IPC)机制(管道、消息队列等) 通过共享内存直接通信(需同步机制避免冲突)
地址空间 每个进程有独立的地址空间 同一进程内的线程共享相同的地址空间
稳定性 稳定 不稳定
包含关系 一个进程可以包含多个线程 线程是进程的一部分,不能独立存在

(4)创建新线程函数

1.pthread_create()

int pthread_create(
    pthread_t *restrict tidp,        // 输出参数:新线程的ID
    const pthread_attr_t *restrict attr,  // 输入参数:线程属性(可NULL)
    void *(*start_routine)(void *),  // 输入参数:线程执行的函数(函数指针)
    void *restrict arg               // 输入参数:传递给线程函数的参数(可NULL)
);

1.一次pthread_create执行只能创建一个线程。
2.每个进程至少有一个线程称为主线程。
3.主线程退出则所有创建的子线程都退出。
4.主线程必须有子线程同时运行才算多线程程序。
5.线程id是线程的唯一标识,是CPU维护的一组数字。
6.pstree 查看系统中多线程的对应关系。
7.多个子线程可以执行同一回调函数。

2.pthread_self()获取当前线程 ID(TID)

  • 线程 ID 的唯一性
  • 与 pthread_create 输出的 ID 一致性
  • pthread_t 是抽象类型(可能是整数、指针或结构体),不能直接用 %d 或 %p 打印。在 Linux 系统中,通常可转换为 unsigned long 打印:
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
void *th1(void *arg)//子线程函数
{
    while(1)
    {
        printf("th1 发送视频 tid1:%lu\n",pthread_self());
        sleep(1);
    }
    return NULL;
}
void *th2(void *arg)
{
    while(1)
    {
        printf("th2 接收控制 tid2:%lu\n",pthread_self());
        sleep(1);
    }
    return NULL;
}
int main(int argc,char **argv)
{
    pthread_t tid1,tid2;// 存储新线程ID的变量
                        // 传入tid的地址,创建后tid会被赋值为新线程的ID
    pthread_create(&tid1,NULL,th1,NULL);
    pthread_create(&tid2,NULL,th2,NULL);
    while(1)
    {
        printf("main_th tid%lu\n",pthread_self());
        sleep(1);
    }
    return 0;
}

(5)线程退出:

1. 线程函数自然返回(推荐)

线程执行完入口函数(start_routine)后正常返回,是最安全的退出方式。

特点

  • 线程会自动清理自身栈资源,不会导致资源泄漏;
  • 可通过返回值向主线程传递结果;
  • 适用于任务明确、可正常执行完毕的场景。
2.调用 pthread_exit 函数(主动退出)

线程在执行过程中,可通过 pthread_exit 主动终止,功能类似进程中的 exit,但仅退出当前线程。

void pthread_exit(void* retval); // retval:返回给主线程的结果
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void *th(void *arg)
{
    int i=3;
    while(i--)
    {
        printf("th tid:%lu\n",pthread_self());
        sleep;
    }
    pthread_exit(NULL);
}
int main(int argc,char **argv)
{
    pthread_t tid;
    pthread_create(&tid,NULL,th,NULL);
    while(1)
    {
        printf("main_th tid:%lu\n",pthread_self());
        sleep(1);
    }
    return 0;
}

特点

  • 可在函数任意位置触发退出(如条件满足时);
  • 退出后会自动释放线程私有资源(如栈);
  • 若线程是 JOINABLE 状态,仍需 pthread_join 回收,否则会产生 “僵尸线程”。
3. 被其他线程取消(pthread_cancel
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void* th(void* arg)
{
  
  while (1)
  {
    printf("th tid:%lu\n", pthread_self());
    sleep(1);
  }
  // return NULL;
  pthread_exit(NULL);
}

int main(int argc, char** argv)
{
  pthread_t tid;
  pthread_create(&tid, NULL, th, NULL);
  int i = 0 ;
  while (1)
  {
    printf("main_th tid:%lu\n", pthread_self());
    sleep(1);
    i++;
    if(3 == i )
    {
      pthread_cancel(tid);
    }
  }

  return 0;
}

主线程或其他线程可通过 pthread_cancel 强制终止目标线程(需目标线程允许被取消)。

int pthread_cancel(pthread_t thread);  // thread:目标线程ID

特点

  • 属于 “强制终止”,可能导致资源未释放(如已加的锁未释放);
  • 目标线程需处于 “可取消状态”(默认允许,可通过 pthread_setcancelstate 禁用);
  • 被取消的线程返回值固定为 PTHREAD_CANCELED
退出方式 适用场景 安全性 资源释放
函数自然返回 任务完成后正常退出 最高 自动释放
pthread_exit 主动退出 中途满足退出条件 自动释放
pthread_cancel 取消 强制终止异常线程 较低 可能泄漏资源

(6)线程回收

                线程回收是指主线程(或其他线程)获取已终止子线程的退出状态、释放线程占用的资源(如线程控制块 TCB)的操作。若不及时回收,终止的线程会成为 “僵尸线程”,浪费系统资源。             

1.   pthread_join(阻塞式回收)

                

阻塞调用线程(通常是主线程),直到目标子线程终止,然后获取子线程的退出状态并释放资源。

#include <pthread.h>

// 成功返回 0,失败返回错误码(如 ESRCH:目标线程不存在)
int pthread_join(pthread_t thread, void **retval);
2. 参数详解
参数名 类型 作用说明
thread pthread_t 待回收的目标子线程 ID(由 pthread_create 的第一个参数输出)。
retval void ** 用于接收目标子线程的退出状态(输出参数),需注意以下细节:
- 若不需要获取退出状态,可传 NULL
- 若子线程通过 return 返回值(如 return (void*)100;),*retval 会指向该返回值;
- 若子线程通过 pthread_exit(void *retval) 退出,*retval 会接收 pthread_exit 的参数;
- 若子线程被 pthread_cancel 取消,*retval 会被设为 PTHREAD_CANCELED
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>


void* th(void* arg)
{   int i=5;
    while(i--)
    {
        printf("th processing\n");
        sleep(1);
    }
    //char str[]="我要结束了"; error ,是局部变量,当th结束,空间释放。
    //static char str[]="我要结束了";
    char*  str = malloc(20);
    strcpy( str,"我要结束了");
    pthread_exit(str);  //终止当前线程,将str传给回收者pthread_join
}

int	main(int argc, char **argv)
{
    pthread_t tid;
    pthread_create(&tid,NULL,th,NULL);
    void* ret=NULL;
    pthread_join(tid,&ret);//回收目标线程pthread_exit,并回收退出的资源并获取状态
    //void* -> 其他类型的指针 需要强转
    printf("ret %s\n",(char* )ret);
    free(ret);
    return 0;
}
3. 核心特点
  • 阻塞性:调用 pthread_join 的线程(如主线程)会暂停执行,直到目标子线程终止后才继续。
  • 一对一回收:一个 pthread_join 只能回收一个指定的子线程(需明确目标线程 ID)。
  • 资源释放:回收成功后,目标线程的所有资源(如 TCB、栈空间等)会被彻底释放,不会成为僵尸线程。

    pthread_exit() 和 pthread_join() 协作关系

    pthread_exit() 和 pthread_join() 通常是 “终止 - 回收” 的协作关系,流程如下:

    1. 子线程调用 pthread_exit():子线程完成任务后,主动调用 pthread_exit(retval) 终止自己,并将 retval 作为退出状态(如计算结果)。
    2. 主线程调用 pthread_join():主线程调用 pthread_join(tid, &exit_status),阻塞等待子线程终止,然后通过 exit_status 获取子线程传递的 retval,同时回收子线程的 TCB 资源。


    (7)参数传递

    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    void *th(void *arg)
    {
        char *tmp=(char *)arg;
        strcpy(tmp,"hello world");
        return tmp;
    }
    int main(int argc,char **argv)
    {
        pthread_t tid=0;// 存储新线程ID的变量
                        // 传入tid的地址,创建后tid会被赋值为新线程的ID
        char *p=malloc(20);//打开一个空间p
        pthread_create(&tid,NULL,th,p);//p为传递给线程函数的参数(arg)
        void *ret=NULL;
        pthread_join(tid,&ret);//参数的传递过程:p->arg->tmp->ret
        printf("ret is %s\n",(char *)ret);
        return 0;
    }

    结构体:

    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    typedef struct MyStruct
    {
        char name[50];
        int age;
        char addr[100];
    }PER;
    void* th(void* arg)  // 线程入口函数,参数为 void* 类型(通用指针)
    {
        // 将传入的 void* 指针转换为 PER* 类型(结构体指针)
        PER* per = (PER*)arg;
        
        // 获取用户输入的姓名
        printf("input name:");
        // fgets 从标准输入(键盘)读取字符串,存入 per->name,最多读 sizeof(per->name)-1 个字符
        fgets(per->name, sizeof(per->name), stdin);
        // 去除 fgets 读取的换行符 '\n'(因为用户输入后按回车会产生 '\n')
        per->name[strlen(per->name)-1] = '\0';
        
        // 获取用户输入的年龄(先读字符串,再转换为整数)
        printf("input age:");
        char tmp[5] = {0};  // 临时存储年龄的字符串
        fgets(tmp, sizeof(tmp), stdin);
        per->age = atoi(tmp);  // 将字符串转换为整数(如 "20" → 20)
        
        // 获取用户输入的地址
        printf("input addr:");
        fgets(per->addr, sizeof(per->addr), stdin);
        // 注意:这里没有去除换行符,因此打印时会保留用户输入的回车
        
        // 返回结构体指针(供主线程通过 pthread_join 获取)
        return per;
    }
    int main(int argc, char **argv)
    {
        PER per;  // 定义 PER 结构体变量(在主线程的栈上)
        
        pthread_t tid;  // 存储子线程 ID
        pthread_create(&tid, NULL, th, &per);
        // 创建子线程:
        // 参数1:tid 的地址(输出子线程 ID)
        // 参数2:NULL(使用默认线程属性)
        // 参数3:线程入口函数 th
        // 参数4:传递给 th 的参数(&per,即结构体指针)
        
        void* ret = NULL;  // 存储子线程的返回值
        // 等待子线程终止,并获取返回值:
        // 参数1:子线程 ID(tid)
        // 参数2:ret 的地址(输出子线程的返回值,即 per 的指针)
        pthread_join(tid, &ret);
        
        // 打印子线程获取的信息:
        // 将 ret 转换为 PER* 类型,访问结构体成员
        printf("name:%s age:%d addr:%s\n", 
               ((PER*)ret)->name, 
               ((PER*)ret)->age, 
               ((PER*)ret)->addr);
        
        return 0;
    }
    • 参数传递:子线程通过 arg 参数接收主线程传递的 PER 结构体指针,实现数据共享(子线程填充数据,主线程后续读取)。
    • 输入处理
      • fgets 用于安全读取字符串(避免缓冲区溢出),但会将用户输入的回车(\n)也读入,因此姓名需要手动去除换行符。
      • 年龄需要先读入字符串,再通过 atoi 转换为整数(fgets 不能直接读取整数)。

    理想输出:

    input name:lisi       # 用户输入姓名
    input age:20          # 用户输入年龄
    input addr:sichuan    # 用户输入地址
    name:lisi age:20 addr:sichuan  # 主线程打印结果(地址后会有换行,因为没去除)

    (8)清理函数:

    清理函数的核心作用:

    • 确保线程在异常退出(如被取消) 时仍能释放资源(如锁、内存);
    • 统一管理清理逻辑,避免在多个退出路径中重复编写释放代码。

    1. 注册清理函数:pthread_cleanup_push

    #include <pthread.h>
    
    void pthread_cleanup_push(void (*routine)(void *), void *arg);
    • 作用:向当前线程的 “清理函数栈” 中注册一个清理函数。
    • 参数
      • routine:清理函数指针,函数原型为 void (*)(void *),线程退出时会被调用;
      • arg:传递给清理函数的参数。
    • 特点
      清理函数按 “后进先出(LIFO)” 顺序执行(最后注册的函数最先执行),类似栈结构。

    2. 移除清理函数:pthread_cleanup_pop

    void pthread_cleanup_pop(int execute);
    • 作用:从线程的 “清理函数栈” 中移除最顶部的清理函数。
    • 参数
      • execute 为 0:仅移除清理函数,不执行;
      • execute 非 0:移除清理函数的同时执行该函数。
    • 强制配对pthread_cleanup_push 和 pthread_cleanup_pop 必须成对出现(编译器会将它们视为语句块,缺少任何一个会导致编译错误)。
    #include <stdio.h>
    #include <pthread.h> 
    #include <stdlib.h>
    #include <string.h>
    typedef struct 
    {
        FILE*fp;
        char* p;
    }
     TH_ARGS;
     void clean(void *arg )
     {
        TH_ARGS* tmp = (TH_ARGS*)arg;
        printf("this clean ,p is %s\n",tmp->p);
        free(tmp->p);
        fclose(tmp->fp);
       
     }
    void* th(void* arg)
    {
        FILE* fp =fopen("1.txt","w");
        char* p = malloc(20);
        TH_ARGS args={0};
        args.fp = fp;
        args.p = p;
        pthread_cleanup_push(clean,&args);
    
        fputs("hello",fp);
        strcpy(p,"hello");
    
        printf("th well done\n");
        pthread_cleanup_pop(1);
        return NULL;
        
    }
    
    int	main(int argc, char **argv)
    {
        
        pthread_t tid;
        pthread_create(&tid,NULL,th,NULL);
        pthread_join(tid,NULL);
        printf("main th will exit...\n");
    
        return 0;
    }

    3.何时触发清理函数:

    注册的清理函数会在以下三种场景下自动执行:

            ①线程调用 pthread_exit 退出时;

            ②线程被其他线程调用 pthread_cancel 取消时;

            ③调用 pthread_cleanup_pop 且 execute 参数非 0 时。

    注意:线程函数正常 return 退出时,不会触发清理函数(这是最容易出错的点)。

    4.关键注意事项

            ①配对使用pthread_cleanup_push 和 pthread_cleanup_pop 必须成对出现,否则会导致编译错误(编译器将它们视为宏,依赖栈结构实现)。

            ②执行顺序:清理函数按 “后进先出” 顺序执行(最后注册的先执行)

    pthread_cleanup_push(clean1, arg1);  // 先注册
    pthread_cleanup_push(clean2, arg2);  // 后注册
    pthread_cleanup_pop(1);  // 执行 clean2
    pthread_cleanup_pop(1);  // 执行 clean1

            ③return 不触发清理函数:线程函数通过 return 正常退出时,清理函数不会自动执行(仅 pthread_exitpthread_cancel 或 pthread_cleanup_pop(execute!=0) 会触发)。若需在 return 时执行清理,需手动调用 pthread_cleanup_pop(1)

            ④避免重复释放:清理函数和正常退出路径可能都涉及资源释放(如解锁),需确保逻辑不冲突(例如用标志位判断是否已释放)。

    (9)进程和线程调用函数的区别

    功能 进程(Process)相关函数 线程(Thread)相关函数 核心差异
    创建 fork() / vfork() / clone()(系统调用) pthread_create()(库函数) - 进程创建是复制整个地址空间(fork),线程创建是共享地址空间
    - 进程创建开销大,线程创建开销小
    执行新程序 exec 系列(execl/execvp 等) 无(线程共享进程地址空间,不能直接替换整个线程代码) 进程可通过 exec 完全替换自身代码,线程只能通过函数调用切换执行逻辑
    终止 exit() / _exit()(进程自身终止) pthread_exit()(线程自身终止) exit() 终止整个进程,pthread_exit() 仅终止当前线程
    - 进程终止释放所有资源,线程终止仅释放私有资源
    被动终止 kill() / pthread_kill()1(向进程发送信号) pthread_cancel()(请求线程取消) - 进程可被信号杀死,线程需配合取消状态(默认允许)
    - 线程取消需处理清理函数避免资源泄漏
    回收资源 wait() / waitpid()(回收子进程) pthread_join()(回收子线程) - 进程回收获取退出状态码,线程回收获取返回值指针
    - 线程可通过 pthread_detach() 自动回收
    Logo

    智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

    更多推荐