在计算机科学中,同步是对并发活动的协调,使它们之间的交互满足指定的执行顺序和共享资源约束。同步可以防止相互冲突的访问,使一个活动等待另一个活动,并确保共享状态的变更在需要时可见。同步是并发计算和并行计算的核心概念;它并不一定意味着各项活动同时执行或以相同速度执行。(pubs.opengroup.org)
目的与范围
同步用于满足几类相互关联的需求:
- **独占访问:**防止对共享资源的不兼容操作在时间上重叠。
- **执行顺序:**确保某项操作只有在其前置操作完成后才会发生。
- **内存可见性:**确定一个执行上下文必定能够观察到哪些写入。
- **集体协调:**要求一组参与者先到达指定阶段,才能继续执行。(pubs.opengroup.org)
无论各项活动是在同一个处理器上交错执行,还是在不同处理器上执行,都会产生这些需求。单处理器并不能消除同步问题:一项活动可能在某个操作的步骤之间被中断,从而让另一项活动有机会访问同一状态。(cdn.kernel.org)
在共享内存系统中,同步通常通过锁和其他共享对象来协调线程。在分布式计算中,参与者可以通过消息交换和屏障等集体操作进行协调。这些机制所提供的保证取决于通信接口,而不是共享地址空间。(pubs.opengroup.org)
共享状态与冲突操作
临界区是一段执行区域,其中访问的状态需要通过受控访问来维护其完整性。互斥保证同一时刻只有一个参与者能够执行由同一把独占锁保护的区域。这种保护只有在所有相关访问都遵循同一同步协议时才有效;获取一把无关的锁并不能保护该状态。(cdn.kernel.org)
例如,counter++ 这样的递增操作可能包含读取数值、加一和存储结果三个步骤。如果两个线程在任何一方存储结果之前都读取了相同的原始值,就可能丢失一次递增。用锁保护整个“读取—修改—写入”序列,或者使用适当的原子递增操作,可以防止这种特定的相互干扰。(docs.oracle.com)
数据竞争涉及相互冲突的内存访问,这些访问缺少适用内存模型所要求的顺序保证或原子性。竞态条件则是一个更广泛的问题,指正确性取决于不受控制的事件顺序。消除数据竞争并不自动意味着一个多步骤协议就是正确的:即使各项操作分别受到保护,它们仍可能需要组成一个不可分割的事务。(pubs.opengroup.org)
同步机制
锁
互斥锁授予独占持有权,直至锁被释放。其他试图获取该锁的参与者可能需要等待。读写锁允许多个读取者同时访问,但要求写入者独占访问,因此在受保护的状态未被修改时,可以允许并发读取。(pubs.opengroup.org)
自旋锁通过反复尝试获取锁来等待,而不是挂起正在等待的执行上下文。阻塞锁可以挂起等待者,让处理器执行其他工作。两者的相对开销取决于竞争程度、等待时长、调度方式和具体实现;自旋在等待期间会消耗处理器时间。(cdn.kernel.org)
信号量与条件变量
信号量维护可用许可的数量。获取操作消耗一个许可;没有可用许可时,获取者需要等待。释放操作则提供一个许可。计数信号量可以限制同时使用某项资源的参与者数量,而不必将访问限制为单个参与者。(docs.oracle.com)
条件变量允许线程等待,直到某个基于共享状态的判定条件可能已经成立,例如队列变为非空。在 POSIX 中,等待操作以原子方式释放关联的互斥锁并阻塞调用者;从等待中返回时,会重新获取该互斥锁。被唤醒并不证明判定条件已经成立,因此需要再次检查该条件,通常是在循环中检查。这也能处理虚假唤醒。(pubs.opengroup.org)
屏障与完成对象
屏障(计算机)用于在指定的执行点协调一组参与者。例如,在 MPI 中,对组内通信器执行屏障操作时,只有当所有成员都进入该调用后,调用才会向任何参与者返回。屏障将计算划分为不同阶段,但并不要求参与者在完全相同的时刻到达。(mpi-forum.org)
其他抽象机制用于表达更具体的依赖关系。倒计时门闩用于等待一组事件完成;期值表示一个可能在稍后才可用的结果。这些机制协调任务的完成,但不一定授予对共享状态的独占访问权。(docs.oracle.com)
原子操作与消息传递
原子操作以相对于相关并发操作不可分割的方式执行访问或更新。原子的“读取—修改—写入”操作可用于实现计数器和更复杂的并发算法。原子性与内存顺序是不同的概念:在 C++ 中,采用宽松内存顺序的原子操作本身并不会为无关的内存访问建立同步关系。(docs.oracle.com)
消息传递可以将通信与同步结合起来。例如,在 Go 中,通道发送操作在同步顺序上先于对应接收操作的完成。这种顺序关系可以使较早的写入对接收方活动可见。带缓冲和不带缓冲的通信提供不同的保证,因此消息交换并不意味着存在一种普遍适用的同步行为。(go.dev)
内存顺序
内存模型规定了不同执行上下文之间允许观察到哪些内存操作结果。仅凭源代码中的顺序,并不一定能够建立另一个线程所需观察到的顺序。同步操作会对这些可观察结果施加明确规定的约束。(pubs.opengroup.org)
许多模型通过先行发生关系来描述这些约束。这种关系结合了执行上下文内部的顺序,以及通过上下文之间的同步建立的顺序。例如,释放一个互斥锁,随后再获取该互斥锁,可以使先前受保护区域内的写入变得可见。具体规则由相关语言或接口规范规定。(pubs.opengroup.org)
因此,不应将执行屏障与内存顺序机制视为可以互换的机制。等待参与者到达某个执行点,与确保特定内存或存储更新的可见性,是两种不同的保证;一个接口可能要求执行额外操作,才能同时提供这两种保证。(mpi-forum.org)
进展受阻与性能
同步可以维护共享状态的完整性,却仍可能使计算无法取得有效进展:
- **死锁:**参与者等待彼此持有的资源,导致无法继续推进。
- **饥饿:**某个参与者反复无法获得继续执行所需的资源。
- **活锁:**参与者持续活动并相互响应,却没有完成任何有效工作。(docs.oracle.com)
同步还会引入开销,并可能降低可实现的并行程度。竞争激烈的锁会使对其保护区域的访问串行化。将保护职责分散到更多锁上,可以提高并发程度,但也会增加协调访问和避免死锁的复杂性。因此,同步粒度的选择需要在协调开销、并行程度以及保证正确性的复杂性之间进行权衡。(cdn.kernel.org)
该术语的相关用法
**时钟同步**关注的是让各个时钟保持一致,而不是直接保护共享数据或规定程序操作的顺序。网络时间协议通过交换计时信息,使联网客户端和服务器的系统时钟同步。这是“同步”的另一种用法,与锁、条件变量和屏障所涉及的同步不同。(rfc-editor.org)
参考来源
- General Conceptspubs.opengroup.org
- The Go Memory Modelgo.dev
- System Interfaces Chapter 2pubs.opengroup.org
- Thread Interferencedocs.oracle.com
- Locking lessonscdn.kernel.org
- Unreliable Guide To Lockingcdn.kernel.org
- Debugging Multithreaded Programsoracle.com
- Multithreaded Programming Guidedocs.oracle.com
- pthread_cond_clockwaitpubs.opengroup.org
- Barrier Synchronizationmpi-forum.org
- java.util.concurrent (Java SE 25 & JDK 25)docs.oracle.com
- C++ International Standardisocpp.org