早期 Linux没有内核原生线程,只有进程。
-
进程的痛点 进程是资源分配的最小单位,创建进程
fork()会复制页表、文件描述符、内存空间,开销大;进程间切换要刷新 TLB、更换地址空间,上下文切换开销高;多任务如果全部用多进程,系统性能损耗很大。 -
轻量级进程 LWP(Light Weight Process) Linux 内核没有单独的 "线程" 内核对象,Linux 下线程本质就是共享资源的进程 。 内核提供
clone()系统调用,clone()可以选择性共享:虚拟地址空间、文件描述符、信号处理、用户 ID 等资源。
进程的空间开辟速度和开销量较大,侧重为一个资源分配的最小单位,而线程侧重于成为一个基本执行体,线程主要由程序计数器、相关寄存器、线程栈构成,开辟更快,更灵活。
1. 线程创建 pthread_create
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);
thread:输出参数,保存用户态线程 IDpthread_tattr:线程属性,NULL使用默认属性,默认可以结合start_routine:线程执行函数,**返回值 void*,参数 void*arg:传给线程函数的参数;传多个参数传结构体指针- 返回值:成功返回 0,失败返回错误码
同一进程下的多个线程,资源共享,共用同一块进程空间,如果操作同一块资源会相互影响,线程之间的关系是对等的,并不存在父子关系。
void *do_thread1(void *arg)
{
int *var = arg;
while(1)
{
printf("---do_thread1 run ---\n");
(*var)++;
printf("---thread1 var+1---\n---------------------\n");
sleep(1);
}
return NULL;
}
int ret = pthread_create(&tid1,NULL,do_thread1,&var);
if(ret != 0)
{
errno = ret;
perror("pthread_create fail");
return -1;
}
注意:Linux早期内核并不具备真正的线程支持,内核中只有进程这一调度实体,但可以借助clone()系统调用创建调用进程的拷贝,通过参数配置让新创建的实体与原进程共享地址空间等资源,内核依旧为其生成独立的task_struct进程控制块,由用户态线程库把这类轻量级进程封装成线程使用;后期NPTL结合CLONE_THREAD标志引入线程组,完善了POSIX线程语义,不过底层实现依旧基于clone()。
线程被创建后,内核调度,立刻就会执行 start_routine。
三种结束线程执行的方式:
- 线程函数
return;→ 正常退出 - 线程内部调用
pthread_exit(void *retval);→ 主动退出 - 被别的线程取消
pthread_cancel(tid);
⚠️主线程 return 0; 会整个进程结束,所有线程全部销毁 ,不要用 return 退出主线程而子线程还在跑;主线程想等待子线程,用pthread_join。
pthread_exit 线程主动退出
void pthread_exit(void *retval);
retval:线程退出返回值,给pthread_join接收;不能传局部变量地址!局部变量函数结束销毁。类似于进程的exit- 在子线程调用:只退出当前线程,不影响其他线程。
pthread_join 阻塞等待线程回收
int pthread_join(pthread_t thread, void **retval);
-
阻塞等待指定线程结束,回收线程资源;类似进程的wait阻塞等待回收进程
-
retval接收线程返回值:return的值 或pthread_exit参数;传NULL代表不关心返回值。 -
默认非分离线程必须 join,否则产生线程僵尸,资源泄漏。
void *func(void arg)
{
pthread_exit((void)200);
}int main()
{
pthread_t tid;
void *res;
pthread_create(&tid,NULL,func,NULL);
pthread_join(tid, &res);
printf("线程退出值:%ld\n",(long)res);
return 0;
}
在线程内部,单独调用exit退出并返回的效果,与直接return返回结束线程,并返回退出值的效果是一样的。如果需要带出的是一个字符串,告知主线程,子线程结束了,可以直接return然后接双引号,输入需要带回的字符串,这样的字符串会放在常量区,可以直接定义一个字符型指针接收。
pthread_cancel 取消线程(被动退出)
int pthread_cancel(pthread_t thread);
传入参数为,需要控制的线程的tid参数,类似于kill命令控制进程的效果
-
取消状态:pthread_setcancelstate ()
PTHREAD_CANCEL_ENABLE:允许被取消(默认)PTHREAD_CANCEL_DISABLE:忽略pthread_cancel(),取消请求挂起
-
取消类型:pthread_setcanceltype ()
PTHREAD_CANCEL_ASYNCHRONOUS:异步取消,收到 cancel 信号尽量立刻终止线程,可以在任意指令点退出(风险高,资源容易泄漏)PTHREAD_CANCEL_DEFERRED:延迟取消(系统默认),不会马上退出,线程执行到 ** 取消点 (cancellation point)** 才真正退出。
大部分系统调用是取消点:sleep、read、write、pthread_testcancel、pthread_cond_wait 等。 如果线程一直在做纯 CPU 运算、没有任何取消点,就算调用了pthread_cancel,线程会继续跑,不会退出。可以手动调用 pthread_testcancel() 设置人工取消点。
可结合 vs 分离态-----joinable /detached
这一组只管线程结束之后资源怎么处理 ,完全不控制什么时候终止。
- 可结合态(joinable,默认创建的时候选择NULL) :线程退出后,线程的描述符、退出状态保留,必须调用
pthread_join(),才会释放线程资源。如果线程被 cancel 终止,pthread_join可以拿到PTHREAD_CANCELED返回值。 - 分离态(detached) :线程结束(不管是 return、pthread_exit、被 pthread_cancel 取消)之后,系统自动回收线程资源,不需要也不能 pthread_join。
以下函数的返回值均为,成功返回0,不成功返回非0值!!!!
设置线程属性pthread_attr_t
int pthread_attr_init(pthread_attr_t *attr);
//初始化线程属性结构体,把 attr 设置成默认属性值。
int pthread_attr_destroy(pthread_attr_t *attr);
//销毁属性对象,释放 attr 内部分配的资源,attr 变成无效状态。
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
//设置线程属性里的分离状态,只对后续 pthread_create 创建的线程生效。
使用流程:初始化属性 → 设置属性 → create 传入 attr → 销毁属性
初始化属性:pthread_attr_init;
pthread_attr_t attr;
//定义attr变量
pthread_attr_init(&attr);
//初始化attr变量
设置属性:int pthread_attr_setdetachstate
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
// detachstate 两个宏:
// PTHREAD_CREATE_JOINABLE 可接合(默认,需要join)
// PTHREAD_CREATE_DETACHED 分离状态,不需要join,结束自动释放资源
创建线程的时候设置state: 若此时设置为不可结合态,则线程结束自动回收资源,不能调用 pthread_join (),join 会报错。
pthread_create(&tid, &attr, thread_func, NULL);
//传入attr参数,不再使用NULL默认可结合的状态参数
销毁属性变量:
pthread_attr_destroy(&attr);
线程创建之后,动态修改分离属性 pthread_detach
int pthread_detach(pthread_t thread);
传入参数就是需要改变分离属性的线程对应的tid参数
- 将一个 ** 可接合 (joinable)** 线程设置成分离状态;
- 调用之后,该线程退出自动释放资源,不能再
pthread_join; - 可以自己在线程内部调用
pthread_detach(pthread_self());
线程退出清理函数:pthread_cleanup
线程被 cancel 取消,或者 pthread_exit 退出,执行资源释放;return不会触发清理函数!!
//注册清理函数
void pthread_cleanup_push(void (*routine)(void *), void *arg);
//执行清理函数
void pthread_cleanup_pop(int execute);
非0 执行清理函数
0 ,不执行清理
两个函数必须成对出现,不然会造成语法不完整,编译报错。
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<unistd.h>
pthread_t mtid; // 保存主线程的线程ID
// 子线程执行函数
void *do_thread(void *arg)
{
printf("---do_thread---tid = %ld\n",pthread_self());
sleep(1); // 休眠1秒,让主线程先运行起来
pthread_cancel(mtid); // 发送取消请求,尝试取消主线程
return NULL;
}
// 线程清理回调函数:线程被取消触发点退出时执行
void cleanup(void *arg)
{
printf("cleanup ------\n");
}
int main(int argc, const char *argv[])
{
mtid = pthread_self(); // 获取主线程ID存入全局变量
pthread_t tid;
// 创建子线程,执行do_thread,传入参数NULL
int ret = pthread_create(&tid,NULL,do_thread,NULL);
if(ret != 0)
{
errno = ret; // pthread函数错误码存在返回值,赋值给errno给perror打印
perror("pthread_create fail");
return -1;
}
// 注册线程清理处理函数
// push:把cleanup压入清理栈;第二个参数为传给cleanup的实参
pthread_cleanup_push(cleanup,NULL);
printf("tid = %ld\n",tid); // 打印子线程id
printf("main_tid = %ld\n",pthread_self());// 打印主线程id
sleep(3);
// pthread_exit(NULL); // 如果调用pthread_exit,主线程主动退出,会触发cleanup
// 死循环休眠,主线程阻塞在此
while(1)
sleep(1);
// pop弹出清理栈,参数非0:执行注册的cleanup函数;0:不执行直接丢弃
// 注意:pthread_cleanup_push / pop 是宏,必须成对出现,不能中间return跳出
pthread_cleanup_pop(1);
return 0;
}
进程与线程的区别:
进程:操作系统资源分配的最小单位。每个进程拥有独立虚拟地址空间、文件描述符、信号处理、堆、栈、代码段。
线程 :CPU 调度的最小单位。一个进程内的多条执行流,共享进程绝大部分资源,每个线程仅有自己私有栈、寄存器、线程局部存储。
| 对比项 | 进程 (process) | 线程 (thread,pthread) |
|---|---|---|
| 资源拥有 | 独立地址空间,独占堆、全局变量、fd、信号 | 共享进程地址空间,共享堆、全局变量、文件描述符;仅私有栈、寄存器、TLS |
| 创建方式 | fork()系统调用 |
pthread_create()库函数 |
| 开销 | 大。拷贝页表,虚拟内存复制,耗时高 | 小。仅新建栈,复用进程资源,创建销毁快 |
| 上下文切换 | 切换页表,刷新 TLB,开销很大 | 同一进程内切换,不用换页表,开销小 |
| 内存隔离 | 进程之间完全隔离,一个进程崩溃,其他进程不受影响 | 同进程所有线程共享地址空间;任意一个线程崩溃,整个进程直接退出 |
| 通信方式 (IPC) | 管道、socket、消息队列、共享内存、信号 | 直接读写全局变量 / 堆内存,但必须加锁pthread_mutex;也可用条件变量 |
| 返回回收 | 父进程wait()/waitpid()回收子进程,防止僵尸进程 |
pthread_join()回收,或设置分离属性 detach |
| 信号处理 | 信号发给整个进程,由进程内某一个线程响应 | 信号是进程级别,线程可设置掩码 |
高频坑点
- 线程没有独立内存:线程内野指针越界,整个进程直接段错误崩溃;而进程崩溃不会影响别的进程。
- 全局变量、malloc 堆内存全部线程共享;局部栈变量是线程私有。
- fork 只会复制调用线程,子进程里面只有一个线程,多线程程序慎用 fork。
- 进程间不能直接指针传递,虚拟地址空间隔离;同进程线程之间指针可以直接传。
- 僵尸进程:fork 子进程退出父不 wait;僵尸线程:pthread 不 join 也不 detach。
线程进程如何选择
- 需要强隔离、一个任务挂掉不影响主程序 → 多进程
- 任务之间大量数据交互,追求切换速度,不怕一个任务崩全部挂 → 多线程