学习视频链接
目录
一、线程概念
1.1 什么是线程
LWP:light weight process 轻量级的进程,本质仍是进程(在 Linux 环境下)
进程:独立地址空间,拥有 PCB
线程:有独立的 PCB,但没有独立的地址空间(共享)
区别:在于是否共享地址空间 独居(进程);合租(线程)
Linux下:
线程:最小的执行单位
进程:最小分配资源单位,可看成是只有一个线程的进程
进程创建线程,每个线程都有 PCB,原来的进程变成线程了
1.2 查看火狐浏览器的线程
进程 id 是同一个,进程号有多少,共有 22 个
1.3 Linux内核线程实现原理
类 Unix 系统中,早期是没有“线程”概念的,80 年代才引入(应减少信号和线程混用),借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切
1、轻量级进程(light-weight process),也有 PCB,创建线程使用的底层函数和进程一样,都是 clone
2、从内核里看进程和线程是一样的,都有各自不同的 PCB,但是 PCB 中指向内存资源的三级页表(参考操作系统分页式存储)是相同的
3、进程可以蜕变成线程
4、线程可看做寄存器和栈的集合
5、在 linux 下,线程最是小的执行单位;进程是最小的分配资源单位
察看 LWP 号:ps -Lf pid 查看指定线程的 lwp 号
1.4 线程共享资源
1、文件描述符表
2、每种信号的处理方式
3、当前工作目录
4、用户 ID 和组 ID
5、内存地址空间(.text/ .data/ .bss/heap/共享库)(不贡献栈)
1.5 线程非共享资源
1、线程 ID
2、处理器现场和栈指针(内核栈)
3、独立的栈空间(用户空间栈)
4、errno 变量
5、信号屏蔽字
6、调度优先级
1.6 线程优、缺点
1、优点
提高程序并发性,开销小,数据通信、共享数据方便
2、缺点
库函数、不稳定,调试、编写困难、GDB不支持,对信号支持不好
优点相对凸出,缺点不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大
二、进程控制原语
2.1 pthread_self 函数
1、作用
获取线程 ID。其作用对应进程中 getpid() 函数
2、pthread_t pthread self(void);
返回值:成功:0;失败:无
3、线程 ID
pthread_t 类型,本质:在Linux下为无符号整数 (%lu),其他系统中可能是结构体实现
线程 ID 是进程内部 识别标志。(两个进程间,线程 ID 允许相同)
4、注意
不应使用全局变量 pthread_t tid; 在子线程中通过 pthread_create 传出参数来获取线程 ID,而使用 pthread_self
2.2 pthread_create 函数
1、作用
创建一个新线程 其作用,对应进程中 fork() 函数
2、int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
返回值:成功:0; 失败:错误号 —— Linux 环境下,所有线程特点,失败均直接返回错误号
参数:
参数1:传出参数,保存系统为我们分配好的线程 ID
参数2:通常传 NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数
参数3:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束
参数4:线程主函数执行期间所使用的参数
2.3 测试代码
1、查看父进程的线程 id
2、创建线程,主线程执行 main 函数,创建的线程执行 tfn
3、循环创建多个子线程
4、错误写法
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
void *tfn(void *arg)
{
int i = *((int *)arg);
sleep(i);
printf("I'm %dth thread: pid = %d, tid = %lu\n", i + 1, getpid(), pthread_self());
return NULL;
}
int main(int argc, char *argv[])
{
int i;
int ret;
pthread_t tid;
for (int i = 0; i < 5; i++) {
ret = pthread_create(&tid, NULL, tfn, (void*)&i);
if (ret != 0) {
perror("pthread_create error");
}
}
sleep(5);
printf("I'm main thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
return 0;
}
原因:地址传递和值传递的效果不一样,线程传参要用值传递而不是地址传递。其中要借助两次强制类型转换 int(4字节) —> char(8字节) —> int(4字节)
5、线程间全局变量共享
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
int var = 100;
void sys_err(const char *str)
{
perror(str);
exit(1);
}
void *tfn(void *arg)
{
var = 200;
printf("In thread, change var = %d\n", var);
return NULL;
}
int main(void)
{
printf("At first var = %d\n", var);
pthread_t tid;
pthread_create(&tid, NULL, tfn, NULL);
sleep(1);
printf("After pthread_create var = %d\n", var);
return 0;
}
2.4 pthread_exit
1、作用
将单个线程退出
2、以前代码存在的问题
(1) 我们想退出某个线程,使用 exit 会退出整个进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
void *tfn(void *arg)
{
int i =(int)arg;
sleep(i);
if (i == 2) {
exit(0);
}
printf("I'm %dth thread: pid = %d, tid = %lu\n", i + 1, getpid(), pthread_self());
return NULL;
}
int main(void)
{
int i;
int ret;
pthread_t tid;
for (i = 0; i < 5; i++) {
ret = pthread_create(&tid, NULL, tfn, (void *)i);
if (ret != 0) {
sys_err("pthread_create error");
}
}
sleep(5);
return 0;
}
(2) return 能达到退出线程的目的
但是在函数调用内使用 return 是达不到这样的效果的
3、void pthread_exit(void *retval);
参数:retval 表示线程退出状态,通常传 NULL
在不添加 sleep 控制输出顺序的情况下。pthread_create 在循环中,几乎瞬间创建 5 个线程,但只有第 1 个线程有机会输出(或者第 2 个也有,也可能没有,取决于内核调度)如果第 3 个线程执行了 exit,将整个进程退出了,所以全部线程退出了
所以,多线程环境中,应尽量少用,或者不使用 exit 函数,取而代之使用 pthread_exit 函数,将单个线程退出。任何线程里 exit 导致进程退出,其他线程未工作结束,主控线程退出时不能 return 或 exit
另注意,pthread_exit 或者 return 返回的指针所指向的内存单元必须是全局的或者是用 malloc 分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了
4、代码
现在主线程不用睡眠了,只退出主线程就可以了,子线程不受影响
5、总结
在主函数里面执行 return 和 exit 效果是一样的
return:回到调用者那里去
exit:退出当前进程
pthread_exit():退出当前进程
2.5 pthread_join函数
1、作用
阻塞等待线程退出,获取线程退出状态其作用, 对应进程中 waitpid() 函数
2、int pthread_join(pthread_t thread, void **retval);
成功:0;失败:错误号
参数:thread:线程 ID([注意]:不是指针);retval:存储线程结束状态
和 wait 类比,wait 退出值是 int 所以传出参数是 int *,线程返回值是 void * 所以传出参数是 void **
3、代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
struct thrd {
int var;
char str[256];
};
void sys_err(const char *str)
{
perror(str);
exit(1);
}
void *tfn(void *arg)
{
struct thrd *tval;
tval = malloc(sizeof(tval));
tval->var = 100;
strcpy(tval->str, "hello thread");
return (void*)tval;
}
int main(void)
{
pthread_t tid;
struct thrd *retval;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if (ret != 0) {
sys_err("pthread_create error");
}
ret = pthread_join(tid, (void **)&retval);
if (ret != 0) {
sys_err("phread_join error");
}
printf("child thread exit with var = %d, str = %s\n", retval->var, retval->str);
pthread_exit(NULL); // 将当前线程退出
}
2.6 pthread_cancel函数
1、作用
杀死(取消)线程。 其作用,对应进程中 kil() 函数
2、int pthread_cancel(pthread_t thread);
成功:0;失败:错误号
[注意] 线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)
类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。杀死线程也不是立刻就能完成,必须要到达取消点
取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用 creat,open,pause,close,read,write .... 执行命令 man 7 pthreads 可以查看具备这些取消点的系统调用列表。也可参阅 APUE.12.7 取消选项小节
可粗略认为一个系统调用(进入内核)即为一个取消点。如线程中没有取消点,可以通过调pthread_testcancel 函数自行设置一个取消点
被取消的线程,退出值定义在 Linux 的 pthread 库中。常数 PTHREAD_CANCELED 的值是 -1 可在头文件 pthread.h 中找到它的定义:#define PTHREAD_CANCELED(void *) -1)。因此当我们对一个已经被取消的线程使用 pthread_join 回收时,得到的返回值为 -1
3、代码
(1) 杀死子进程演示
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
void *tfn(void *arg)
{
while (1) {
printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
sleep(1);
}
return NULL;
}
int main(void)
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if (ret != 0) {
sys_err("pthread_create error");
}
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
sleep(5);
ret = pthread_cancel(tid);
if (ret != 0) {
sys_err("phread_join error");
}
while(1);
pthread_exit(NULL);
}
(2) 测试被杀死的子进程的返回值
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
void *tfn1(void *arg)
{
printf("thread 1 returning\n");
return (void *)111;
}
void *tfn2(void *arg)
{
printf("thread 2 exiting\n");
pthread_exit((void *)222);
}
void *tfn3(void *arg)
{
while(1) {
printf("thread 3 : I'm going to die in 3 seconds ...\n");
sleep(1);
}
return (void *)111;
}
int main(void)
{
pthread_t tid;
void *tret = NULL;
pthread_create(&tid, NULL, tfn1, NULL);
pthread_join(tid, &tret);
printf("thread 1 exit code = %d\n\n", (int)tret);
pthread_create(&tid, NULL, tfn2, NULL);
pthread_join(tid, &tret);
printf("thread 2 exit code = %d\n\n", (int)tret);
pthread_create(&tid, NULL, tfn3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &tret);
printf("thread 3 exit code = %d\n", (int)tret);
return 0;
}
(3) 杀死一个进程需要一个契机(进入内核)
现在处于死循环进入不了内核
需要有系统调用或者手动添加一个取消点
2.7 pthread_detach 函数
1、作用
实现线程分离
2、int pthread_detach(pthread_t thread);
成功:0;失败:错误号
3、线程分离状态
指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用
4、进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被放,一点残留资源仍存于系统中,导致内核认为该进程仍存在
也可使用 pthread_create 函数参 2(线程属性)来设置线程分离
5、代码
(1) 以前查看错误的方法
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void *tfn(void *arg)
{
printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
return NULL;
}
int main(void)
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if (ret != 0) {
perror("pthread_create error");
}
ret = pthread_detach(tid);
if (ret != 0) {
perror("phread_join error");
}
sleep(1);
ret = pthread_join(tid, NULL);
printf("join ret = %d\n", ret);
if (ret != 0) {
perror("pthread_join error");
}
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
pthread_exit(NULL);
}
很明显没看出有啥错误,但是报错了
(2) 现在查看错误的方式
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void *tfn(void *arg)
{
printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
return NULL;
}
int main(void)
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if (ret != 0) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(1);
}
ret = pthread_detach(tid); // 设置线程分离 子线程终止,会自动清理PCB,无需回收
if (ret != 0) {
fprintf(stderr, "pthread_detach error: %s\n", strerror(ret));
exit(1);
}
sleep(1);
ret = pthread_join(tid, NULL);
printf("join ret = %d\n", ret);
if (ret != 0) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(1);
}
printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
pthread_exit((void*)0);
}
错误原因是无效的参数,
错误原因:子线程分离了,线程自己把所有的东西都回收了,再用main线程回收就错误了。
2.8 进程和线程控制原语比对
三、线程属性
3.1 简介
本节作为指引性介绍,linux 下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数
typedef struct
{
int etachstate; // 线程的分离状态
int schedpolicy; // 线程调度策略
struct sched_param schedparam; // 线程的调度参数。
int inheritsched; // 线程的继承性
int scope; // 线程的作用域
size_t guardsize; // 线程栈末尾的警戒缓冲区大小
int stackaddr_set; // 线程的栈设置
void* stackaddr; // 线程栈的位置。
size_t stacksize; // 线程栈的大小。
} pthread_attr_t;
3.2 流程
设置分离属性
pthread_attr_t attr
创建一个线程属性结构体变量
pthread_attr_init(&attr); 初始化线程属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 设置线程属性为分离态
pthread_create(&tid, attr, tfn, NULL); 借助修改后的设置线程属性创建为分离态的新线程
pthread_attr_init(&attr); 销毁线程属性
3.3 代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void *tfn(void *arg)
{
printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
return NULL;
}
int main(void)
{
pthread_t tid;
pthread_attr_t attr;
int ret = pthread_attr_init(&attr);
if (ret != 0) {
fprintf(stderr, "attr_init error: %s\n", strerror(ret));
exit(1);
}
ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (ret != 0) {
fprintf(stderr, "pthread_setdetachstate error: %s\n", strerror(ret));
exit(1);
}
ret = pthread_create(&tid, &attr, tfn, NULL);
if (ret != 0) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(0);
}
sleep(1);
ret = pthread_attr_destroy(&attr);
if (ret != 0) {
fprintf(stderr, "attr_destroy error: %s\n", strerror(ret));
exit(1);
}
ret = pthread_join(tid, NULL);
if (ret != 0) {
fprintf(stderr, "pthread_jerror error: %s\n", strerror(ret));
exit(1);
}
// printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
pthread_exit((void*)0);
}
四、注意事项
1、主线程退出其他线程不退出,主线程应调用 pthread_exit
2、避免僵尸线程
pthread_join
pthread_detachs
pthread_create 指定分离属性
被 join 线程可能在 join 函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;
3、malloc 和 mmap 申请的内存可以被其他线程释放
4、应避免在多线程模型中调用 fork 除非,马上 exec,子进程中只有调用 fork 的线程存在,其他线程在子进程中均 pthread_exit
5、信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制