linux系统编程笔记—线程

线程

线程中出错不能像进程中那样直接perror
要用fprintf(stderr,"xxxxx",strerror(ret))

进程与线程不同

进程:有独立的pcb。有独立的 进程地址空间。 分配资源的最小单位(有几个线程就分配几个进程地址空间)
线程:有独立的pcb。没有独立的进程地址空间(因为与创建线程的进程共享三级页表,所以映射在物理地址上是一样的)。 最小的执行单位

进程号跟线程号
**ps -Lf **查看进程号 ,里面可以看到线程LWP
image.png

  1. 线程id是在【进程】地址空间内部,用来标识线程身份的id号 。
  2. 而lwp是线程号,cpu根据线程号来划时

线程特点

  1. 轻量级线程,存在pcb,创建线程使用的底层函数和进程一样,都是clone
  2. 从内核看进程和线程一样,都有各自不同的pcb,但是线程pcb中指向内存资源的三级页表是相同的,所以共享地址内存空间
  3. 进程可以变成线程
  4. 线程可以看成寄存器和栈的集合
  5. 线程是最小的执行单位,进程是最小的资源分配的单位

cpu时钟中断,所以把进程划分成很多个线程能增大占用cpu几率。但是分太多后占有率反而会低。

三级映射

当a.out程序运行,系统为它创建一个进程,有了进程地址空间,如果在该进程中调用函数创建线程,那么线程就来共享进程地址空间,但是新建的线程有自己独立的PCB。
image.png
当在进程里面创建了线程。 线程的PCB直接在进程的内存地址空间里
image.png

从内核看,线程跟进程是一样的,不同的个体都有各自不同的PCB,但是线程PCB中指向内存资源的三级页表是一样的。
对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟地址,映射到不同的物理页面内存单元,最终访问不同的物理页面。
但!线程不同!虽然两个线程具有各自独立的PCB,但共享同一个三级页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。

进程与子进程的三级页表不一样。虽然PCB独立
线程之间的三级页表一样。虽然PCB独立

之前在讲MMU映射的时候说虚拟内存空间通过MMU直接映射到物理地址,其实中间是PCB->三级页表->MMU的过程
image.png
image.png

线程共享、非共享资源

下面两个常见:
独享 栈空间(内核栈、用户栈)
共享 ./text./data ./rodataa ./bsss heap —> 共享【全局变量】/堆区

共享:
1.文件描述符表
2.每种信号的处理方式
3.当前工作目录
4.用户ID和组ID
5.内存地址空间 (.text/.data/.bss/heap/共享库)
非共享:
1.线程id
2.处理器现场(寄存器)和栈指针(内核栈)
3.独立的栈空间(用户空间栈) (因为线程就是寄存器和栈的集合,当然不能共享)
4.errno变量
5.信号屏蔽字
6.调度优先级

线程优、缺点

优点: 1. 提高程序并发性 2. 开销小 3. 数据通信、共享数据方便
缺点: 1. 库函数,不稳定 2. 调试、编写困难、gdb不支持 3. 对信号支持不好
优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。

创建线程

pthread_self函数
pthread_t pthread_self(); 头文件#include <pthread.h>
获取线程id。 线程id是在【进程】地址空间内部,用来标识线程身份的id号 。
返回值:本线程id
image.png

而lwp是线程号,cpu根据线程号来划时 ps -Lf 进程号

pthread_create函数
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_rountn)(void *), void *arg);

main中的叫主线程。主子线程同内存地址空间,
创建子线程,子线程去执行回调函数

参数:
参1 tid:传出参数,表新创建的子线程 id
参2 ttr:线程属性。NULL,表使用默认属性。常用NULL
参3 start_rountn:子线程回调函数,传入函数名即可。创建成功,ptherad_create函数返回时,该函数会被自动调用。
参4 arg:回调函数的参数。没有的话,传NULL。 要传void*类型的指针进去,所以有时候需要强转,并且必须是值传递!不能(void*)&i,必须要用(void*)i。在下一节笔记我会记录。
返回值:
成功:0
失败:errno。

注意:

  1. pthread_t 在大多数系统上被定义为无符号长整型,因此tid使用 %lu 是一种较好,%lu 是用于无符号长整型(unsigned long int)的格式说明符
  2. 编译的时候需要加上 -pthread
  3. 主线程结束,子线程还会继续执行。全局变量子线程还能继续使用
  4. 参数3必须值传递

线程中检查出错返回:
fprintf(stderr, “xxx error: %s\n”, strerror(ret));

循环创建多个线程/参数3解析

上一届参数3的解析: 下面展示的是地址传递,可以看到输出是错误的。为什么会错?为何一定要值传递?image.pngimage.png

