多线程编程基本概念

基本介绍

在操作系统中进程是进行资源分配和调度的基本单位,也是线程的容器,而线程也可以进一步划分作为协程的容器,比如说 Go 语言里的 goroutine。三者的结构大致如下:
在这里插入图片描述
每个进程都是一个实体,有着自己的地址空间,代表一个执行中的程序;线程则是进程中的一个单一顺序的控制流,是处理器调度的基本单位,每个进程中至少有一个线程,它们共享进程的地址空间和各项资源;协程本质上是一种用户态线程,不需要操作系统调度,它的实现是寄存在线程当中,因此开销非常小,一个进程里可以存在着数十倍与线程数量的协程,协程与线程不同的地方在于控制权不在操作系统而由用户自己掌控,什么时候从一个协程切换到另一个协程完全由用户自己决定。
这种技术作为解决单核转多核 CPU 性能提升的一种解决方案,作用在于让一个多核心的处理器甚至多个这样的处理器高效、正确的工作,如何将串行程序改造成并行,已经成为了一门学问。在正式了解多线程编程之前还需要了解下面几个概念:

同步、异步

同步和异步通常用来形容一次方法调用,同步方法开始调用后必须要等待方法执行完毕返回后,才能继续后续行为。异步方法更像一个消息传递,一旦开始调用就会立即返回,调用者可以继续后续的操作,而异步方法通常会在另一个线程中真实的执行,整个过程不会阻碍到调用者的工作,如果需要返回异步调用的结果,那么在调用真实完成时会通知调用者。
在这里插入图片描述
如图,假设执行每个方法需要 1 秒钟,同步调用这边的方法执行完成后总共需要的时间是 5 秒,异步调用这边由于在执行方法一和方法三的时候分出了两个线程分别执行方法二和方法四,也就是说执行方法一的时候同时也在执行方法二,执行方法三的时候同时也在执行方法四,最后方法执行完成后总共用掉的时间就只有主线程执行的方法一+方法三+方法五= 3 秒钟。

串行、并发、并行

串行非常简单,就是有多个任务,一个接一个的按顺序执行,一个完成了才进行下一个任务。
并发和并行是两个很容易混淆的概念,都能表示为多个任务一起执行,但是偏重点不同。

  1. 并发偏向于多个任务交替执行,多个任务间总体来看还是串行的;
  2. 并行是真正意义上的分开并同时执行;
    在这里插入图片描述

临界区

用来表示一种共享资源,能被多个线程使用,但是每次只能有一个线程去使用它,一旦临界区资源被占用,其他线程想使用这个资源就必须等待直至解除占用。在并行程序中,临界区资源是受保护对象,如果意外出现两个线程同时占用,就会造成错误,执行结果都不是两个线程所要的。

阻塞、非阻塞

阻塞和非阻塞通常用来形容线程之间的相互影响,比如一个线程占用临界区资源另一个线程在等待的情况,这时候在等待的线程就会是挂起状态,这就是阻塞,如果占用一直没有释放资源,那么其他所有阻塞在这个临界区的线程都不能继续工作。
非阻塞的意思刚好相反,强调没有一个线程能妨碍其他线程执行,所有线程都会不断的尝试继续执行。

死锁、饥饿、活锁

这三个属于线程活跃性的问题,如果发生这三种情况,那么相关线程就很难继续执行下去了。
死锁:这是最坏的一种情况,指多个线程运行中互相持有对方所需要的资源,导致这些线程一直处于阻塞状态,无法继续执行,产生死锁需要这4个条件:

  1. 临界区资源;
  2. 当线程因请求资源阻塞时,对已获得资源保持占用;
  3. 线程在已获得资源未使用完前占用权不能被剥夺,只能够主动释放;
  4. 发生死锁时,必然存在着一个线程——资源的环路;

饥饿:指某个或多个线程因为各种原因无法获得所需资源,导致无法执行,比如线程优先级太低,不断有高优先级的线程在抢占它需要的资源,导致低优先级线程无法工作。也可能是某个线程一直占用着资源,导致其他需要这个资源的线程无法正常执行。这个情况和死锁相比不同的地方在于不会出现互相占用对方需要的资源并且不主动解除占用的情况。
活锁:指的是线程没有被阻塞,由于一些条件没有被满足,导致一直重复尝试,然后失败,再尝试…,就好像两辆车过一个窄桥,这一头在主动谦让,另一头也在主动等待对面先过,导致两辆车都无法通过窄桥,这种事发生在线程之间,会出现资源不断在两个线程中跳动,而没有一个线程能够同时拿到所有资源正常执行,这就是活锁,和其他两种情况的区别在于活锁是活动的状态而饥饿和死锁是阻塞等待的状态,因此活锁有机会能自行解开。

