1 基本概念

并发控制是指在多个执行单元同时访问共享资源时,对其访问顺序、冲突处理与结果一致性进行协调的一类机制。它既存在于软件系统内部,也广泛用于数据库、操作系统分布式环境中。其基本出发点是:在不显著牺牲效率的前提下,尽量避免由于并发访问带来的逻辑错误。

1.1 并发与并行

并发强调多个任务在同一时间段内交替推进,关注的是任务组织方式;并行则指多个任务在同一时刻真正同时执行,通常依赖多核处理器或多台机器。二者经常同时出现,但并不完全等同。一个系统可以具备很强的并发能力,却未必拥有高程度的并行执行。

1.2 共享资源与临界区

共享资源是指可被多个任务访问的数据、设备或状态,例如内存变量、数据库记录、文件句柄等。临界区则是其中需要避免同时进入的一段代码或操作范围。若多个执行单元在临界区内无序交错,就容易造成数据破坏或结果偏差,因此通常需要同步手段加以保护。

1.3 竞争条件

竞争条件是指程序结果依赖于线程、进程或事务的执行时序,而这种时序又并非稳定可控。只要参与者对共享状态的读写发生交叉,且缺少适当协调,就可能出现某些运行正常、某些运行异常的现象。它是并发程序中最常见也最难排查的问题之一。

1.4 一致性与可见性

一致性关注系统各部分对数据状态的理解是否相互协调;可见性则强调一个执行单元对共享数据所做的修改,何时、以何种方式被其他单元观察到。由于编译器优化缓存机制和内存模型的存在,修改并不一定立刻对所有参与者可见,因此并发控制还必须处理“写入完成但他人暂不可见”的情况。

2 并发控制的目标

并发控制的目标并不只是“让程序跑起来”,而是要在正确、稳定和高效之间取得平衡。不同系统的侧重点不同:数据库更强调一致性,操作系统更重视响应与公平,多线程应用则常在简洁性和吞吐量之间权衡。

2.1 正确性

正确性是并发控制的首要目标,要求程序在各种合法执行顺序下都能得到符合预期的结果。对于事务系统而言,这通常意味着数据状态满足既定约束;对于线程程序而言,则意味着共享变量不会被错误修改,计算结果不因调度变化而失真

2.2 安全性

安全性强调系统不会进入非法或破坏性状态,例如不会同时写坏同一份数据,也不会因为异常交错导致结构损毁。它通常通过互斥、隔离、原子操作或一致性协议来实现。与之相对,某些低层实现虽然速度更高,但若安全边界不足,风险也会随之上升。

2.3 活性

活性关注系统是否能够持续推进,而不是长期停滞在等待状态。一个并发系统即使保持了安全,也可能因为资源争用、循环等待或调度偏差而无法继续执行。活性的核心要求是:相关任务最终能够获得执行机会,并完成必要操作。

2.4 性能与可扩展性

高性能并发控制要尽量减少等待、阻塞和不必要的同步开销。可扩展性则指当线程数、请求量或节点数增加时,系统仍能保持较好的处理能力。现实中,越强的约束通常意味着越大的协调成本,因此设计时常需在吞吐量、延迟与实现复杂度之间折中。

3 主要应用场景

并发控制几乎贯穿所有现代计算系统。其具体实现会因场景不同而变化,但核心任务始终是协调多个执行者对共享状态的访问,并维持结果稳定。

3.1 操作系统

操作系统需要协调大量进程、线程和中断事件,是并发控制最基础的应用环境之一。由于系统资源有限且调度频繁,操作系统必须为共享内核数据、设备访问和任务切换提供可靠机制。

3.1.1 线程调度

线程调度决定哪些线程在何时获得 CPU 时间。调度策略会影响程序响应速度、CPU 利用率以及不同任务之间的公平性。为了避免某个线程长期占用资源,系统通常还会结合优先级、时间片和抢占机制进行管理。

3.1.2 进程同步

进程同步用于协调多个进程对共享文件、管道、内存映射区或其他系统资源的访问。常见手段包括互斥锁信号量和事件对象等。其作用是保证进程之间的协作顺序,避免互相干扰。

3.2 数据库系统

数据库中的并发控制尤为重要,因为多个用户可能同时读取和修改同一批数据。若缺少适当机制,事务间就容易产生覆盖、丢失或逻辑冲突。

3.2.1 事务处理

事务处理要求一组数据库操作要么全部成功,要么在失败时全部撤销。并发控制在这里负责协调多个事务的执行顺序,使它们在共享数据上的交互仍保持一致性和可恢复性。

3.2.2 隔离级别

隔离级别定义了事务之间可互相观察到的数据变化程度。较高的隔离级别能减少异常读写现象,但往往意味着更多等待和更低并发度;较低的隔离级别则提高吞吐量,却可能让某些中间状态被看到。

