首页 > 其他分享 >无锁和无等待的定义和例子

无锁和无等待的定义和例子

时间:2023-08-04 17:01:42浏览次数:25  
标签:无锁 定义 free 算法 例子 线程 集居 等待

在查阅google之后,我发现没有一处对并发算法或是数据结构规定的演进条件(progress condition,注:参考[1],译者认为翻译为演进状态更为合适)做合理的解释。甚至在”The Art of Multiprocessor Programming“中也只有围绕书本的一小段定义,大部分定义是单行的句子,因而造成了我们普通人含义模糊的理解,所以在这里我把我对这些概念的理解整理在一起,并且在每一种概念后面给出相应的例子。


我们先将演进条件分为四个主要类别,阻塞(blocking),无干扰(obstruction-free),无锁(lock-free),和无等待(wait-free)。详细列表如下:


Blocking

1. Blocking

2. Starvation-Free

Obstruction-Free

3. Obstruction-Free

Lock-Free

4. Lock-Free (LF)

Wait-Free

5. Wait-Free (WF)

6. Wait-Free Bounded (WFB)

7. Wait-Free Population Oblivious (WFPO)


在我们开始解释每一个术语的意义之前,让我们先了解一些相关细节。


根据《The Art of Multiprocessor Programming》一书3.6节中的相关描述,我们很难理解有界无等待(wait-free bounded,注:参考[1])和集居数无关无等待(wait-free population oblivious,注:参考[1])的概念是否相关。就我个人而言,我认为一个算法如果满足集居数无关无等待,就意味着这个算法是有限界的,从而这个算法也满足有界无等待。这代表一个方法(或者是一个算法,一个类)可以是无等待,或者有界无等待,或者集居数无关无等待,最后一个条件是所有演进条件中“最好”的。


在这篇文章中我们讨论的例子都是关于某个(一个算法中,或一个类中)的特定方法的演进条件,而不是完整的算法。我们这样做的原因是一个算法中不同的方法可能会采用不同的演进保证(progress guarantees),举个例子:(某个算法)写操作可能是阻塞的,然而读操作是满足集居数无关无等待,这样的情况在你第一次观察某个无锁和无等待数据结构的时候并不是显而易见的。


是的,你的结论是正确的:一个数据结构中可能有某些操作是阻塞的,另外一些操作是无锁的,剩下的甚至是无等待的。另一方面,当我们称某个特定的数据结构是无锁,这代表这个数据结构的所有操作都是无锁,或者更好的状态(无等待或者更好)。


事实上在著作中没有严格对那种状态更好有严格的定义,但是一般而言说是这样的:


无等待>无锁>阻塞


上述说法的理由是:现实中并没有太多无锁或者是无等待的(实用)数据结构,所以通常不会出现关于演进状态合理分类的问题。Andreia和我提出了很多不同种类的并发算法,其中混合了各种演进状态。因此我们需要对这些演进状态合理地命名,并且对他们排序,从而继续对他们的处理,并且发现哪些算法值得研究。


我们提出的顺序就是文章开头描述的列表,表中阻塞是最差的,集居数无关无等待是可能达到的最好状态。


另一个通常会导致困惑的说法是“无等待意味着无锁”(但是不能反过来说)。这意味着一个方法如果满足无等待,那么这个方法有着和一个无锁方法同样的演进保证。下图是一张venn图,可能能够更好的解释我们的意思。




这张图展示了算法的集合,图中无等待的算法也是无锁算法的一部分。


1. 阻塞

阻塞是大家所熟知的。基本所有加锁的算法都可以说是阻塞的。某个线程所引起的意外延迟会阻止其他线程继续运行。在最坏的情况下,某个占有锁的线程可能被睡眠,从而阻止其他等待锁释放的线程进行接下来的任何操作。


定义:


一个方法被称为阻塞的,即这个方法在其演进过程中不能正常运行直到其他(占有锁的)线程释放。


