Motivation
承并发编程笔记Outline,这篇文章专注于记录学习基于锁的并发概念的过程中出现的一些知识点,为并发高层抽象做必要的准备。
尽管存在Doug Lee开山之作Concurrent Programming in Java, 2th edition: Desing Principles and Patterns、Brian Goetz撰写的一些列文章(http://www.ibm.com/developerworks/cn/java/j-jtp/ )、以及随后出现的一系列有关Java并发的技术书籍(大部分可以见Outline中参考文献部分)、甚至JLS中单独划出的一章,但已经厌倦了从Observable现象出发,建立似是而非的知识体系,自己尝试将潜台词变为显台词。
这其实是一个可接受的原子性概念问题,API用户不会去考虑API实现者所面临的问题和goose swimming,API实现者不会去考虑实现机制基础设施提供者设计时所做的权衡;亦即,有一种信任或盲目的信仰:我正使用的黑盒就是正确的。而取乎其上得乎其中,取乎其中得乎其下,取乎其下则无所得矣,学习基础性知识时,需要了解一些元知识,才能够对基础性知识结构有充分的认识。本文基于这些考虑,将基于锁的并发概念理论基础作为Java并发构造和组织的元知识考察。
Scope
- NOT
(1) 不争辩并发/并行、进程/线程之间的区别
(2) 不讨论任何汇编或CPU支持的特定指令
(3) 不讨论任何成熟语言内嵌支持的并发语法构造
(4) Others not included in the YES PART
- YES
(1) 互斥问题定义(mutual exclusion problem)
(2) 基于锁的互斥问题解决方案
(3) 命令式语言中基于锁的并发对象
(4) 原子性概念(atomicity/linearizability)
Progress
2015/10/12 start reading [1]'s PART I Lock Based Synchronization
2015/10/20 start reading [1]'s PART II The Atomicity Concept
2015/10/23 night init this manual script
2015/10/25 afternoon only chapter 5 about atomicity concept left
2015/10/25 night first version done, should add more observations
Content
0 并发计算机器模型
这里使用[2]中的并行计算机模型CTA(Candidate Type Architecture),见下图。
将该模型中Interconnection Network视为共享主存,移除NIC(Network Interface chip),能够表征共享内存的并发计算机器抽象。
其中,Pi是代表标准顺序性机器的处理器(processor),特别的,控制处理器Pc负责各处理器之间的协同。各处理器由分别代表程序和数据的处理单元和内存构成。模型中存在两类内存引用:廉价的局部内存引用和昂贵的非局部内存引用。
有关顺序性(sequential)的概念会在后面阐述原子性概念中做进一步说明,这里可以简单的认为,处理器中处理单元严格的依次执行程序指令。
1 并发词汇
1.1 基础性术语[1]
在下面的内容中,如无特别说明,均参考了文献[1]。
(1) 顺序性算法(sequential algorithm)
顺序性算法是顺序状态机行为的形式化描述,算法的内容声明了状态的转换必须按顺序执行。
(2) 程序(program)
算法被特定的编程语言可靠实现,转变为可以在机器上执行的程序。
(3) 进程(process/thread)
进程概念的引入是为显式区分算法的两种形态:文本形态、(在处理器上)执行形态。进程作为算法/程序在处理器上执行所产生的动态实体抽象,可以理解为text in action。
(4) 顺序性进程(sequential process)
顺序性进程是一类由单个控制流定义的进程,即其所表征的动态实体的状态转变只受单个控制流影响。通常,这个控制流所承载的实体是顺序性机器中的程序计数器(programm counter)。
(5) 并发算法/并发程序(concurrent algorithm)
并发算法是就一组顺序状态机通过通信媒介交互行为的文本描述,这里通信媒介特指共享内存。将进程视为独立的顺序状态机,容易生成并发程序的定义。
(6) 处理器(processor)
处理器是进程执行的承载体。
当进程数量多于处理器数量时,假设总存在一个调度器负责将进程分配到处理器上执行。因存在进程需要等待处理器资源的情况,其上次执行与获得处理器再次执行之间的时间不定,这构成了进程异步执行的假设。将进程获得处理器资源得以继续执行称为取得进展(progress),在不引起歧义的情况下,也可以称为进程的执行。
(7) 进程同步(synchronization)
进程同步指一些进程的执行依赖于其它一些进程的行为,即进程间存在依赖关系。从建立进程间依赖关系的意图来看,有两类情况需要同步:竞争(competition)和协作(cooperation)。
1.2 解读
基于对进程需协作同步高效解决生产者-消费者问题的讨论,文[1]中提出了一个重要的观点:同步的目标是保持所依赖数据结构的特性具备的不变性。
并发程序的复杂性在于,在为获取程序预期结果的努力过程中,如何安排进程的执行顺序、如何维护可以作为进程内部状态和程序预期结果的数据结构上的副作用;同时进程执行顺序安排与数据结构上的副作用维护,或多或少总是存在相互影响,例如:基于现有数据结构上的副作用确定进程是否能够执行、数据结构上的副作用受进程执行先后顺序的影响。
明确的通过锁的方式显式控制进程的执行顺序,在此基础上只需要维护数据结构副作用,降低了验证算法/程序正确性的难度。这是本文主要考察的内容。此外,存在一类无锁的(lock-free)并发算法,其算法正确性验证显然需要付出额外的努力。
需要注意的是,锁概念包含两个涵义,一是作为静态实体对象的抽象存在、二是动态实体(进程)在该静态实体对象上执行获取/释放这两个动作和动作执行结果对动态实体行为的影响。
2 互斥问题定义
2.1 概念[1]
(1) 关键区域(critical section)
假设出于一致性的原因,一个算法中的一部分代码、或多个算法中的一些代码,在任一时间点只能被单个进程执行,其他进程不能执行这部分代码或这些代码的一部分。前面提到的这段代码或这些代码称为关键区域。
同时假设在任一时间点执行关键区域的进程,总能终止关键区域的执行。
(2) 互斥(mutex exclusion/mutex)
将关键区域中的代码抽象成过程cs_code(in)
,in是输入参数,不失一般性认为无返回值。
互斥概念包括关键区域进入协议acquire_mutext()
、关键区域和关键区域退出协议(release_mutex()
,进入协议和退出协议包裹关键区域,保证一次最多有一个进程可以执行cs_code(in)
。
良式的进程代码是指,该进程想执行cs_code(in)
,必须先执行acquire_mutext()
、再执行cs_code(in)
,最终执行release_mutext()
。
将此概念表述为高层抽象中的过程:
procedure protected_code(in) is
acquire_muext(); r <- cs_code(in); release_mutex(); return(r)
end procedure.
(3) 互斥问题
互斥问题在于实现acquire_mutext()
、release_mutex()
时,需要满足:
(a) 互斥:一次最多只有一个进程执行关键区域代码;
(b) 无饥饿(starvation freedom):任意进程p,p的每次acquire_mutext()
调用,最终总会终止。
(4) 并发问题的属性
并发问题通常用安全性(safety)和活性(liveness)属性定义。
安全性属性声明没有坏的事情发生,通常可以用不变量约束表示;在互斥问题定义中,不变量约束指互斥(a),即在任一时刻最多只能有一个进程在执行关键区域代码;
活性属性声明好的事情会发生;在互斥问题定义中,活性属性指无饥饿(b),即如果一个进程尝试执行关键区域中代码,最终总会执行。
其他的一些活性属性包括:
(a) 无死锁(deadlock freedom) 任意时刻t,如果在t之前已有一个或多个进程调用了acquire_mutex()
,但这些调用均没有在时刻t终止,则存在一个时间点t' > t,这个或这些进程中的一个会终止acquire_mutex()
调用,即进入关键区域。
(b) 有界等待(bounded bypass) 设f(n)是可能参与竞争的进程总数n的函数。存在函数f(n),每次进程调用acquire_mutex()
时,最多会竞争失败f(n)次。将无饥饿视为f(n)值域为有限的有界等待,亦即有限等待。
(c) 这些活性属性按强度由高到低排列:有界等待 > 无饥饿(有限等待) > 无死锁。
(5) 锁(lock)
锁LOCK是一个共享的对象,提供了两个操作LOCK.acquire_lock()
和LOCK.release_lock()
。
LOCK内部状态有两个值:free
和locked
,初始值为free
。操作LOCK.acquire_lock()
终止后,LOCK内部状态为locked
,操作LOCK.release_lock()
终止后,相应的状态为free
。
LOCK对象的行为由顺序行规范描述(进一步讨论见原子性概念一节),从对象外部观察者的角度来看,所有LOCK对象上操作的调用按顺序依次执行:(LOCK.acquire_lock(); LOCK.release_lock())*
。
锁对象与互斥之间的关系
可以将LOCK.acquire_lock()
、LOCK.release_lock()
分别视为acquire_mutex()
、release_mutex()
的等价操作,则可以将锁对象理解为带有互斥特性约束的对象。进一步,正确的实现锁对象问题与解决互斥问题等价。
3 基于锁的互斥问题解决方案
文[1]中总结出三类基于锁的互斥问题解决方案:
(1) 原子读写寄存器(atomic read/write registers)
约束:进程间只通过读写共享的原子寄存器方式通信。
(2) 特殊的硬件原语(hardware primitives)
约束:多处理器架构通常会提供一些能够辅助解决同步问题的硬件原语。
(3) 无原子性支持的互斥(mutex without underlying atomicity)
约束: 不同于(1)、(2),没有对底层的共享内存提供低层次原子性操作的基础性假设。
3.1 基于原子读写寄存器的互斥实现
3.1.1 基础性术语
(1) 寄存器(register)
寄存器R可以被两个基本操作访问:R.read()
返回R的值,记为x <- R;R.write(v)
,向R写入值v,记为R <- v。只有单个进程访问的寄存器称为进程的局部寄存器,可被多个进程访问的寄存器称为共享寄存器。
(2) 原子寄存器(atomic register)
共享的原子寄存器是满足下列属性的寄存器:
(a) 对于读写操作调用op,有:
- 从外部观察者的角度看,该调用在时间线上的某一时刻t(op)执行;
- t(op)满足tb(op) <= t(op) <= te(op),tb(op)、te(op)分别表示op的开始和结束时间点;
- 对于任两个不同操作op1、op2,有(op1 != op2) => (t(op1) != t(op2))。
(b) 每个读调用操作返回之前最近写调用操作写入的值,之前最近由t()时间点序列定义;如果之前不存在写操作调用,则返回寄存器的初始值。
(3) SWSR, SWMR, MWMR
按可读写寄存器的进程数量区分,寄存器可以是single-writer/single-reader、single-writer/multi-reader、multi-writer/multi-reader。
注意,writer和reader可以是不同的进程。
3.1.2 Peterson's algorithm for 2 processes
基于两个基本的操作:
// DATA STRUCTURE DEFINITIONS
i, j: process identifiers
AFTER_YOU: atomic register,
element value domain: {i, j}
FLAG: atomic register array,
element value domain: {UP, DOWN}
element init value: DOWN
// PROCEDURES
operation acquire_mutex1(i) is
AFTER_YOU <- i;
wait(AFTER_YOU != i);
return();
end operation.
operation release_mutex1(i) is
return();
end operation.
operation acquire_mutex2(i) is
FLAG[i] <- UP;
wait(FLAG[j] = DOWN);
return();
end operation.
operation release_mutext2(i) is
FLAG[j] <- DOWN;
return();
end operation.
其中,acquire_mutex1(i)
、release_mutex1(i)
是进程pi执行的操作,注意并不是满足互斥定义的操作;acquire_mutex2(i)
、release_mutex2(i)
是另一个操作。
AFTER_YOU
体现了绅士行为特征,尝试让别人先执行;FLAG
体现了意图预警,在执行前升起旗子,表明即将进行操作。
下面是Peterson提出的两个进程的互斥算法(进程pi执行代码部分):
// DATA STRUCTURE DEFINITIONS
i, j: 2 processes' identifiers
AFTER_YOU: atomic register,
element value domain: {i, j}
FLAG: atomic register array,
element value domain: {UP, DOWN}
element init value: DOWN
// PROCEDURES
operation acquire_mutex(i) is
FLAG[i] <- UP;
AFTER_YOU <- i;
wait((FLAG[j] = DOWN) or (AFTER_YOU != i));
return();
end operation.
operation release_mutex(i) is
FLAG[i] <- DOWN;
return();
end operation.
定理1 上述算法满足互斥和有界等待(f(n) = 1)。
证明该系列定理通常采用的策略是反证法、归约和构造性证明。囿于对理论证明仍存在不足,这里不敢班门弄斧,同时不对算法效率进行分析,仅记录重要的结论。
3.1.3 Peterson's algorithm for n processes
Peterson提出的多个进程的互斥算法(进程pi执行代码部分):
// DATA STRUCTURE DEFINITIONS
n: number of processes
i: process's identifiers
l: level's identifier,
value domain: {1, ..., n-1}
AFTER_YOU: atomic register array,
index value domain: {1, ..., n-1}
element value domain: {0, ..., n-1}
FLAG_LEVEL: atomic register array,
index value domain: {0, ..., n-1}
element value domain: {1, ..., n-1}
init value: 0
// PROCEDURES
operation acquire_mutex(i) is
for l from 1 to (n-1) do
FLAG_LEVEL[i] <- l;
AFTER_YOU[l] <- i;
wait (all(k != i): FLAG_LEVEL[k] < l) or (AFTER_YOU[l] != i)
end for;
return();
end operation.
operation release_mutex(i) is
FLAG_LEVEL[i] <- 0;
return();
end operation.
基本思想是进程等待晋级机会,最终只有一个进程会在等级(n-1)。从算法中可以看出,进程获得晋级机会的条件:
(1) 所有的进程在比当前进程低的等级上,即all(k != i): FLAG_LEVEL[k] < l
;或者
(2) 当前进程不是最后一个进入当前等级l
的,即AFTER_YOU[l] != i
。
定理2 上述算法满足互斥和无饥饿。
3.1.4 Lamport L.'s concurrency abort algorithm
该算法尝试解决如下问题:
是否存在解决无竞争场景下n体互斥问题复杂度为常量的算法?
并发终止操作(concurrent abortable operation)
并发终止操作,又称为竞争终止操作,是一类允许在存在并发时返回值abort的操作,不存在并发时返回值commit。假设每个进程最多只调用并发终止操作(conc_abort_op()
)一次,则需要满足如下属性:
(1) 第一个进程在无并发情况下调用conc_abort_op()
,返回commit;
(2) 最多只有一个进程获得返回值commit。
下面是Lamport提出的多进程并发终止算法:
// DATA STRUCTURE DEFINITIONS
X: MWMR atomic register
value domain: process identifier, init Any
Y: MWMR atomic register
value domain: process identifier, init NULL
// PROCEDURES
operation conc_abort_op() is
X <- i;
if(Y != NULL) then return(abort1);
else
Y <- i;
if(X = i) then return(commit);
else return(abort2);
end if
end if
end operation.
定理3 上述算法可以保证:
(1) 最多只有一个进程获得值commit;
(2) 如果第一个进程在无并发场景下调用conc_abort_op()
,其获得值commit。
3.1.5 Lamport L.'s fast mutex algorithm
rather complicated!!!㋡
3.2 基于特殊硬件原子操作原语的互斥实现
3.2.1 test&set()/reset()
X是初始值为1的共享寄存器。X.test&set()
将X置为0,返回寄存器之前的值。X.reset()
将X置为初始值1。
基于该寄存器的互斥算法:
// DATA STRUCTURE DEFINITIONS
X: specific shared register
value domain: {0, 1} init 1
{
//OPERATIONS
test&set(): set value 0, return previous value
reset(): set value 1
}
// PROCEDURES
operation acquire_mutex() is
repeat
r <- X.test&set()
until (r = 1)
end repeat
return();
end operation.
operation release_mutex() is
X.reset();
return();
end operation.
3.2.2 swap()
X是共享寄存器。X.swap(v)
原子的将X置为v,返回X之前的值。
基于该寄存器的互斥算法:
// DATA STRUCTURE DEFINITIONS
X: specific shared register
value domain: {0, 1} init 1
{
//OPERATIONS
swap(r): set value r, return previous value
}
// PROCEDURES
operation acquire_mutex() is
r <- 0;
repeat
r <- X.swap(r)
until (r = 1)
end repeat
end operation.
operation release_mutex() is
X.swap(r);
return();
end operation.
约束:进程在关键区域中不修改局部变量r
。
3.2.3 compare&swap()
啊,Javaer怎么这么熟悉?
X是共享寄存器,取值范围为{old, new}。X.compare&swap(old, new)
原子的返回一个布尔值,满足:
X.compare&swap(old, new) is
if(X = old) then
X <- new;
return(true);
else return(false)
end if.
基于该寄存器的互斥算法:
// DATA STRUCTURE DEFINITIONS
X: specific shared register
value domain: {0, 1} init 1
{
//OPERATIONS
compare&swap(old, new): set value new if previous value is old, return true; otherwise return false
}
// PROCEDURES
operation acquire_mutex() is
repeat
r <- X.compare&swap(1, 0)
until (r)
end repeat
end operation.
operation release_mutex() is
X <- 1;
return();
end operation.
3.2.4 活性提升:从无死锁到无饥饿
注意到,因参与并发的进程异步执行的特性,3.2.3节中互斥算法并不能避免饥饿。以X.compare&swap()
为例,一个进程在acquire_mutex()
中repeat执行,但因不存在公平的调度方式,极有可能,它总是不能获得返回值true,即不能进入关键区域。
这部分内容的目的是提升3.2.3节中互斥算法的活性,生成活性为无饥饿的算法。其基本思路是,在低层满足互斥和无死锁的LOCK对象基础上,实现round-robin调度机制,保证进入关键区域的尝试不会被永久推迟。
// DATA STRUCTURE DEFINITIONS
n: number of processes;
FLAG: SWMR atomic register array
index domain: {1, ..., n}
value domain: {DOWN, UP} init DOWN
FLAG[i] can only be written by process i(pi)
LOCK: lock object
Lock.op(i): operation invoked by pi
TURN: MWMR atomic register
store process's identifier
value domain: {1, ..., n} init Any
// PROCEDURES
operation acquire_mutex() is
FLAG[i] <- UP;
wait(TURN = i) or (FLAG[TURN] = down);
LOCK.acquire_lock(i);
return();
end operation.
operation release_mutex() is
FLAG[i] <- DOWN;
if(FLAG[TURN] = DOWN)
then TURN <- (TURN + 1) mod n
end if;
LOCK.release_lock(i);
return();
end operation.
定理4 假设上述算法中LOCK是无死锁的,则该算法实现了无饥饿的互斥。
3.3 无低层原子性操作支持的互斥实现
低层的原子寄存器保证了其上的读写操作在时间线中被观察为是顺序性的。无该保证,是否能实现互斥呢?答案是可以。
3.3.1 基础性术语
(1) 安全寄存器(safe register)
SWMR安全寄存器的读操作满足条件:
(a) 不与写操作并发的读操作返回当前寄存器中的值。
(b) 与写操作并发的读操作返回寄存器能够包含的值。
条件(b)表明,读操作可能返回从未写入的值。
(2) 常规寄存器(regular register)
SWMR常规寄存器是SWMR安全寄存器,在条件(b)上有额外约束:
(b') 与写操作并发的读操作返回的值是,在写操作之前或这些写操作写入寄存器中的值。
(3) 原子寄存器(atomic register)
见3.1.1节,原子寄存器是比常规寄存器约束更多的一类寄存器,其上操作之间存在一个全序关系(total order)。
3.3.2 Lamport L.'s bakery mutext algorithm
Lamport的面包房互斥算法:
// DATA STRUCTURE DEFINITIONS
n: number of processes;
FLAG: SWMR register array
index domain: {1, ..., n}
value domain: {DOWN, UP} init DOWN
FLAG[i] can only be written by process i(pi)
MY_TURN: SWMR register array
value domain: priority number of proeccess which want to enter the critical section(non-negative number), init 0
MY_TURN[i] can only be written by process i(pi)
<x, i>: request of pi, with priority number x
a total order
<x, i> < <y, j>: equals (x < y) or ((x = y) and (i < j))
// PROCEDURES
operation acquire_mutex(i) is
// in the doorway
FLAG[i] <- UP;
MY_TURN[i] <- max(MY_TURN[1], ..., MY_TURN[n]) + 1;
FLAG[i] <- DOWN;
// in the waiting room
for each j in {1, ..., n}\{i} do
wait(FLAG[j] = DOWN);
wait((MY_TURN[j] = 0) or <MY_TURN[i], i> < <MY_TURN[j], j>)
end for;
return();
end operation.
operation release_mutex(i) is
MY_TURN[i] <- 0;
return();
end operation.
定理5 上述算法满足互斥和有界等待(f(n) = n - 1)。
因没有低层原子性支持,该定理的证明及其复杂。
4 命令式语言中基于锁的并发对象: 监视器
4.1 基础性术语
(1) 对象类型(object type)
对象类型定义由有限的操作集合和描述该类型对象上正确行为的规范构成。对象的内部表示对进程(操作调用者)是隐藏的。
进程访问对象的唯一方式是调用对象上的操作。
(2) 并发对象(concurrent object)
并发对象是可以被多个进程并发访问的对象。并发对象上的规范可以是顺序性的,也可以不是,亦即不是所有并发对象都有一个顺序性的规范。
(3) 基于锁的并发对象实现(lock based implementation of concurrent object)
在此,限定为具有顺序性规范的并发对象。
实现该对象最简单的方式是,使用锁,强制对象上的每个操作按互斥的方式执行。
(4) 信号量(semaphore)
信号量S是一个共享的计数器对象,提供了两个原子性的操作:S.down()
和S.up()
。
信号量对象的规范如下:
(a) S的初始值为非负值s0;
(b) 谓词S >= 0总成立;
(c) S.down()
原子性的将S减1;
(d) S.up()
原子性的将S加1。
当S = 1时,有两个或多个进程调用S.down()
,则其中一个成功,其他的被阻塞;在其他进程调用S.up()
时,被阻塞的进程被激活。
从定义中可以得到一个不变量约束:
记#(S.down)为S.down()
已终止的调用数量,则有S = s0 + #(S.up) - #(S.down)。
强信号量 在信号量上,已经被阻塞的进程按被阻塞的顺序激活。
二元信号量 S取值范围为{0, 1}的信号量。
进程私有信号量 只有进程pi能够调用S.down()
,其他进程只能调用S.up()
。
(5) 监视器(monitor)
监视器是并发对象,是一个在编程语言层次提供的并发对象抽象。
被并发访问时,监视器对象保证其上操作调用的互斥性质:一次只能激活对象中的一个操作。
(6) 监视器条件(condition/queue)
条件C是只能被用在监视器对象中的对象,提供一些操作:
(a) C.wati()
进程p调用该操作则停止执行。从操作的角度说,p在队列C中等待。
因进程不再活跃,监视器上的互斥被释放。
(b) C.signal()
进程p调用该操作,根据C当前的值有:
- 没有进程阻塞在C中,该操作没有任何副作用;
- 至少有一个进程阻塞在C中,该操作激活其中的一个。这种情况下,为保证一次只能有一个进程访问监视器的内部状态,需要添加如下规则:
-- 被激活的进程执行其调用C.wait()
后的语句;
-- 进程p不再活跃,当优先可重入监视器。当被允许进入监视器后,它开始执行调用C.signal()
之后的语句。
(c) C.empty()
该操作返回一个布尔值,表明队列C是否为空。
4.2 监视器
啊,Javaer怎么这么熟悉?
同步栅栏(synchroniztion barrier/rendezvous)对象
同步栅栏对象是有n个控制点的对象,每个进程在到达控制点后不再执行,只有在其他所有进程也到达控制点才通过控制点;n为参与进程的数量。
该对象提供了一个操作:rendezvous()
(或者barrier()
)。这样控制点是进程控制流中调用rendezvous()
的位置。
基于寄存器、基于监视器的并发对象实现:
基于寄存器的同步栅栏对象实现
// DATA STRUCTURE DEFINITIONS
n: number of processes;
m: number of control points
COUNTER: atomic register
value domain: {0, ..., n}, init 0
{
read();write();fetch&add();
}
FLAG: atomic binary register
value domain: {0, 1}
/FLAG is other value than FLAG
// PROCEDURES
operation rendezvous(i) is
flag <- FLAG;
COUNTER.fetch&add();
if(COUNTER = m)
then
COUNTER <- 0;
FLAG <- /FLAG;
else
wait(falg != FLAG)
end operation.
基于监视器的同步栅栏对象实现
// DATA STRUCTURE DEFINITIONS
m: number of control points
COUNTER: atomic register
QUEUE: condition object of monitor
monitor Rendezvous is
// interl repesentation
COUNTER in {0, ..., m} init 0;
QUEUE condition;
// PROCEDURES
operation rendezvous(i) is
COUNTER <- COUNTER + 1;
if(COUNTER < m) then QUEUE.wait()
else COUNTER <- 0
end if
QUEUE.signal();
return();
end operation.
end monitor.
5 原子性概念(atomicity/linearizability)
这一部分提供提供原子性的形式化定义。
5.1 基础性术语
因这一部分主要提供概念的定义,不在这里统一给出。
并发对象设计者面临的一些基础性问题:
- 并发对象的实现是否正确?
- 如何描述并发对象的行为?
- 一组进程访问一个或多个并发对象的执行是否正确?
- 当考虑不基于锁的并发对象实现时,如果存在进程在操作期间意外通知执行,正确性问题如何处理?
5.2 计算模型
计算模型由有限的进程构成:p1, ..., pn。为完成计算目标,进程需要协作、通过访问并发对象同步它们的活动。
5.2.1 进程和操作
操作执行(operation execution)和事件
进程通过执行并发对象暴露的操作进行同步。进程执行对象X上的一个操作记为X.op(arg)(res)
,arg
、res
分别表示操作调用的输入和输出参数。在不引起歧义的情况下,简记为X.op()
。这些进程、对象在多进程程序场景下定义。
进程p执行对象X上的操作op()
建模为两个事件:(1) inv[X.op(arg) by pi]
在pi调用操作开始时发生(又称为调用或开始事件);(2) resp[X.op(res) by pi]
在操作终止时发生(又称为响应/回复或结束事件)。称这些事件是由进程pi产生的、与对象X关联。
给定操作X.op(arg)(res)
,事件resp[X.op(res) by pi]
称为匹配inv[X.op(arg) by pi]
调用事件的响应事件。
执行(execution/run)
多进程的执行产生进程和对象间交互的一个序列。序列中每个交互称为事件。事件的序列称为历史(history)。
进程的顺序性(sequentiality of processes)
每个进程被认为是顺序性的,这意味着一个进程在某一时刻只能执行一个对象上的一个操作。换句话说,顺序性进程执行的算法规定,在调用一个对象上的一个操作之后,在该操作返回之前,该进程不能执行任何其他操作。
5.2.2 对象
对象
对象有名称和类型。对象的类型(type)定义为:
(1) 该类型对象可能拥有的值;
(2) 一组有限的操作;
(3) 一个规范,描述操作可以被调用的条件、操作调用后产生的副作用。
顺序性规范(sequential specification)
顺序性规范描述对象被顺序性的访问时(即在顺序性执行中)表现出的行为。对象的顺序性规范一般的定义方式是说明每个操作上的两个谓词(predicate):前置断言(pre-assertion)和后置断言(post-assertion)。前置断言在操作执行钱必须被满足,后置断言描述对象新值(新的内部状态)和操作返回的结果。
完整(total)和部分(partial)操作
如果对象操作在任何内部状态上均可被执行,则该操作是完整的;否则是部分的。完成操作上的前置断言在操作执行前必须被满足。
确定性(deterministic)操作和非确定性操作
如果给定任意满足对象前置断言的内部状态、任意操作上合法的输入参数,其返回结果和对象的最终状态是被唯一确定的,则称该对象操作是可确定的;否则,是不可确定的。
5.2.3 历史
将执行表示为事件的历史
当考虑由顺序性进程访问顺序性规范定义的并发对象生成的事件时,总是可以任意的安排其中出现的同时发生的(simultaneous)事件之间的顺序。作为事件实际发生时在时间线上的顺序的抽象,定义执行中事件上的一个全序关系(<H)。
历史(history)
将一组进程和一组共享对象之间的交互建模为调用事件、响应事件的一个序列,称该序列为历史(又称轨迹(trace)),记为Ĥ=(H, <H)。H是进程产生的事件集合,<H是这些事件上的一个全序关系。与Ĥ中事件相关的进程和对象,称做被包含在历史Ĥ中。特别的,历史Ĥ中由进程pi产生的事件构成的序列成为局部历史,记为Ĥ|pi。
完整(complete)历史和部分(partial)历史
一个操作在历史中是完整的,仅当历史中包含了对应于该操作的调用和响应事件。否则,称该操作是未决的操作(pending)。没有未决操作的历史称为完整历史,有未决操作的历史称为部分历史。
等价历史
两个历史Ĥ、Ĥ'称为是等价的,仅当它们的局部历史相同,即对每个进程pi,有Ĥ|pi = Ĥ'|pi。
良式历史
每个进程pi的局部历史是顺序性的,即该局部历史从一个操作调用开始,后接匹配的相应,接着是另一个操作调用,等等。每个局部历史都是顺序性的历史Ĥ,称为良式历史。
操作上的偏序
历史Ĥ生成操作上的一个偏序关系: ->H。操作op在操作op'之前发生(op ->H op'),仅当op在op'开始之前终止。这里,开始、终止对应于由H op') == (resp[op] <H inv[op'])
如果两个操作op, op'在历史称为在历史Ĥ中不存在resp[op] <H inv[op']或者resp[op‘] <H inv[op],则成这两个操作在历史Ĥ中重叠(或称为并发)。
顺序性的历史中不存在重叠的操作,即(op != op') => ((op ->H op') or (op' ->H op))。如果历史Ĥ是顺序性的历史,则->H变为一个全序关系。
5.2.4 顺序性历史
这里给出顺序性历史的正式定义。
顺序性的历史
历史是顺序性的(sequential),仅当其第一个事件是一个操作调用事件,且
(1) 每个调用事件后,可能除最后一个外,立即有匹配的响应事件;
(2) 每个响应事件后,可能除最后一个外,立即有一个调用事件。
不是顺序性的历史,称为并发的历史。
有顺序性历史定义后,可以在进程调用的操作粒度上,而不是在事件粒度上,对执行进行推理。
对象上的顺序性规范是一组顺序性历史,这些历史仅包含该对象的。同时,对象上的顺序性规范蕴含了所有对该对象执行顺序性访问的方式,以及该对象操作的前置断言和后置断言。
5.3 原子性
合法历史(legal history)
给定顺序性的历史Ⓢ,记Ⓢ|X为Ⓢ中所有与对象X相关的事件子序列,称Ⓢ是合法的,仅当:
对每个对象X,事件序列Ⓢ|X属于对象X的顺序性规范。
这里只考虑完整的历史;完整的历史Ĥ是原子的(atomic/lineraizable),仅当,存在历史Ⓢ,满足:
(1) Ĥ和Ⓢ等价;
(2) Ⓢ是顺序性的、合法的;
(3) ->H ⊆ ->S。
Ⓢ成为Ĥ的线性化表示(linearization of Ĥ)。
线性点(linearization point)
原子的历史Ĥ存在线性化表示,这意味着Ĥ中每个操作可以视为在时间线上调用事件和响应事件发生的两个时间点之间瞬时执行。这个瞬时执行的时间点称为线性点。
5.4 对象组合和保证终止属性
局部属性(local property)
P是定义在一组对象上的属性,称属性P是局部的,仅当:如果每个对象满足属性P时,这组对象作为一个整体也满足属性P。
重要的结论:原子性是一个局部属性。
定理6 历史Ĥ是原子的,当且仅当,对每个包含在Ĥ中的对象X,Ĥ|X是原子的。
定理7 inv[op(arg)]是在原子历史Ĥ中完整操作的调用事件,但处在未决状态。存在匹配的响应事件resp[op(res)],历史Ĥ'= Ĥ.resp[op(res)]是原子的。
5.5 其他一致性条件
not interested now.㋡
References
[1] Raynal M. Concurrent Programming: Algorithms, Principles, and Foundations. Berlin Heidelberg: Spring-Verlag. 2013.
[2] Lin C., Snyder L.. Principles of Parallel Programming. 北京:机械工业出版社. 2008.