3.3 分布式系统

分布式系统由多台机器协同工作,节点之间通信存在延迟、故障和网络分区等不确定因素,因此并发控制比单机环境更复杂。

3.3.1 副本一致性

当同一数据存在多个副本时,系统需要决定更新传播的顺序和时机。副本一致性机制用于保证各节点对数据状态的认知尽量一致,或至少在可接受时间内趋于一致。

3.3.2 分布式事务

分布式事务跨越多个节点执行,协调难度较高,因为任一参与节点的延迟或失败都可能影响整体结果。此类机制通常需要额外的提交协调与故障恢复流程,以尽量保持原子性与一致性。

3.4 多线程应用开发

在应用程序开发中,并发控制常用于提升响应速度、利用多核性能或处理大量 I/O 请求。无论是服务器、桌面软件还是移动端组件,只要存在共享状态,就需要考虑线程安全与同步问题。

4 常见并发问题

并发环境下的问题往往并非立即显现,而是在特定时序、负载或硬件条件下才暴露出来。因此,识别并发缺陷通常比修复更困难。

4.1 数据竞争

数据竞争指多个执行单元在没有足够同步的情况下同时访问同一数据,其中至少一个为写操作。它会使程序行为变得不可预测,甚至在不同机器、不同编译条件下产生不同结果。

4.2 死锁

死锁是指多个任务彼此等待对方释放资源,结果谁都无法继续执行。常见于多个锁按不同顺序被持有时。死锁一旦形成,若无外部介入,系统通常会一直停在等待状态。

4.3 活锁

活锁与死锁不同,相关任务并未停止动作,而是不断响应彼此的行为,反复让步却始终无法完成目标。它常见于过于“礼让”的协调策略中,看似系统仍在运行,实际上有效进展很少。

4.4 饥饿

饥饿是指某些任务长期得不到资源或执行机会。它通常与不公平调度、优先级偏置或激烈竞争有关。与死锁相比,饥饿并不要求所有任务都停滞,但会导致部分任务被持续忽略。

4.5 丢失更新

丢失更新发生在两个或多个任务基于旧值分别修改同一数据,而后写回时后者覆盖前者,导致其中一部分修改被抹掉。这类问题在计数、库存、余额等场景中尤其常见。

4.6 脏读、不可重复读与幻读

脏读是指读取到尚未提交的中间结果;不可重复读是指同一事务中两次读取同一数据却得到不同结果;幻读则是指多次查询同一条件时,出现了前一次不存在、后一次却出现的新记录。这三类现象都与并发事务的隔离不足有关。

5 并发控制方法

并发控制方法种类繁多,不同方法适用于不同层级与不同负载条件。实际系统中常常不是单独使用某一种技术,而是将多种机制组合起来。

5.1 锁机制

锁机制通过限制资源的同时访问,来保证临界区内操作的原子性和顺序性。它是最直观也最常见的并发控制方式。

5.1.1 互斥锁

互斥锁要求同一时刻只能有一个执行单元持有锁并进入临界区。它实现简单,适合保护短小而明确的共享操作,但如果持锁时间过长,也容易造成等待增加。

5.1.2 读写锁

读写锁区分读操作和写操作,允许多个读者同时访问,但写入时需要独占。它适用于“读多写少”的场景,能够提高并发读性能,不过在写请求较多时,优势会明显下降。

5.1.3 自旋锁

自旋锁在获取不到锁时不会立即睡眠,而是在原地反复检查,等待锁释放。它避免了线程切换带来的开销,适合极短临界区和低等待场景,但在争用激烈时会浪费 CPU。

5.1.4 公平锁与非公平锁

公平锁通常按请求顺序分配锁,较少出现长期插队;非公平锁则可能让新来的线程直接抢占锁,从而提高整体吞吐量。前者更稳定,后者往往更高效,但也更容易导致个别线程等待时间变长。

5.2 信号量与条件变量

信号量和条件变量用于描述资源数量或事件状态,常见于线程同步与进程协作。它们不直接保护数据,而是协调任务何时继续执行。

5.2.1 计数信号量

计数信号量用一个数值表示可用资源数量。每次获取都会减少计数,释放则增加计数。它适合管理多个相同资源,例如连接池、缓冲槽位或并发许可数。

5.2.2 二值信号量

二值信号量只有两种状态,通常用于表示“可用”与“不可用”。在功能上,它与简单互斥保护有一定相似性,常被用于轻量级同步或事件通知

5.2.3 条件等待与唤醒

条件变量允许线程在某个条件未满足时进入等待,并在条件改变后被唤醒继续执行。它常与锁配合使用,以避免忙等。典型用途包括队列为空时等待新数据,或缓冲区满时等待空间释放。