例子:


循环中对拥有两个状态的变量的简单CAS操作


01	AtomicInteger lock = new AtomicInteger(0);
02  
03	public void funcBlocking() {
04  
05     while (!lock.compareAndSet(0, 1)) {
06  
07         hread.yield();
08  
09     }
10  
11	}

2. 无饥饿

(无饥饿)有的时候也被称为无闭锁。这是一个独立的性质,只有当底层平台/系统提供了明确的保障以后讨论这个性质才有意义。


定义:


只要有一个线程在互斥区中,那么一些希望进入互斥区域的线程最终都能够进入互斥区域(即使之前在互斥区中的线程意外停止了)。


例子:


一个严格公平的互斥锁通常是无饥饿的。


在JDK 8中的StampedLock有这样的性质,因为它创建了一个线程队列(链表)等待获取锁。这个队列的插入操作是无锁的,但是在插入之后,每个线程都会自旋或者让步从而被当前占有锁的线程锁阻塞。释放锁的线程采用unsafe.park()/unpark()机制,够唤醒下一个在队列中等待的线程,从而执行了严格的优先级。这个机制的意义是,如果给予其他线程(占有锁的线程)足够的时间去完成他们的操作,那么当前线程可以确保最终获取锁,然后完成自己的操作。


有关StampedLock的源码详见这里:


http://grepcode.com/file/[email protected][email protected]@jsr166e$StampedLock.java


3. 无干扰

这是一个非阻塞性质。关于无干扰和无饥饿的更多细节可以查看《The Art of Multiprocessor Programming》(revised edition)的第60页。


定义:


如果一个方法满足无干扰性质,那么这个方法从任意一点开始它的执行都是隔离的,并且能够在有限步内完成。


例子:


我所知道的唯一例子就是Maurice Herlihy,Mark Moir和Victor Luchangco所提出的Double-ended Queue。


http://cs.brown.edu/~mph/HerlihyLM03/main.pdf


4. 无锁

无锁的性质保证了至少有一个线程在正常运行。在理论上这代表了一个方法可能需要无限的操作才能完成,但是在实践中只需要消耗很短的时间,否则这个性质就没有什么价值了。


定义:


如果一个方法是lock-free的,它保证线程无限次调用这个方法都能够在有限步内完成。


例子:


一个调用CAS操作的循环增加原子整形变量。


01	AtomicInteger atomicVar = new AtomicInteger(0);
02  
03	public void funcLockFree() {
04  
05     int localVar = atomicVar.get();
06  
07     while (!atomicVar.compareAndSet(localVar, localVar+1)) {
08  
09         localVar = atomicVar.get();
10  
11     }
12  
13	}

另外一个比较著名的例子是java.util.concurrent中的ConcurrentLinkedQueue,其中add()和remove()操作是无锁的。


5. 无等待

无等待性质保证了任何一个时间片内的线程可以运行,并且最后完成。这个性质保证步骤是有限的,但是在实践中,这个数字可能是极大的,并且依赖活动的线程数目,因此目前没有很多实用的无等待数据结构。


定义:


假如一个方法是无等待的,那么它保证了每一次调用都可以在有限的步骤内结束。


例子:


这篇论文给出了一个无等待(有界无等待)算法的例子。


http://www.cs.technion.ac.il/~moran/r/PS/bm94.ps


6. 有界无等待

任何一个有界无等待的算法,也是无等待的(但并不一定是集居数无关无等待的)。


定义:


如果一个方法是有界无等待的,那么这个方法保证每次调用都能够在有限,并且有界的步骤内完成。这个界限可能依赖于线程的数量。


例子:


一个扫描/写入到长度和线程数目相关的数组的方法。如果数组中每个条目的操作数是常量,那么显然这个方法是有界无等待的,并且不是集居数无关无等待,因为数组的长度和线程的数目有关。


