并发--无阻塞、无障碍、无锁、无等待

一般认为并发可以分为阻塞与非阻塞,对于非阻塞可以进一步细分为无障碍、无锁、无等待,下面就对这几个并发级别,作一些简单的介绍。
在这里插入图片描述
并发级别

1、阻塞
阻塞是指一个线程进入临界区后,其它线程就必须在临界区外等待,待进去的线程执行完任务离开临界区后,其它线程才能再进去。

2、无障碍(obstruction-free)
无障碍是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界区的问题导致一方被挂起。换言之,大家都进入临界区了。那么如果一起修改共享数据,把数据改坏了可怎么办呢?对于无障碍的线程来说,一旦检测到这种情况,它就会立即对自己所做的修改进行回滚,确保数据安全。从这个策略中也可以看到,当临界区中存在严重的冲突时,所有的线程可能都会不断地回滚自己的操作,而没有一个线程可以走出临界区。这种情况会影响系统的正常执行。
一种可行的无障碍实现可以依赖一个“一致性标记”来实现。线程在操作之前,先读取并保存这个标记,在操作完成后,再次读取,检查这个标记是否被更改过,如果两者是一致的,则说明资源访问没有冲突。如果不一致,则说明资源可能在操作过程中与其他写线程冲突, 需要重试操作 。而任何对资源有修改操作的线程,在修改数据前,都需要更新这个一致性标记,表示数据不再安全。

跟非阻塞调度比较,阻塞调度可以认为是一种悲观的策略,它会认为多个线程一起修改数据会使数据损坏,所以阻塞调度每次只能允许一个线程去修改数据。而非阻塞调度相对来说比较乐观,它认为如果多个线程一起修改也未必会把造成数据损坏,所以它允许多个线程同时进入临界区,但无障碍是一种宽进严出的策略,进的时候不作限制,所有的线程都能进入临界区做其想做的事情,包括读与写,但是出来的时候就不那么宽松了,如果一个线程在临界区中的操作遇到了数据竞争,跟其它线程产生了冲突,它就会回滚这条数据,然后重试自己的操作。比如读取x与y的值,这个操作是分步进行的先读x,再读y,当读完x,发现别的线程修改了x,再读y就已经没有意义了,因为可能会读到一个错误的数据,所以该线程会重试,再去读取一次,直到自己读到的x、y没有问题为止,所以无障碍是一种会不断重试的调度策略,但它会保证没有数据竞争时,线程必然能在有限的步骤内执行完任务。

在无障碍的调度方式当中,所有的线程都相当于在拿取一个系统当前的快照,它们会一直重试,直到拿到的快照有效为止

3、无锁(lock-free)
是无障碍的
保证有一个线程可以胜出
前面说的无障碍是指所有的线程都能进入临界区,但如果发生了竞争,无障碍并不保证临界区的线程能够顺利的出来,因为如果线程发现自己的数据每次去读取或者去操作,总是跟其它线程产生冲突,它就会不停地重试,如果在临界区当中有10个线程,线程1修改了部分数据,结果它被线程2干扰了,线程2又被线程3干扰,依此类推,最后线程1它又可能去干扰线程10,如果它们之间是彼此干扰的,最终会导致所有的线程都卡死在里面,系统的性能会受到比较严重的影响,因此,无锁必须在无障碍的基础上加一个约束,保证在竞争当中有一个线程是必然能够胜出的,这样就能保证在临界区的线程当中至少有一个是能顺利走出去的,而不至于全部在里面阵亡掉,如果至少有一个线程能够出去,那么就有第二个线程能够出去,假设里面有一百个线程,第一个线程竞争胜利,走出了临界区,剩下99个再竞争又必然能胜利一个,因为每次竞争它必然保证能有一个胜利,使得系统至少是能够顺畅的执行下去的,这就是无锁,下面这段代码在java当中是比较典型的使用无锁的代码:

while(!hyes.compareAndSet(localHyes,localHyes+1)){
    localHyes = hyes.get();
}

在高并发多线程中,CAS(Compare And Swap,比较交换)boolean compareAndSet(int expect, int update)技术就是一种无锁实现.在它的实现中,使用了一个无限循环,当要修改的内容和期望内容一致时,才去做修改.因此,CAS对死锁是免疫的.在java.util.concurrent.atomic包下(在jdk的rt.jar中)的各种原子类实现,都使用了CAS技术.例如在AtomicInteger中的getAndSet(int newValue)方法.