5.3 事务并发控制

事务并发控制主要用于数据库和类似的事务型系统,目标是在多个事务同时运行时维持可接受的一致性。

5.3.1 两阶段锁协议

两阶段锁协议要求事务在“加锁阶段”只能获取锁,之后进入“解锁阶段”只能释放锁,不能再申请新锁。该方法有助于保证可串行化,但可能增加阻塞和死锁风险。

5.3.2 时间戳排序

时间戳排序依据事务的时间戳决定操作先后,系统按照预定顺序判定冲突是否允许。它减少了锁等待,但若冲突频繁,事务可能被反复回滚。

5.3.3 乐观并发控制

乐观并发控制假设冲突较少,事务先并行执行,在提交前再检查是否出现冲突。如果冲突不存在,则直接提交;若发现冲突,则需要回滚并重试。它适合读多写少的工作负载。

5.3.4 多版本并发控制

多版本并发控制为数据保留多个历史版本,使读取操作可以访问某个稳定快照,而写入操作生成新版本。这样既能减少读写阻塞,又能提高查询并发性,是现代数据库中常见的实现方式。

5.4 无锁与低锁技术

无锁与低锁技术试图减少或避免传统锁带来的等待和切换开销,适合高并发、高性能场景。不过,这类方法设计更复杂,调试和验证难度也更高。

5.4.1 原子操作

原子操作是不可被中断的基本操作单元,例如一次完整的读改写。它为更复杂的并发算法提供底层保障,是构建无锁结构的重要基础。

5.4.2 CAS

CAS 即比较并交换,先检查内存中的值是否与预期相同,再决定是否写入新值。它常用于无锁算法、计数器和队列实现,但在高竞争下可能产生大量重试。

5.4.3 线程安全数据结构

线程安全数据结构通过内部同步、原子更新或版本控制来支持多线程并发访问,例如并发队列、并发哈希表等。它们可以减少应用层手工加锁的负担,但使用时仍需注意整体访问模式。

5.5 基于消息传递的控制

基于消息传递的控制不直接共享内存,而是通过发送和接收消息来协调状态变化。由于参与者之间的交互被显式化,这种方式有助于降低共享内存冲突,常见于分布式系统和部分并发框架中。

6 数据库中的并发控制

数据库并发控制的重点在于维护事务正确性,同时尽量提高并发查询与更新能力。它通常结合锁、版本和日志机制共同实现。

6.1 锁粒度

锁粒度指锁定对象的范围大小,从整库、整表到行级、字段级不等。粒度越粗,实现越简单,但并发度越低;粒度越细,并发能力越强,却会增加锁管理成本。

6.2 隔离级别设计

隔离级别设计用于平衡一致性需求与系统性能。较强隔离能减少异常读写,但会让事务之间的阻塞更多;较弱隔离则提高吞吐量,但需要应用方接受一定程度的可见性变化。

6.3 死锁检测与预防

数据库系统通常会通过等待图分析、超时机制或锁获取顺序约束来处理死锁问题。检测方式侧重于发现后处理,预防方式则试图在锁申请阶段就避免形成循环等待。

6.4 多版本读写冲突处理

多版本机制下,读取通常不会阻塞写入,但写写冲突仍需识别与解决。系统会判断事务读到的是哪个版本,以及提交时新写入是否与其他事务形成冲突,从而决定是否允许提交。

6.5 提交与回滚机制

提交表示事务所做修改正式生效,回滚则撤销未完成或失败的修改。提交与回滚机制为并发执行提供收尾保障,使系统在异常中仍能恢复到一致状态。

7 操作系统中的并发控制

操作系统层面的并发控制多发生在内核、驱动和系统调用路径中。由于这些部分直接接触硬件与全局状态,因此对同步的要求很高。

7.1 进程同步原语

进程同步原语包括互斥锁、信号量、条件变量、事件等,用于协调进程或线程的执行顺序。它们是操作系统向上层提供的基础工具,许多高层并发框架都建立在这些原语之上。

7.2 临界区管理

临界区管理的核心是确保同一时刻只有合适的执行者访问关键资源。操作系统通常会结合锁、禁用中断或内核态保护机制来维护临界区安全,避免内核数据结构被并发破坏。

7.3 调度与抢占

调度决定任务执行次序,抢占则允许系统在更高优先级任务到来时中断当前任务。二者共同影响并发行为的公平性与响应速度,也会间接改变竞争发生的概率。

7.4 中断与并发访问

中断会在任意时刻打断当前执行流程,因此是并发控制中的重要干扰源。操作系统必须处理好中断处理程序与普通代码之间对共享状态的访问,避免出现不一致或重入问题。

