Linux——线程2|线程控制

什么是线程

  1. 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。

  1. 一切进程至少都有一个执行线程

  1. 线程在进程内部运行,本质是在进程地址空间内运行

  1. 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化

  1. 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

线程的优点

  1. 创建一个新线程的代价要比创建一个新进程小得多

  1. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

  1. 线程占用的资源要比进程少很多

  1. 能充分利用多处理器的可并行数量

  1. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务

  1. 计算密集型(大部分时间线程都在做计算)应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现

  1. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

线程的缺点

  1. 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

  1. 健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

  1. 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

  1. 编程难度提高:编写与调试一个多线程程序比单线程程序困难得多

线程异常

单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃

线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该

进程内的所有线程也就随即退出

线程用途

合理的使用多线程,能提高CPU密集型程序的执行效率

合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是

多线程运行的一种表现)

进程和线程的关系如下图:

fork底层调用以clone为主,clone是创建一个轻量级进程,第二个参数是子进程的栈,clone可以设置是否共享地址空间

还有vfork,这也是创建子进程,这个创建的子进程和父进程共享地址空间

当主线程和新线程创建成功后,谁先运行,这个没有标准答案,由调度器决定谁先运行。

让新线程除0,1s后我们发现进程直接退出。因此线程一旦异常,可能会导致整个进程直接退出。

线程在创建并执行的时候,也是需要进行等待的,如果主线程不等待,即会引起类似于进程的将是问题,进而导致内存泄漏。

用pthread_join去进行等待

第一个参数是线程id,第二个后面说。返回值成功0,失败返回错误码。

新线程退出后,主线程等待成功并回收相关资源

进行资源监视,由于我们的程序没有让主线程完成等待后执行其它任务,这里在等待完成后,直接执行到了return 0,但我们可以确定的是子线程被进行了回收。

while :; do ps -aL | head -1 && ps -aL |grep mythread; sleep 1; done

对于创建线程时的传参问题:我们可以像上面程序一样按照类似于 (void*)"thread 1",这个参数是给第二个函数当实参的。对于第二个函数的返回值,如果我们想返回某个值,我们可这样进行强制类型转换。也就是说我们把10当作了一个指针数据,也就是说有一个地址,地址数据是10。

但这里的返回值又是返回给谁呢?一般是给主线程,谁等待给谁。

那主线程又如何获取到这个数据呢?我们这里通过pthread_join进行获取。

pthread_join第二个参数

主线程返回值是void *,所以我们这里要用void * *接收

我们提取传进来的数据,这里我们用ret接收,ret是指针变量大小是void *,void类型大小不明确,但是void *有大小,这里是8个字节。指针就是地址,地址就是一个数据。整整型变量:1.要开辟空间2.这个空间里保存的都是整型,这里ret是一个指针变量,可以去盛装对应的数据,我们把10强转为void*,此时(void*)10就是一个指针数据,ret就是指针对应的空间,有了空间我们就能进行取地址操作。

当我们最后把这个10拿到的时候,由于ret有空间,所以这个10直接保存在ret里面

我们把ret强转成整数,并打印

运行程序,此时报错,这份代码如果在32位机器下可以直接跑过,在64位就跑不过了,因为int是4字节的,而ret在这里是8字节,因此我们强转位long long类型

修改后

此时我们拿到了10

新线程不仅仅可以返回数字,还可以这样操作,若我们想把下面新线程中i的计算结果交给主线程

注意我们这里的ret是int*,在函数当实参的时候转换成了void **

我们看到新线程退出后,主线程拿到了遍历的结果。

终止线程

exit和pthread_exit

从新线程里进行return能终止线程。

我们在新线程中加入exit进行测试。

我们发现新线程退出后,主线程里的消息没有被打印,主线程也跟着直接退出。因此我们在多线程当中不要用exit,exit是终止整个进程的

pthread_exit是线程终止,让线程退出。这个参数也是void*,返回值是void*

此时打印了主线程的语句

pthread_cancel

pthread_cancel线程取消,如果要取消某个线程,发送一个取消请求给目标线程,参数是线程id

我们让新线程陷入死循环,主线程count=5时,退出循环,然后将线程取消,主线程由于有pthread_join所以会等待子线程退出后,继续执行剩下的代码。

我们可以看到线程id特别大,

我们让主线程最后sleep5s

当新线程取消后,主线程执行完自己的代码sleep 5s,我们可以看到这5s期间只有主线程。

对于上面的现象我们可以得出结论:

  1. 一旦线程被取消,join的时候,退出码是-1,-1其实是PTHREAD_CANCELD;即一个线程被取消,该线程的退出码会自动被设置为-1,所以我们会提取到-1

当我们创建线程之后,立马取消,线程卡在了这里,而且始终只有一个线程。这是因为pthread_cancel用法不是这么用的

我们在使用pthread_cancel的时候要首先要保证线程是存在的,并且线程已经彻底运行起来了,而且用主线程去取消新线程。