01	AtomicIntegerArray intArray = new AtomicIntegerArray(MAX_THREADS);
02  
03	public void funcWaitFreeBounded() {
04  
05     for (int i = 0; i < MAX_THREADS ; i++) {
06  
07         intArray.set(i, 1);
08  
09     }
10  
11	}

7. 集居数无关无等待

这个性质用来描述这些在一定数量步骤内完成一些指令,并且指令数目与活动线程数目无关的方法。任何一个集居数无关无等待的方法都是有界无等待的。


定义:


一个无等待的方法,如果其性能和活动线程数目无关,那么被称为集居数无关无等待的。


例子:


最简单的例子是使用fetch-and-add原语(在X86 CPU上是XADD指令)增加一个原子变量。这个操作可以用C11/C++11中的fetch_add()原子方法完成。


1	atomic counter;
2  
3	void funcWaitFreeBoundedPopulationOblivious() {
4  
5     counter.fetch_add(1);
6  
7	}

结论

上述的这些并不是问题的全部。我们忽略了两个了解全貌需要掌握的知识点:


第一点,如果你的方法需要分配内存,那么这个方法可以提供的演进保证在实际中受到内存分配机制的演进条件所限制。我认为,我们需要针对方法是否需要分配内存进行不同的分类。你可以在这篇文章中了解更多的细节,但是基本的思想是创造一个集居数无关无等待的,并且一直需要用阻塞机制分配内存的方法没有太大的意义。


第二点,关于演进条件的完整概念是用于将算法和方法按照时间保证分类,但是这些定义却是基于操作数目。这基于一个操作的完成时间与活动线程数目无关这个假设的,这些假设在单线程代码中是正确的,但是在多线程程序中将会失效。


CPU缓存一致性的工作机制,将会导致多线程/多核访问(原子)变量的竞争,从而使得一个操作/指令在一定情况下(因为cache-miss)需要相对更长的时间才能完成。如果你不相信我的话,可以看一看这篇文章,这就是为什么许多wait-free数据结构的实现要比lock-free的相同数据结构更慢的主要的原因(或者说主要原因之一),尽管他们对执行的操作总次数有更强的保证,每一个操作因为竞争的因素却可能要用很长的时间去完成……还可能是他们平均执行的操作次数更多。


总的来说,对于倾向于数学的读者,这里有我提出的定义:


设F为一个函数方法


设L为同时调用F的并发线程数目


设N为一个与L无关的变量


设OpsF()代表一个指定线程完成F需要进行的操作数目。


设C(n,L)为一个依赖N和L的函数


当任何有限值L满足以下条件时,F方法是对应的进行状态:


Lock-free:


如果至少有一个L线程在有限步骤内完成操作;OpsF() < C(N,L)


Wait-free:


如果所有的L个线程在有限的步骤内完成操作:OpsF() < C(N,L)


Wait-free bounded:


如果所有的L个线程消耗C(N,L)或者更少的时间完成操作:OpsF() < C(N,L)


Wait-free population oblivious:


如果所有的L个线程在有限操作内完成F,并且和L无关:OpsF() < C(N)


在实践中,“无等待”和“有界无等待”的区别很小,这篇论文中有很好的解释:


http://www.cs.technion.ac.il/~moran/r/PS/bm94.ps


另一个细节是,并没有“集居数无关无等待”(的数据结构,注:准确的话说是没有一个所有方法都是集居数无关无等待的数据结构),因为达到集居数无关的状态意味着,F方法有一个使其运行结束所需的最坏操作数目上限。


希望这篇文章可以帮助你理解lock-free和wait-free的区别。


参考

[1] 多处理器编程的艺术,http://book.douban.com/subject/3901836/




相关资源:low-latency-primitive-concurrent-queues:低延迟、无锁、原始...


标签:无锁,定义,free,算法,例子,线程,集居,等待
From: https://blog.51cto.com/u_2650279/6964901