8 分布式系统中的并发控制

分布式环境下的并发控制不仅要协调并发请求,还要面对网络延迟、节点失效和消息乱序等额外复杂性。

8.1 分布式锁

分布式锁用于协调多台机器上的多个客户端对同一资源的访问。其实现通常依赖中心协调服务或一致性协议,以避免多个节点同时认为自己获得了锁。

8.2 共识与一致性协议

共识协议用于让多个节点对某个决定达成一致,例如某次写入是否生效、某个顺序是否成立。它是分布式并发控制的重要基础,尤其适用于需要强一致性的系统。

8.3 时钟与顺序控制

在分布式系统中,不同节点的本地时钟并不完全同步,因此仅靠物理时间难以准确判断事件先后。系统往往需要借助逻辑时钟、向量时钟或消息序关系来推断操作顺序。

8.4 副本更新协调

副本更新协调负责决定更新如何传播到各个副本,以及在冲突时如何合并或覆盖。其设计目标通常是在一致性、可用性和延迟之间取得平衡。

9 性能优化与工程实践

并发控制不仅是理论问题,也是典型工程问题。很多系统性能瓶颈并不在计算本身,而在同步、等待和资源争用上。

9.1 减少锁竞争

减少锁竞争的常见做法包括缩短持锁时间、降低共享范围、拆分热点数据以及避免在锁内执行耗时操作。锁竞争一旦过高,系统吞吐量往往会明显下降。

9.2 分段与分片

分段与分片通过将大对象或大数据集拆成多个独立部分,让不同线程或节点处理不同区域,从而降低冲突概率。它是提升并发度的有效手段,尤其适合高访问量场景。

9.3 读写分离

读写分离将读请求与写请求分配到不同路径或不同实例,以减轻主写节点压力。它适用于读多写少的业务模型,但需要额外处理数据同步与延迟可见的问题。

9.4 批处理与合并提交

批处理可以把多次小操作合并为一次大操作,减少同步和提交开销;合并提交则通过统一刷新或统一落盘来提升效率。此类方法通常能改善吞吐量,但也可能增加延迟。

9.5 监控与故障排查

并发问题具有隐蔽性,工程上往往需要依赖监控、日志、追踪和压力测试来定位。死锁检测、锁等待统计、线程栈分析和事务回放等手段,都有助于发现隐藏缺陷。

10 典型算法与实现示例

一些经典算法常被用来说明并发控制的基本思想,也常作为教学或系统设计的参考模型。

10.1 Peterson算法

Peterson算法是一种经典的软件互斥算法,主要用于说明两个进程如何仅靠共享变量实现互斥进入临界区。它具有很强的教学意义,但在现代多核和复杂内存模型下,实际应用范围有限。

10.2 Lamport算法

Lamport相关算法在并发顺序控制和分布式一致性理论中具有代表性。它强调用逻辑顺序而非仅凭物理时间来描述事件关系,因此常被用来解释分布式环境中的先后判断。

10.3 生产者-消费者模型

生产者-消费者模型描述一类常见协作关系:一方生成数据,另一方消费数据,中间通常通过缓冲区连接。它广泛用于任务队列、消息系统和流式处理,是并发同步的典型例子。

10.4 读者-写者问题

读者-写者问题关注多个读操作与少量写操作如何协调,目标是在保证写入正确性的同时尽量提高读取并发度。它直接对应读写锁等机制的设计思想。

10.5 餐厅哲学家问题

餐厅哲学家问题是并发控制中的经典示例,用来说明资源争用、死锁与饥饿等现象。由于模型简洁而具有代表性,它常被用于展示锁设计和调度策略的优缺点。

11 相关理论基础

并发控制并不只依赖工程实现,也建立在一系列形式化理论之上。这些理论帮助人们描述、证明和验证并发系统的行为。

11.1 图论与等待图

图论常用于刻画任务与资源之间的关系,其中等待图尤其常见。通过分析图中是否存在环,可以判断系统是否可能出现死锁,因此它是并发分析的重要工具。

11.2 形式化验证

形式化验证通过数学模型检查系统是否满足预定性质,例如互斥性、无死锁或顺序一致性。它适合用于高可靠场景,但建模成本较高,通常用于关键组件而非全部代码。

11.3 线性一致性

线性一致性要求并发操作看起来像是按某个全局顺序依次完成,并且这个顺序与真实时间保持合理一致。它是衡量共享数据可观察行为的一种重要标准,常见于高可靠分布式存储与同步系统。

11.4 可串行化理论

可串行化理论研究多个并发事务的执行结果是否等价于某种串行执行顺序。它是数据库并发控制的核心理论之一,用来判断事务调度是否足以保证逻辑正确性。