我们也可以用新线程取消主线程,但不推荐这么干,会有很大麻烦,如果主线程被干掉了,新线程没人处理,而且可能会引起其它问题。这种做法没有啥意义。

线程ID

我们打印线程id,tid打出来这么大,这是因为它本质是一个地址,因为我们目前用的不是Linux自带的创建线程的接口,我们用的是pthread库中的接口。

pthread库加载到内存中,然后通过页表映射到地址空间(共享区)当中,我们自己的代码在代码区当中,当我们调用pthread库时,跳转到共享区中去调库函数。

由于线程在运行时,需要自己的独立栈结构,CPU中有ebp和esp,当有多个线程执行流来回被切换时,如果使用同一个栈,我们压栈,入栈时对应的数据就全部乱起来了,那么如何保证栈区是每一个线程独占的呢?

设计者对于OS的设计思路是不想让OS感知到栈的存在,最多感知到轻量级进程,因此在用户层提供栈,而不是让OS提供栈。

库的设计者提供了给每个线程进行维护的私有数据,包括线程的id,局部存储和线程的栈结构。注意这个栈是由库提供的,在共享区中维护,为了更好的让每个线程找到自己的用户及属性,我们用线程属性集合的起始地址充当线程的tid。

主线程用的是内核级别的栈结构,新线程用的是共享区当中库所提供的栈结构,因此可以保证每个线程都有独立的栈结构。

如何保证,创建线程的时候用共享区的一个地址来充当栈结构?

clone函数,第一个参数是执行方法,第二个参数是轻量级进程在用户层的栈结构(这个栈结构,可以由我们人为指定),其实pthread库底层创建PCB时候调用clone,同时在自己的库内部申请对应的线程相关属性字段,然后把线程的相关地址传入给新线程,新线程在调度的时候,就直接用共享区的栈区。

pthread_self

哪个线程调的pthread_self,就获取当前线程的线程id

我们可以用pthread_cancel和pthread_self组合,让自己取消自己pthread_cancel(pthread_self());但不推荐这种做法,可能会引起一些不必要的麻烦。

证明全局数据被所有线程共享

我们看到新线程和主线程的count是同步变化的,因此可以说明新线程和主线程访问的是同一个全局变量

若想让全局变量称为每个线程内私有的变量我们在变量前面加上__thread

__thread修饰全局变量,带来的结果是让每一个线程各自拥有一个全局的变量,这叫做线程的局部存储。

加了__thread之后,编译的时候给买个线程都拷贝一个g_val。

我们看到主线程打印出来的是0,而新线程的count的值一直在++。

若我们在线程里调用execl()进行程序替换,带来的后果就是将整个代码和数据全部进行替换掉。并且会影响其它线程。

运行结果。

我们先让它sleep5s

进入新线程之后sleep5s,5s后线程全部退出。我们发现线程一旦被替换,整个代码会全部被替换,exec系列是进程替换。

如果在线程里调fork,我们子进程会拷贝主线程PCB。

线程分离

线程在终止时,不想再被等待,我们就可直接进行线程分离。

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。

如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

主线程可以分离新线程,但是一般都采用线程自己分离自己。

用pthread_deatch进行线程分离

程序会打印错误信息,这是因为我们把线程分离后,无法join,错误原因是非法参数,线程分离之后就不能等了,因为等不到。

若主线程提前退出,就意味着进程退出。线程也会跟着退出。无论是多进程还是多线程,父进程或主线程永远是最后退出。

我们让子线程分离,之后让子线程运行错误程序,整个进程直接结束。因此线程分离,若子线程异常,整个进程都会退出。

C++中有自己对应的线程。我们注释掉makefile里面的-lpthread

程序运行后会对pthread_create进行报错,报错信息为没有该函数。

我们再在makefile里面加上-lpthread

此时C++的线程跑了起来

此时我们看到用到了pthread库

因此我们可以看出,语言级别的线程库底层使用的是原生线程库。

Linux线程互斥

临界资源:多线程执行流共享的资源就叫做临界资源。

临界区:每个线程内部,访问临界资源的代码,就叫做临界区。

互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用

原子性(后面讨论如何实现):会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

观察下面现象:

如果多线程访问同一个全局变量,并对它进行数据计算,多线程会互相影响吗?

我们写了一个多线程抢票程序,这里tickets就是临界资源

我们看到打印了俩次0,就说明多卖了票。

这个抢票为什么会出错?

这是因为CPU执行--操作时,该线程可能会被其它线程挤下去,然后该线程带走自己的上下文数据,其它线程带着自己的上下文数据对tickets进行修改,修改后第一个线程恢复上来,tickets又变了。因此,tickets在并发访问的时候,导致了我们数据不一致的问题。

当进行数据运算时:1.读取数据到cpu内的寄存器中,2.cpu内部进行计算--3.将结果写回内存中

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

头发没有代码多

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值