操作系统-原子性与锁机制

让人想犯罪 __ 提交于 2020-03-03 02:39:21
原子性和锁机制

所谓原子性和原子操作即一条或者一系列不可以被中断的指令

原子性的保证:

  1. 单核CPU如何保证指令的原子性?

    • 单核CPU下各个指令都是串行的,中断只会发生在一条指令执行完毕,那么自然每个指令都是原子的,如果想要实现单核CPU下多个指令的的原子操作,则可以通过关中断实现
  2. 多核CPU如何保证指令的原子性?

    • 多核CPU下由于出现并发问题,所以多个核心可能同时读写同一个内存,所以需要通过一些机制来实现原子操作

    • 多核CPU确保一条指令的原子性,例如递减指令,实际上是三个操作,先读取内存,递减,写回,但是由于是多核CPU,所以即便是一条指令,也无法保证原子性,所以多核总线是通过总线锁来实现多核CPU的原子性的,即在指令执行前先通过总线锁锁住CPU和内存的通信(CPU1向总线发出LOCK#信号,其他处理器便不可以再访问该共享的内存)

    • 总线锁会锁住CPU和内存的通信,开销大(此时并行化的操作变成了串行化的操作,但是),所以可以使用缓存锁,并通过缓存一致性机制来保证原子操作,缓存一致性即MESI协议,介绍如下:

      • 每个CPU有自己的高速缓存,高速缓存以缓存行的形式存在,而MESI就是给每个缓存行保存一个标志位,标志位如下:
      • M:被修改的,当前缓存行中的数据相对内存而言已经被修改了,但是还没有更新到内存中,同时处于该状态的数据只有当前CPU缓存中有,而其他CPU缓存中没有
      • E:独占的,缓存行中的数据没有被修改,但是数据被当前CPU独占
      • S:共享的,缓存行中的数据被多个CPU共享
      • I:无效的,当前缓存行中的数据已经无效
      • 一个处于M状态的缓存行,要时刻监听所有视图读取该缓存行对应内存的CPU,如果监听到,则必须在其他CPU读取前,先将缓存行中的数据写入;一个处于S状态的缓存行,必须时刻监听其他试图修改该缓存数据或者独占该缓存数据的请求,如果监听到,则将S状态置为I;一个处于E状态的缓冲行必须时刻监听其他试图读取该数据内存地址的CPU,如果监听到,则将E置为S
      • 有了上述的准备过程后,CPU对数据的读写过程如下:
        • 读:如果缓存行中的数据是I的,则需要从内存中读取,但是要先等其他拥有此数据的处于M状态的CPU将数据写回内存,如果是S的,则可以直接使用缓存行中的数据
        • 写:如果缓存行中的数据是M/E,则CPU可以直接写,如果缓存行中的数据是S的,则要通过总线事务通知其他CPU将缓存置为无效I,这种情况下开销大,之后CPU可以向缓存行中写,写完后将缓存行置为M(这里的读写都是针对CPU对高速缓存的读写)
        • https://blog.csdn.net/martin_ke/article/details/88851393
        • https://www.cnblogs.com/yanlong300/p/8986041.html
    • CAS

      • CAS可以用来实现乐观锁,当需要修改某个变量时,先将预期的旧的值和内存中值进行比较,如果相同则写入新的值,如果不是相同的值则写入错误,不会继续写入。可以通过CAS来实现自旋锁

      • CAS,即比较交换,其底层实现是基于cmpxchg原子指令,该指令即将当前内存中的值和旧的预期值进行比较,如果相等则把需要写入值写入到内存中

      • CAS可能出现ABA问题,所以往往给内存中的值还会加上一个版本号,在上述条件的基础上,还要保证版本号的值相同才可以

    • test and set

所谓锁,就是当多线程并发访问同一个资源时,用于保证数据一致性的一种机制,当给资源加锁时,只有一个线程能够访问资源,其他线程将阻塞

锁的底层实现:

对于一个锁而言,其实现无非就是一个flag位,例如flag为1表示锁定,flag为0表示未锁定,加锁即可,整个过程即为先读取flag的值,如果是0则flag置为1,否则线程阻塞,但是读取flag是一个原子操作,同时flag置1也是一个原子操作,但是这个两个操作合起来则不是一个原子操作,需要通过一定的机制来保证该操作的原子性。

那么锁的底层实现究竟如何呢?

实现加锁的关键还是读取锁和置锁的过程必须是原子的,所以

  1. 对于单核CPU而言,最好的方法就是关中断,这样可以保证在读取锁后不会发送中断导致其他进程/线程的调度。
  2. 对于多核CPU而言,常见的方法是通过TSL指令(关中断对于多核CPU没有意义),这是一条无法分割的原子指令,所谓TSL指令即读取锁的值,将值读取到寄存器中,然后给LOCK设置一个非0的值,这个过程是原子的,那么TSL指令的实现则是总线锁LOCK来实现(单处理器通过关中断实现,多处理器通过总线锁实现)。
  3. 除了TSL指令外,XCHG指令也可以实现原子加锁,XCHG是一个原子指令其主要作用是两个寄存器或者寄存器和内存变量之间的内容进行交换的指令,实现加锁的方法如下,先将寄存器值置为1,然后将寄存器和内存交换,如果寄存器值为0,则表示成功加锁,如果寄存器值为1则表示已经被加锁,加锁失败。
线程安全

所谓线程安全,即采用了加锁机制,对某个共享数据进行互斥的访问,当某个线程获取锁后才能访问该变量,而其他变量则是不能访问的,当该线程释放了锁,则其他变量再获取锁从而继续访问,这样就不会出现数据不一致的问题,避免了脏数据(多个线程不加锁的修改某个数据后得到数据)的出现

为什么会有线程安全问题?

​ 因为当多个线程共享同一个变量时(全局变量,静态变量,堆内存),可能出现数据不一致的情况,例如都做写操作时,或者一个写一个读时,但是如果都是读则不会出现线程安全问题

怎么保证线程安全?

​ 给共享的变量加锁,即一次只允许一个线程去访问,访问完后释放锁,其他变量继续访问

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!