嵌入式解谜日志之Linux操作系统—线程3
是进程内的执行单元,共享进程的内存和资源。同一进程内的多个线程可以并发执行,但线程崩溃可能导致整个进程终止。
线程(pthread):轻量级的进程,属于某个进程
(1)线程的基本信息:
1.定义:线程是进程内的执行单元,共享进程的内存和资源。同一进程内的多个线程可以并发执行,但线程崩溃可能导致整个进程终止。
2.概念:线程是轻量级的进程
3.目的:实现并发,提高效率,简化编程模型
4.线程的特征:
① 轻量级实体
②共享进程资源
③ 独立调度单位
④并发执行
⑤线程间协作
(2)主线程和子线程:
1.主线程(Main Thread)
- 定义:进程启动时系统自动创建的第一个线程,是程序执行的入口点。
- 特点:
- 是进程的 “初始线程”,负责启动程序的核心逻辑(如
main函数的执行)。 - 可以创建其他线程(子线程),并对其进行管理(如回收资源、同步协作)。
- 主线程的生命周期通常与进程绑定:若主线程退出且未回收子线程,整个进程可能终止(除非子线程已被标记为 “分离状态”)。
- 是进程的 “初始线程”,负责启动程序的核心逻辑(如
- 示例:C 语言程序中,
main函数的执行线程就是主线程,它可以通过pthread_create创建子线程。
2.子线程(Child Thread)
- 定义:由主线程或其他子线程通过线程创建函数(如
pthread_create)主动创建的线程。 - 特点:
- 依赖于创建它的线程(父线程)存在,但父线程退出后,子线程可继续运行(前提是进程未终止)。
- 与主线程共享进程的地址空间(全局变量、文件描述符等),但拥有独立的栈空间和程序计数器。
- 通常用于执行并行任务(如后台计算、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() 通常是 “终止 - 回收” 的协作关系,流程如下:
- 子线程调用
pthread_exit():子线程完成任务后,主动调用pthread_exit(retval)终止自己,并将retval作为退出状态(如计算结果)。 - 主线程调用
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_exit、pthread_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() 自动回收 |
更多推荐



所有评论(0)