另外,使用无锁方式,省去了线程之间竞争临界区资源锁而产生的性能损耗,也没有线程之间频繁调度带来的开销.
4、无等待(wait-free)

  • 无锁的
    要求所有的线程都必须在有限步内完成

  • 无饥饿的
    前面说了无锁是能保证至少有一个线程能够在有限步当中完成它的操作,所有的线程在不停地竞争直到有一个胜出为止。无等待相比于无锁更进一步,它首先要求是无锁的,保证所有线程能进并且至少有一个线程能出来,同时无等待它在提高要求,它要求所有进入临界区的线程都能够在有限步当中完成其操作,这个要求很高,因为任何线程都能够无障碍进入临界区,并且任何线程都能够在有限步当中完成操作后离开临界区,这就会使得整个系统的运行变得非常顺畅,无等待可以说是并行最高级别了,它基本上能使整个系统发挥到最好佳效率。
    无等待必须然也是无饥饿的,因为所有的线程都能在有限步当中完成,因此必然不会有线程永久地呆在临界区内出不去,所以它一定是无饥饿的。

无等待的一个典型案例是,有读写两个线程,如果说只有读线程没有写线程,那么所有的读线程之间必然是无等待的,因为读不会修改数据,如果有一个写线程在里面,由于会修改数据 ,写线程必然会导致读线程不是无等待。因此可以提出一种算法去作一点改进,比如说有一种算法它会这样做,因为写可能会影响到读,所以每次写之前先把数据拷贝一份副本,线程修改的是这个副本而非原始数据,修改数据的过程可能需要一点时间,因为修改的是副本数据而不是原始数据,所以这个修改的过程也不影响线程读,因此在这个过程当中所有的读线程一样是无等待的,它们都能够在有限的步骤当中完成自己的操作,而所有的写线程相对来讲,因为每个写线程它都是写自己的副本,因此它们的写也是无等待的,所以它们都不需要去跟彼此作同步,最后需要同步的只是将写完之后的数据覆盖原始数据,而这个覆盖原始数据的动作是非常快的,因为我们并不需要作大量的写操作,只不过是一个指针或引用作一个替换而已,不管哪个写线程胜出,总是能够保证替换上去的数据是一致的,并不像其它的算法一样,可能会把数据写坏,因为大家都写的是副本,最后是一个指针指向谁的问题,这样数据必然是安全的,这种方式它就是无等待的一个典型的实现。

扫描二维码关注公众号,回复: 10690417 查看本文章

无等待表示任何线程都可以在有限步骤内结束,而不必关心其他线程进度如何.
————————————————
版权声明:本文为CSDN博主「qq_32534441」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_32534441/article/details/83989442

本文是一系列并发编程文章的学习笔记之一,意在厘清非阻塞、无锁、无等待着三个概念。

非阻塞
阻塞是操作系统层面的概念,发生阻塞的准确含义为:当前执行上下文通过调用操作系统的接口,使自己进入等待某一事件发生的状态。在这种等待中,该上下文将被从操作系统的调度队列中移除,而将不获得任何CPU执行时间。直到等待的事件发生,才被从新纳入调度。

因此,所谓非阻塞算法是指,算法中不存在任何位置将使当前上下文进入阻塞的状态。

无锁
**无锁是一种比非阻塞更强的条件,也就是说所有无锁算法都是非阻塞的,**但是非阻塞算法不一定是无锁的。

无锁算法指的是,在不用互斥锁的情况下解决并发执行环境的Race Condition。无锁算法与非阻塞算法的关键区分在于:无锁算法在执行过程中,某一上下文因为任何原因无法继续执行需要暂时搁置时,其他上下文能否继续执行,如果可以则该算法是无锁算法,否则该算法就不是无锁算法,顶多只有可能是非阻塞算法。

例如,自旋锁(Spin-lock)算法:某一执行上下文在获得锁之后,其他上下文需要循环忙等,
就是一种非阻塞算法但不是无锁算法。因为在自旋锁算法中,所有上下文在并发冲突时都是忙等,通过调用操作系统的接口把自己从操作系统的调度队列中移除,是无阻塞算法。但它不是无锁算法,因为当获得锁的上下文无法继续执行时,其他所有上下文都必须忙等而无法继续执行。

无等待
无等待是一种比无锁更强的条件,无等待算法要求在无锁算法的定义基础上,增加一个条件:所有上下文的执行都必须在有限的步骤内可以完成,而不依赖于其他上下文的状态。

发布了30 篇原创文章 · 获赞 0 · 访问量 2045

猜你喜欢

转载自blog.csdn.net/s11show_163/article/details/103433356