相关文章

  • 配置文件定义值引用
    1.yml中写自己定义的一些固定值,定义为一个数组:system:default-role:-SUPER_ADMIN-DEVICE_WB_LEADER_ROLE-WEIXIU_ROLE-WX_TEAM_LEADER-FAULT_AUDIT_ROLE-FAULT_PINDOWN_ROLE-XY_VERIFICATION_ROLE-DEVICE_WBGCS_ROLE2.定义到一个实......
  • Element-plus组件库的MessageBox 消息弹框组件自定义样式的坑
    一、问题描述:在使用Element-plus组件库的MessageBox消息弹框组件时,需要更改该组件的按钮样式,于是根据官网文档: 找到cancel-button-class、confirm-button-class两个属性,并在js代码中进行了添加,如下案例图: 然后,我在使用该组件的页面写了如下样式:.my-confirm-button{......
  • Spring Boot 之自定义Starter
    没有调查就没有发言权!做JavaEE开发的小伙伴们肯定听说过“约定大于配置”这样一句话,就是说系统,类库,框架应该假定合理的默认值,而非要求提供不必要的配置springboot中也为我们提供了丰富的starterspring-boot-starter-web:快速构建基于SpringMVC的Web项目,使用Tomcat做默认嵌......
  • Java反射与自定义注解实现不同Bean属性映射的高效解决方案
    假设现有一个叫user的bean,里面有username、passsword、sex、createTime这四个属性,需要实现一个功能,把其它bean里的字段分别映射到user里的username、passsword、sex、createTime这几个属性上,然后可以根据其它的bean的实例自动转化为user的实例(填充对应属性值),下面是代码实现。Java......
  • 自定义sonar插件
    第一步:创建插件应用mvnarchetype:generate-DgroupId=com.yy-DartifactId=sonar-yy-plugin-Dpackage=com.yy-Dversion=1.0.0-SNAPSHOT-DarchetypeGroupId=org.apache.maven.archetypes-DarchetypeArtifactId=maven-archetype-quickstart......
  • jmeter自定义参数使用固定前缀
    使用元件随机变量参数填写方式若随机数取值为123生成值为:name_000000123若随机数取值为12312321生成值为:name_012312321......
  • 《Ext详解与实践》节选:自定义单元格的显示格式
    有时候,需要在Grid中显示一个链接,或根据数字的大小显示不同的颜色,又或者使用固定格式显示记录编号等,这就需要自定义单元格的显示格式。在Ext的Grid可以很容易的实现自定义单元格的显示格式。在定义Grid的ColumnModel的时候,在需要自定义显示格式的列中加入参数renderer,该参数指向一个......
  • protoc-gen-doc 自定义模板规则详解
    protoc-gen-doc自定义模板规则详解配套演示工程此项目中所用proto文件位于./proto目录下,来源于官方proto示例此项目中所列所有模板case文件位于./tmpl目录下此教程均基于markdown文本演示前言最近有通过proto文件生成其接口文档的需求,而protoc-gen-doc所生成......
  • Python中实现远程调用(RPC、RMI)简单例子
    远程调用使得调用远程服务器的对象、方法的方式就和调用本地对象、方法的方式差不多,因为我们通过网络编程把这些都隐藏起来了。远程调用是分布式系统的基础。远程调用一般分为两种,远程过程调用(RPC)和远程方法调用(RMI)。RPCRPC属于函数级别的远程调用,其多是通过HTTP传输数据,数据形式有......
  • 定义命运的决策:你的选择塑造你的未来
    命运是每个人都经历的不可预知的旅程。然而,我们的选择却是塑造这一旅程的重要因素。每个人都拥有决策的自由,无论是小至日常的选择还是大至重大的人生决策,都会对我们的未来产生深远的影响。命运,作为人们所认知的未来的走向和结果,往往被看作是不可抗拒的。然而,我们往往忽视了决策在......