Go 多线程编程-线程同步
临界区是指访问共享资源(如内存、文件、设备等)的代码段,该区域在并发环境中必须保证互斥访问。当多个线程/进程同时进入临界区时,会导致数据竞争(Race Condition),引发不可预测的结果(如数据损坏、程序崩溃)。保护共享状态(条件谓词)的一致性。wait释放锁和进入等待的原子操作是避免竞态的关键。条件谓词与while应对虚假唤醒、抢先唤醒和广播唤醒的必然要求。本身不存储条件状态,仅提供高效的
多线程编程-线程同步
文章是书籍: 《 Go 并发编程实战 》的读书笔记.
同步, 多线程编程中最重要, 最核心的话题之一. 多进程编程中有了解过同步概念以及相关工具, 比如: 临界区, 原子操作, 互斥量. 本文中再次深入介绍.
就目的来说, 再多线程之间采用同步措施只是为了让他们更好地协同工作或维持共享数据一致性. 多数情况是为了维持共享数据一致性, 但是以协同工作作为控制流管理的程序内同步的意义更大.
在实际场景中, 同步的目的则是两者兼有.
1. 共享数据的一致性
共享数据与一致性保证
包含多线程的程序总是用共享数据这一方式作为线程间传递数据的手段.
共享数据大多用内存空间作为载体(一个进程的很大部分虚拟内存可以被该进程中所有线程共享). 如果两个线程读同一块内存但是读到了不同的数据, 此时会产生错误. 共享数据一致性是一种约定, 只有在该约定成立情况下, 程序中的各个线程执行下去才是正确的.
如何使得共享数据一致性这一约定成立, 是在多线程应用程序设计的时候就要考虑并且确定下来.
一致性保证关系到程序设计方案能够正确实施.
在多线程编程过程中, 总是想方设法保证共享数据一致性, 除非共享数据始终只被同一个线程访问(这还算是共享吗?).
保证共享数据一致性最简单彻底的方法就是让数据称为不变量, 比如 const
常量就是绝对不变量, 其不变性由编程语言保证. 不过肯定不可能把所有值都设计为变量, 比如我们如果需要计数器怎么办?
一般情况下, 需要通过额外手段保证多个线程共享的变量的一致性, 所以有了临界区的概念.
临界区
核心定义
临界区是指访问共享资源(如内存、文件、设备等)的代码段,该区域在并发环境中必须保证互斥访问。当多个线程/进程同时进入临界区时,会导致数据竞争(Race Condition),引发不可预测的结果(如数据损坏、程序崩溃)。
关键特性
- 互斥性:同一时刻仅允许一个执行流访问
- 有限等待:等待进入临界区的线程不应被无限期阻塞
- 无忙等待:理想情况下等待线程不应消耗CPU(可通过阻塞实现)
2. 互斥量
互斥: 同一时刻只允许一个线程处于临界区之内的约束.
任意一个线程进入临界区之前, 都要加锁, 只有成功加锁的线程才允许进入临界区, 否则就阻塞.
互斥量(Mutex) 是"Mutual Exclusion Object"的缩写,是一种专门实现互斥(Mutual Exclusion)的锁, 一种用于多线程编程的核心同步机制。其主要目的是确保在任意时刻只有一个线程可以访问共享资源,从而防止数据竞争和并发冲突。
核心特性:
- 独占所有权:获得互斥量的线程拥有独占访问权
- 阻塞机制:未获得锁的线程会被挂起(而非忙等待)
- 原子性操作:加锁和解锁操作是原子的
- 线程绑定:通常只能由加锁线程解锁(递归锁除外)
互斥量是一种简单高效的手段, 适用于共享数据的绝大部分同步场景. 互斥量的实现会用到机器语言级别的原子操作,且仅在锁定冲突时才会涉及系统调用的执行。这使得互斥量比其他同步方法(比如信号量)的速度要快很多。
互斥量有两种可能状态:
- 已锁定状态
- 未锁定状态
互斥量每次只能加锁一次(已经上过锁的互斥量不能上第二次锁), 如果对于已锁定的互斥量再执行锁定操作, 该操作必定会失败.
成功锁定互斥量的线程称为该互斥量的所有者, 只有互斥量的所有者才可以对其执行解锁操作.
多线程对于同一个互斥量的锁争夺可看作是对该互斥量所有权的争夺. 因此锁定可以看作是对互斥量的获取, 解锁因此可以看作是对互斥量的释放.
注意: 对于同一互斥量的锁定和解锁操作一定要成对出现, 也就是有了加锁也就一定要有解锁, 而且不能多次加锁或者多次解锁, 多次加锁总是会操作很严重的错误.
为了安全合理使用共享数据, 要将操作同一个共享数据的代码放置在一个或者是多个临界区之内, 并且使用同一个互斥量保护他们, 使用互斥量要遵循已有的使用规则, 这涉及到操作系统提供的线程库中的一些函数.
下面是一个示例, 改编自之前多进程部分的计数器例子:
该图展示了两个线程共同使用一个计数器的情况, 使用互斥量作为线程同步的工具.
首先要说明, 互斥量和计数器一样都是共享资源(所有权不只属于单一个线程). 互斥量一定要被所有相关线程都访问到, 所以说代表互斥量的变量和常量一般都是全局变量, 但是为了隐藏实现细节, 要尽量最小化互斥量的访问权限.
然后, 初始化互斥量这一操作, 一定要在任何线程用它之前执行. 经过初始化的互斥量会处在未锁定的状态.
注意: 如果多个线程的代码中包含了对同一个互斥量的初始化操作 那么一定要要保证这个互斥量只会被初始化一次. (操作系统线程库中专门提供了函数来满足这一要求, Go 中也有许多可选的实现方式)
最后看图, 注意到自上而下的第五步操作, 这里线程 B 试图锁定互斥量, 但是由于线程 A 已经持有了互斥量, 但是又不能重复锁定, 所以 B 的锁定操作会失败, 之后 B 将会被阻塞并且进入睡眠状态, 直到 A 解锁互斥量, 此时 B 将会被唤醒并且退出睡眠状态. 被唤醒后 B 会立即再次尝试锁定互斥量, 现在互斥量没有被锁定, 所以 B 成功锁定, 之后 B 开始操作计数器并且处理相关数据.
由于对于互斥量的合理使用, 线程 A 和线程 B 都没有干扰到对方使用计数器, 因此存在在它们之间的竞态条件已经被消除了, 这也就达成了我们使用互斥量的目的.
注意:
- 对于互斥量的初始化一定一定要保证唯一性, 这永远是正确使用互斥量的第一个必要条件.
- 线程在离开临界区时一定一定要及时执行互斥量解锁, 以免造成不必的性能损耗乃至死锁.
- 一般情况下, 应当尽量少地使用互斥量. 每个互斥量保护的临界区都应在合理范围之内而且要尽量大.
- 如果发现多个线程频繁出入某个较大的临界区而且他们之间经常存在访问冲突, 那么就应该把这个较大的临界区切分为较小的临界区, 使用不同的互斥量加以保护. 这一行为是为了让等待进入同一临界区的线程数变少, 进而降低线程被阻塞的概率, 减少他们被迫进入睡眠状态的时间, 进而在一定程度上提高程序总体性能.
死锁
此处线程 A 和 线程 B 分别锁定了互斥量1和互斥量2, 之后二者都在未释放已有锁的情况下尝试锁定另一方的资源, 由于对方资源已被加锁, 此处会一直等待或是轮询, 此时就造成了死锁问题, 整个进程在这一步停止了.
可预见的死锁肯定是要坚决避免的, 在该例子中我们能看得出来, 导致死锁的原因是: 不同的互斥量(锁)保护的临界区之间发生了重叠.那么只要避免这一点, 就能避免因为锁使用不当而导致的死锁问题. 但是由于某些原因我们保证不了, 或者一定要用临界区重叠的多个互斥量要怎么办?
试锁定-回退
第一种方法: 使用操作系统线程库提供的“试锁定-回退”功能.
核心思想在于: 如果在执行一个代码块时需要先后(顺序是不确定的)锁定两个互斥量, 那么在成功锁定其中一个互斥量之后应该用试锁定的方式锁定另一个互斥量. 如果第二个互斥量锁定失败, 那么就把已锁定的第一个互斥量解锁, 等待一段时间之后再重新对这两个互斥量进行锁定和试锁定.
如果需要锁定的互斥量多余两个, 那么总是先锁定其中一个, 然后再按照上面说的锁定其他互斥量并且在必要时刻回退.
此处的试锁定代表的是操作性系统线程库提供的一个函数, 该函数会尝试对一个互斥量进行锁定, 如果锁定失败了, 那么该函数会直接返回一个错误码而不是阻塞(这是避免死锁的关键).
回退这一环节主要是针对这样的情况: 如果试锁定失败, 那么一定说明有其他线程先于当前线程进入了或者说是试图进入这个收多个互斥量保护的代码块. 这时候当前线程就应当放弃争夺资源转而让其他先来的线程执行逻辑, 自己过一会再尝试.
这一方法几乎可应对所有临界区争夺的情况, 但是这种方法也很复杂, 想想对多个互斥量的锁定操作夹杂了其他操作的时候, 此时对多个互斥量锁定的同时还要考虑其他状态的回退, 如果状态特别多, 其他的锁也要回退, 控制流会变的非常混乱, 此时几乎是不可行的.
另外, 使用试锁定和回退方法时, 对多个互斥量解锁的顺序要和锁定它们的顺序完全相反, 特别一定要在最后解锁第一个锁定的互斥量.
固定顺序锁定
第二种方法: 采用“固定顺序锁定”的方式
这种方法不会太影响程序复杂度, 思路是: 在需要先后对多个互斥量锁定的场景下, 总是用固定不变的顺序锁定他们.
对于前边的例子来说就是: 如果线程 A 和线程 B 总是先锁定互斥量1然后再锁定互斥量2, 那么对他们的锁定就一定不会造成死锁. 由于在成功锁定互斥量1之前, 线程永远无法执行对互斥量2的锁定操作, 这样也就避免了他们分别持有另一方想要锁定的互斥量的情况.
另一方面来说1, 2的解锁操作顺序和锁定操作顺序是无关的, 只是取决于具体流程设计即可. 一般情况下需要保证解锁操作的顺序和锁定操作顺序相反是需要保证一个线程在完全离开重叠临界区之前不会有其他同样需要锁定那些互斥量的线程进入. 在这里是不需要的.
总体来说, 前一种方法通用但是让程序更复杂, 后一种方法简单而且使用, 但是会降低程序灵活性, 同时在使用场景方面也有所限制, 比如不能确定线程对多个临界区的访问顺序情况下, 这种方法用不了.
混用这两种方法有时是好选择。比如,在访问顺序可控的临界区上使用“固定顺序锁定”的方法,而在访问顺序不可控或者需要更大灵活性的地方使用“试锁定一 回退”方法。
无论怎样,它们都属于下策,并且都是一种对不优雅或者不得以的补救措施。
记住,保持共享数据的独立性是预防因使用互斥量而导致死锁的最佳方法。如果共享数据之间的关联无法消除,那么尽可能使多个临界区之间没有重叠。仅当这两点都无法得到保证时,才考虑使用“试锁定-回退”和“固定顺序锁定”的方法。
死锁是并发编程中最严重的问题, 也几乎是使用互斥量是要特别注意的唯一问题, 要尽一切避免.
3. 条件变量
除了互斥量之外, 另一种同步方法就是条件变量. 条件变量是并发编程中用于线程间协调的核心同步原语,它允许线程在特定条件不满足时主动休眠,并在条件可能满足时被唤醒. 条件变量作用是在对应的共享数据状态发生变化时, 通知其他因此而被阻塞的线程.
想象一下线程需要等待某个共享数据的状态发生改变(例如:队列从空变为非空,缓冲区从满变为有空位,某个任务完成)。轮询(不断检查)是低效且浪费CPU的。条件变量提供了一种机制,让线程在条件不满足时主动休眠(阻塞),并在条件可能满足时被高效唤醒。
条件变量的三要素
- 互斥锁(Mutex):保护共享数据
- 条件检查(Condition Check):判断是否满足执行条件
- 等待/通知机制(Wait/Signal):协调线程执行
核心目标:高效且安全地等待特定条件
条件变量应该始终与互斥量组合使用. 当线程成功锁定互斥量并访问到共享数据时, 共享数据的状态不一定正好满足需求, 这时候我们通过条件变量同步两个线程的状态, 在未达到条件时始终阻塞, 达到条件后允许继续执行.
-
与互斥锁(Mutex)的共生关系:
- 前提: 对共享数据(尤其是构成“条件”的那部分数据)的任何访问(读/写)必须在互斥锁的保护下进行。这是保证数据一致性的基础。
- 耦合: 条件变量本身并不包含任何状态信息(比如“条件是否满足”)。它仅仅是一个等待/通知的机制。实际的条件状态(例如
queue.empty()
,buffer_full
)是由程序员在共享数据上定义的谓词(Predicate),并且这些数据的访问受关联的互斥锁保护。 - 锁传递: 条件变量的操作(
wait
,signal
,broadcast
)必须在已持有其关联互斥锁的前提下调用(signal
/broadcast
有时可以在不持有时调用,但这依赖于具体实现和编程模型,通常建议持有以保证状态一致性)。最关键的是wait
操作内部会原子地执行两个动作:- 释放关联的互斥锁。 (让其他线程有机会修改共享数据和条件)
- 将调用线程置于等待(阻塞)状态。 (挂起线程,直到被唤醒)
- 当线程被唤醒(通过
signal
或broadcast
)并从wait
调用返回时,在返回之前,wait
操作会重新获取关联的互斥锁。 这保证了线程在检查条件和继续执行时,锁是持有的。
-
关键操作:
wait(mutex)
:- 前提: 线程必须已持有
mutex
。 - 动作(原子操作):
- 原子地释放
mutex
。 - 将线程自身加入该条件变量的等待队列,并阻塞。
- 原子地释放
- 唤醒后: 当线程被唤醒(无论是
signal
,broadcast
还是虚假唤醒),它首先需要重新竞争获取mutex
。一旦成功获取mutex
,wait
调用才返回。线程醒来时并不自动持有锁,它需要重新获取锁。 - 为什么原子性至关重要? 释放锁和进入等待必须是原子的。如果非原子:线程释放锁后,在将自己加入等待队列之前,另一个线程可能获取锁、修改条件并发出信号(此时第一个线程还没在队列里),然后第一个线程才加入队列等待,导致信号丢失。原子操作避免了这种竞态条件。
- 前提: 线程必须已持有
signal()
/notify_one()
:- 前提: 通常建议在持有关联
mutex
的情况下调用(以保证在发出信号时,条件状态是稳定的)。 - 动作: 唤醒该条件变量等待队列中的至少一个线程(具体唤醒哪一个取决于调度策略,通常是FIFO或优先级)。被唤醒的线程会从
wait
中醒来并尝试重新获取锁。 - 关键点:
signal
不保证被唤醒的线程立即运行或立即获得锁。它只是将线程从等待状态移到锁的竞争队列或就绪队列。发出信号的线程通常在signal
后释放锁,给被唤醒线程获取锁的机会。
- 前提: 通常建议在持有关联
broadcast()
/notify_all()
:- 前提: 同样建议在持有
mutex
时调用。 - 动作: 唤醒该条件变量等待队列中的所有线程。所有被唤醒的线程都会从
wait
中醒来并尝试重新竞争获取锁。 - 使用场景: 当条件改变可能对多个等待线程都有效时(例如,生产者放入多个资源到空缓冲区,多个消费者都可能消费;释放一个读写锁,所有等待的读者都应被唤醒)。
- 前提: 同样建议在持有
-
条件谓词(Condition Predicate)与等待循环(Wait Loop):
- 定义: 条件谓词是一个关于共享数据的布尔表达式,它定义了线程等待的“条件满足”状态(例如
count > 0
,!task_queue.empty()
)。 - 为什么
wait
必须放在while
循环中检查条件谓词?- 虚假唤醒(Spurious Wakeup): 即使没有线程调用
signal
或broadcast
,等待的线程也可能被唤醒。这是POSIX标准和许多实现允许的行为,可能源于底层实现细节或性能优化。while
循环强制线程在醒来后重新检查条件是否真正满足。 - 抢先唤醒: 假设条件变量上等待多个线程(如多个消费者)。一个生产者
signal
唤醒一个消费者C1。但在C1获取锁之前,另一个消费者C2可能抢先获取了锁,消费了资源,使条件再次变为假。当C1最终获取锁时,条件已不再满足。while
循环让C1重新进入等待。 - 广播唤醒:
broadcast
唤醒所有等待线程。但可能只有一个线程能真正“消费”掉改变的条件(例如,只有一个资源可用)。其他线程被唤醒后发现条件又不满足了,需要重新等待。while
循环处理这种情况。
- 虚假唤醒(Spurious Wakeup): 即使没有线程调用
- 标准使用模式(范式):
lock(mutex); // 获取保护共享数据和条件的互斥锁 while (!condition_predicate) { // 在循环中检查条件谓词 wait(cond_var, mutex); // 原子地释放mutex并等待。唤醒后自动重新获取mutex } // 此时 condition_predicate 为真,且持有 mutex // ... 执行操作(消费资源、修改状态等) ... unlock(mutex); // 释放锁
- 定义: 条件谓词是一个关于共享数据的布尔表达式,它定义了线程等待的“条件满足”状态(例如
-
信号与广播的选择(Signal vs. Broadcast):
signal
: 当你确信最多只有一个等待线程可以被满足(即条件改变只够让一个线程继续执行),或者唤醒哪一个线程都无关紧要时使用。它避免了不必要的唤醒(惊群效应 - Thundering Herd Problem),减少上下文切换和锁竞争开销。broadcast
: 当你确信多个(甚至所有)等待线程都可能被满足(即条件改变足够让多个线程继续执行),或者你无法确定有多少线程能被满足但需要确保所有可能符合条件的线程都被通知到时使用。典型的例子是读写锁的实现(释放写锁时应唤醒所有等待的读者)。- 原则: 在能满足需求的前提下,优先使用
signal
以减少开销。如果不确定,或者条件改变明显影响所有等待者,使用broadcast
。
-
条件变量的本质与理论意义:
- 同步原语: 它是比互斥锁更高级的同步原语,用于解决基于状态的等待问题。
- 事件通知机制: 提供了一种线程间通信的方式,一个线程可以通知其他线程“某个事件(条件满足)可能发生了”。
- 避免忙等: 核心价值在于让等待的线程休眠,释放CPU资源给其他线程使用,显著提高系统整体效率。
- 解耦等待与检查: 将“等待条件”的操作(
wait
)与“检查条件是否满足”(condition_predicate
)和“修改条件”(在锁保护下修改共享数据后调用signal/broadcast
)清晰地分离,并通过互斥锁保证对共享状态访问的互斥性。
-
关键理论挑战与解决方案:
- 丢失唤醒(Lost Wakeup): 如果在线程A调用
wait
之前,线程B就已经修改了条件并调用了signal
,那么线程A的wait
可能错过这个信号而永远等待下去。- 解决方案: 强制要求对条件的检查和进入
wait
必须在互斥锁的保护下,且使用while
循环检查条件。wait
的原子性(释放锁+入队)确保了:如果signal
发生在wait
调用之前(即线程A检查条件后、调用wait
前),那么线程B的signal
是在线程A释放锁之后发出的(因为线程A持有锁时B无法获取锁修改条件和发信号),此时线程A尚未将自己加入等待队列,信号确实会丢失。但线程A接下来会调用wait
并释放锁阻塞。之后,当另一个线程再次满足条件并发出signal
时,线程A会被唤醒。关键在于while
循环保证了即使信号丢失(因为发生在wait
之前),线程A在wait
返回后(可能是被后来的信号唤醒)会重新检查条件。如果条件在signal
之后又被其他线程改回了假,线程A会再次wait
。如果条件在signal
之后一直为真,线程A就能通过循环检查。互斥锁 +while
循环 +wait
的原子性 共同解决了丢失唤醒问题。
- 解决方案: 强制要求对条件的检查和进入
- 虚假唤醒(Spurious Wakeup): 如前所述,
while
循环是应对虚假唤醒的标准解决方案。 - 优先级反转: 与互斥锁类似,如果高优先级线程等待一个被低优先级线程持有的锁(该锁在
wait
时释放,但唤醒后需要重新获取),而低优先级线程又被中优先级线程抢占,可能导致高优先级线程无限期等待。优先级继承(Priority Inheritance)或优先级天花板(Priority Ceiling)协议可以缓解。
- 丢失唤醒(Lost Wakeup): 如果在线程A调用
-
与信号量(Semaphore)的对比:
- 状态: 信号量本身维护一个计数值(状态)。条件变量没有内部状态,状态完全由外部共享数据和条件谓词定义。
- 操作: 信号量的
wait
§ 和signal
(V) 操作直接改变其内部计数值。条件变量的wait
和signal
不直接改变外部状态,只控制线程的阻塞/唤醒。 - 用途: 信号量天然适合管理有限数量的资源(如计数信号量)或简单的互斥(二元信号量)。条件变量适合等待基于共享数据复杂状态的条件成立。
- 灵活性: 条件变量+互斥锁+条件谓词的模式比信号量更灵活,可以表达更复杂的同步条件(例如等待“队列非空且结果有效”)。信号量通常表达更简单的“资源可用数量”。
- 唤醒机制: 信号量的
V
操作总是增加计数并可能唤醒一个等待者(如果计数变正)。条件变量的signal
不改变任何状态,只唤醒等待者,等待者醒来后必须检查外部状态。
总结
条件变量是一种强大的线程同步机制,其理论基础建立在:
- 与互斥锁的强制耦合: 保护共享状态(条件谓词)的一致性。
wait
的原子性: 释放锁和进入等待的原子操作是避免竞态的关键。- 条件谓词与
while
循环: 应对虚假唤醒、抢先唤醒和广播唤醒的必然要求。 - 无状态的通知机制: 本身不存储条件状态,仅提供高效的阻塞/唤醒功能。
signal
与broadcast
的语义区别: 根据唤醒需求选择,平衡效率与正确性。
理解条件变量的关键在于深刻领会“条件”存在于共享数据中(由互斥锁保护),而条件变量只是提供了一种让线程安全、高效地等待该条件变化并得到通知的机制。wait
操作内部的锁释放/获取原子性以及醒来后必须使用 while
循环重新检查条件是正确使用条件变量的核心要点。
编写多线程程序的几个注意点
控制临界区的纯度
临界区中应当仅仅包含操作共享数据的代码. 也就是, 不要把无关的代码包含在其中, 特别是各种相对耗时的 I/O 操作. 夹杂无关的代码只会让临界区界限模糊而且会影响程序的运行性能,
控制临界区的粒度
粒度过细的临界区会增加底层协调工作发生次数, 所以粗化临界区也是有必要的.
如果存在相邻的多个临界区, 而且他们内部都是操作同一个共享数据的代码, 那么就把他们合并,.
如果在他们之间夹杂了无关代码,那么就调整这些代码的位置, 把他们放在合并之后的临界区的前面或后面. 如果实在无法调整, 就在临界区的纯度和粒度之间权衡.
简单来说就是: 如果没有长耗时的无关代码, 那么就合并他们, 否则只能放弃合并, 总之要优先考虑减少粒度过细的临界区.
减少临界区中代码的执行耗时
提高临界区的纯度可以减少临界区中代码的执行耗时。但是,如果操作共享数据的代码本身执行起来就很耗时,那又该怎么办呢? 这分为两种情况:
- 临界区中包含了对几个共享数据的操作代码. 此时, 无论这些操作不同共享数据的代码之间是否存在强关联, 都可以考虑把他们拆分到不同临界区中, 使用不同同步方法保护起来. 这里不存在粒度过细问题, 因为他们针对的是不同的共享数据, 不过这时候也要注意不要因为使用互斥量不当而造成死锁.
- 临界区中仅包含了操作同一个共享数据的代码. 此时往往不能通过调整临界区的方式达到减少耗时的目的, 因为粒度过细的临界区反而会增加额外时间消耗, 所以正确做法是检查其中的业务逻辑和算法并且加以改进.
避免长时间持有互斥量
线程长时间持有某个互斥量所带来的危害相当明显.
同样明显的是, 在减少临界区中代码的执行耗时的同时可以减少线程持有相应互斥量的时间.
不过, 有时使用条件变量同样可以起到很好的作用. 条件变量会适时地对互斥量进行解锁和锁定, 所以线程持有互斥量的时间会明显减少. 在临界区代码等待共享数据的某个状态的情况下, 使用条件变量一般都会有很好的效果.
优先使用原子操作而不是互斥量
这样做的理由是: 使用互斥量一般会比使用原子操作造成大得多的程序性能损耗。并且,随着CPU核心数量的增加,这一差距会被进一步拉大。
当共享数据的结构非常简单(比如数值类型)时,建议使用原子操作来代替附加了互斥量操作的代码。
原子操作会直接利用硬件级别的原语来保证操作成功和数据的并发安全。不过,遗憾的是,对于结构稍复杂一 些的共享数据,原子操作就无能为力了。
因此,这条建议的适用范围比较有限。
总结
以上建议的最终目的都是在不是去程序正确性的前体现最大限度提高程序的可伸缩性.
提高程序可伸缩性的原因在于: 相较于通过优化程序的方式提升性能来说, 为运行它的计算机添加更多的 CPU 核心(或者更换一台拥有更多 CPU 核心的计算机)一般来说更直接, 简单, 甚至是更廉价.
使用多进程以及多线程编程是另一个有力手段, 但是这需要更好的编程人员, 一般来说, 人员支持或者条件不充分情况下, 增强计算机硬件的性能通常是首选, 这也正是云计算和弹性计算火热的主要原因之一.
应当尽量让施加在并发程序上的 软提升 和 硬提升 在效果上产生叠加而不是抵消. 在多核时代, 怎样更好地利用并行计算提升程序运行性能已经是一个程序员的必备技术.
升性能来说, 为运行它的计算机添加更多的 CPU 核心(或者更换一台拥有更多 CPU 核心的计算机)一般来说更直接, 简单, 甚至是更廉价.
使用多进程以及多线程编程是另一个有力手段, 但是这需要更好的编程人员, 一般来说, 人员支持或者条件不充分情况下, 增强计算机硬件的性能通常是首选, 这也正是云计算和弹性计算火热的主要原因之一.
应当尽量让施加在并发程序上的 软提升 和 硬提升 在效果上产生叠加而不是抵消. 在多核时代, 怎样更好地利用并行计算提升程序运行性能已经是一个程序员的必备技术.
更多推荐
所有评论(0)