线程的栈是独立的,如果地址传递,那参数就是(void*)&i,主线程i的地址0xaa传到子线程,那么子线程就会根据这个地址来主线程中取i的值。但是!主线程的i在for循环(原语逻辑)中是变化的!而子线程需要调用clone系统调用,而栈是用户空间,用户空间到栈空间需要MMU进行权级切换,耗时很长,所以子线程相比主线程慢了,等子线程取i的值,主线程的i已经变化了!导致取到的i不是那一时刻的i
解决方法:值传递, (void*)i作为参数3,在子线程中强转回int类型:(int)i 。虽然会提示错误信息(gcc时添加-w去掉警告就可以了),但是可以用,因为i是4字节的int类型,指针在32位下4字节,64位下8字节,转换过去不会丢失数据。
image.png
image.png

线程间全局变量共享

子线程里更改全局变量后,主线程里也跟着发生变化。

pthread_exit退出

exit(0)退出进程(把整个进程关了,进程里面的线程也没了),子线程中如果用了,其他子线程也就都没了。
image.png
比如上个Demo,如果我在里面i =2,也就是第三个子线程中加exit(0),那么全没了。return 返回到调用者处,也不能用来退出。如果在主线程的末尾+return 0也就是在main()函数末尾return 0,那么会返回调用处,就退出了进程,从而导致所有子线程退出。

pthread_exit函数
将当前线程退出,不影响其他线程正常运行。
image.png
参数 retval:退出值。 无退出值时,NULL

区分 exit(); 退出当前进程。
return: 返回到调用者那里去。
pthread_exit(): 退出当前线程。

pthread_join 回收线程

pthread_join函数
阻塞 回收线程
int pthread_join(pthread_t thread, void **retval);
参数 thread: 待回收的线程id
retval:传出参数是指向指针的指针。 回收的那个线程的退出值。
线程异常借助,值为 -1。
返回值:成功:0
失败:errno
pthread_join(tid, 参数2),比如下面这个例子。tfn返回类型是void*的74,把74当指针,值传递传出来,所以我们定义一个int* retval接住,意思74 = retval。但是参数2是指针的指针类型,所以再取指针retval的地址就可以了
void(**)&retval
image.png
用的时候,强转为跟传出的时候类型一直就是74了

如果结构体中,再tfn中这样直接定义结构体是错误的,这样开辟在子线程的栈区,子进程结束了,栈区没了pthread_join(参1,参2)返回的参数2也访问不到了。
image.png

pthread_cancel 杀死线程

image.png
线程的杀死并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。如果没有取消点则不会杀死。
可粗略认为一个系统调用(进入内核)即为一个取消点。如线程中没有取消点,可以通过调用pthr·ead_testcancel()函数自行设置一个取消点。
成功杀死的线程,用pthread_join()的参数2可以回收到值为-1

pthread_detach线程分离(自动回收子线程)

pthread_detach函数
设置线程与主线程分离,分离的线程结束后自动清理PCB无需回收
int pthread_detach(pthread_t thread);
参数 thread: 待分离的线程id
返回值: 成功:0
测试有没有分离,可以在分离后用pthread_join(),判断参数2是否为-1,为-1说明分离了,自动回收了,所以join没回收到。

进程、线程 控制原语对比

image.png

线程属性设置分离属性

如果循环创建多个线程,并且想用detach来自动回收,如果一个一个设置分离太麻烦了。有一个方法是创建线程的时候,修改线程结构体属性。即修改之前pthread_createde的参数2。
pthread_attr_函数

pthread_attr_t attr;创建一个线程属性结构体

pthread_attr_init(&attr);初始化线程属性

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);设置线程属性为 【分离态】

pthread_create(&tid, &attr, tfn, NULL);借助修改后的 设置线程属性 创建为分离态的新线程

pthread_attr_destroy(&attr); 销毁线程属性变量

线程使用注意事项

  1. 主线程退出其他线程不退出,主线程应该调用pthread_exit
  2. 避免僵尸线程
    1. pthread_join
    2. pthread_detach
    3. pthread_create指定分离属性
    4. 被join线程可能在join函数返回前就释放自己的所有内存资源,所以不应当返回被回收线程栈中的值。
  3. malloc和mmap申请的内存(在堆区,线程间共享堆区)可以被其他线程释放
  4. 应避免在多线程中调用fork,除非立马exec。在进程A的某个子线程1中调用fork,子线程中只有调用fork的线程1存在,其他线程在子进程中均pthread_exit没了。
  5. 信号的复杂语义很难和多线程共存,在多线程中避免使用信号机制。
  • 15
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值