并发级别

这是在多线程之间对于临界区访问的一种控制策略,根据策略我们可以把并发的级别分为阻塞、无饥饿、无障碍、无锁、无等待这 5 种。
阻塞
一个线程是阻塞的,那么在其他线程释放资源前当前线程无法继续执行,在重入锁的时候,获得的就是一个阻塞的线程,每当试图执行后续代码前,都会去设法得到临界区的锁,如果获取失败,线程就会被挂起等待,直到占用成功为止。
无饥饿
如果线程之间有优先级,那么线程调度时hi先满足高优先级的线程。也就是说,在多个线程排队等待使用临界区资源的情况下,系统允许高优先级的线程插队,这样就可能会导致线程进入饥饿状态,如果资源锁是公平的,不管最近一次请求的线程优先级多高,想获取资源就要乖乖排队,那么所有线程都有机会执行。
在这里插入图片描述
无障碍
无障碍是一种最弱的非阻塞调度。如果两个线程是无障碍的执行,那么他们不会因为临界区的问题导致其中一方被挂起,一旦检测到资源占用冲突,无障碍线程就会立刻对自己所做的修改进行回滚,确保数据安全,如果没有发生数据竞争,那么线程就能顺利完成自己的工作。
但是这种策略不一定能顺畅运行,因为当临界区中一直有冲突时所有无障碍线程都会不断的回滚自己的操作,没有一个能完成工作。于是就有了一种解决方案:给线程加上一个一致性的标记,线程在操作前先读取并保存这个标记,操作完成后再次读取,检查标记是否被修改,如果是一样的,说明资源访问没有冲突,如果被更改过,说明资源可能在操作时与其他在写的线程冲突,需要重试操作。每次修改数据前,都需要更新一次标记,表示数据不再安全。
在这里插入图片描述
无锁
无锁的并行都是无障碍的。在这种情况下所有线程都会试着去访问临界区,不同的是无锁的并发保证一定有一个线程能完成操作并解除占用。在无锁进程的工作中,有个特点是会在一个循环中,线程不断尝试修改共享资源。如果没有冲突,修改成功,程序退出,否则继续尝试,直到成功完成修改操作。
无等待
无等待在无锁的基础上进一步的扩展,从要求一个线程完成操作到要求所有线程都必须在一定步骤内完成,这样就不会出现饥饿问题。有一种无等待结构就是 RCU,它的思想是不去控制对数据的读取操作。但是在写数据的时候,先读取原始数据副本,然后修改这个副本,修改完成后在合适的时机将改动写入到共享资源中。

锁的优化

锁的竞争必定会导致程序性能下降,为了把这种副作用降到最低,有下面几种措施:
减小锁持有时间
对于用锁进行并发控制的程序来说,单个线程对锁的持有时间与性能有着直接的关系,持有锁的时间越长,竞争程度也就越激烈。如果减少线程持有锁的时间,那么线程之间互斥的可能性就会大大降低。
减小锁的粒度
这个方法也是一种减少线程锁竞争的有效手段,其原理是缩小锁定对象的范围,从而减少冲突的可能性。但这种办法有一个缺点,当系统需要获取全局锁时,消耗的资源会比较多,因为要获取所有子段的锁。
读写分离锁代替独占锁
用读写分离锁替代独占锁是减小锁粒度的一种特殊情况,如果说减少粒度是通过分割数据结构实现的,那么读写锁则是对系统功能点的分割。不像写操作,读取资源的行为并不会对数据造成破坏,因此理论上应该允许多线程同时读取,读写锁实现的就是这种功能。
锁分离
如果把读写锁的思路进一步延伸,就是锁分离。读写锁的原理是根据读写操作功能上的不同进行了有效的锁分离,那么在实际情况中根据程序的功能特点,使用类似的分离思路,也能够对独占锁进行分离,这种方式要求根据业务实际情况来决定,所以更加灵活、细化。
锁粗化
通常为了保证多线程之间有效并发,会要求每个线程持有锁的时间尽量短。但是如果线程对同一个锁不停地进行请求、同步然后释放,这些过程本身也会消耗系统的资源,不利于性能的优化。这个时候就要把所有的锁操作整合成一次对锁的请求,从而减少对锁请求同步次数,达到提升性能的目的。

性能优化其实就是根据运行时的真实情况对各个资源点进行权衡取舍的过程,锁粗化和减少锁持有时间是两种相反的思路,但是在不同场合,某一种思路的优化效果会更好,这些都是要根据实际情况来判断的。

如果有不对的地方,还请各位大佬指出 ^ _ ^


相关文章:
Socket 编程原理

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值