并发编程01-面试中被问到并发基础知识答不上来?



由于计算机中 CPU 的技术问题,在发展到一定程度的时候越来越难取得重大突破(比如从60分考到90要比从99分考到100分容易)。但是对于计算的需求量一直在增加,既然单核心 CPU 的工作能力不足以支持如此大量的运算任务,那么制造厂商就开始做横向的扩展,给一台计算机安装多核心 CPU。在同一个时刻每个核心都可以执行运算。这样提高了计算机的整体性能和工作效率,但是也引入了并发问题。


进程交互有关于并行程序能够借此相互通信的机制。最常见的交互形式是共享内存和消息传递。

共享内存 (shared memory)

共享内存在硬件术语中指在多处理器的计算机系统中,可以被不CPU 访问的大容量内存。共享内存在软件术语中指的是可以被多个进程存取的内存,一个进程是一段程序的单个运行实例。在这种情况下,共享内存被用作进程间的通讯。

共享内存是进程间传递数据的高效方式。在共享内存模型中,并行进程共享它们可以异步读写的全局地址空间。异步并发访问可能导致竞态条件,和用来避免它们的机制比如:锁、信号量、监视器。常规的多核处理器直接支持共享内存,很多编程语言和库在设计上利用了它。

消息传递 (message passing)
在消息传递模型中,并行进程通过消息传递相互交换数据,这种通信可以是异步的,消息可以在接收者准备好之前发出。或是同步的,消息发出前接收者必须做好准备。



同步 synchronization
在计算机科学中同步是指两个不同但有联系的概念:
1. 进程同步:指的是多个进程在特定点汇合 (join up) 或者握手使得达成协议或者使得操作序列有序。
2. 数据同步:指的是一个数据的多分拷贝一致以维护完整性。

临界区 critical section

多个线程或进程要执行同一个特定的不可重入的程序代码块(称为临界区,英文 critical section)。



竞态条件
竞争冒险(race hazard)又名竞态条件、竞争条件(race condition),它旨在描述一个系统或者进程的输出依赖于不受控制的事件出现的顺序或者出现的时机。此词源自于两个信号试着彼此竞争,来影响谁先输出。举例来说,如果计算机中的两个进程同时试图修改该一个共享内存的内容,在没有并发控制的情况下,最后结果依赖于两个进程的执行顺序和写入时机。而如果发生了并发访问冲突,最后的结果是不正确的。

Monitors
管程也叫监控器,是一种程序结构。结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件或一些变量。管程实现了在一个时间点,最多只能有一个线程在执行管程的某个子程序。与哪些通过修改该数据结构实现互斥访问的并发程序设计相比,管程很大程度上简化了程序设计。一个管程包括:
1. 多个彼此可以交互并共享资源的线程。
2. 多个与资源使用有关的变量。
3. 一个互斥锁。
4. 一个用来避免竞态条件的不变量。

一个管程的程序在运行一个线程前会先获取互斥锁,直到完成线程或是线程等待某个条件被满足才会放弃互斥锁。若每个执行中的线程在放弃互斥锁之前都能保持不变量的成立,则所有的线程皆不会导致竞态条件成立。当一个线程执行管程中的一个子程序时,称为占用(occupy)该管程。管程的实现确保了在一个时间点,最多只有一个线程占用了该管程。这是管程的互斥访问性质。当线程要调用一个定义在管程中的子程序时,必须等到已经没有其他线程在执行管程中的某个子程序。在管程的简单实现中,编译器为每个管程对象自动加入了一把私有的互斥锁。该互斥锁初始状态为解锁,在管程的每个公共子程序的入口给该互斥锁枷锁,在管程的每个公共子程序的出口给该互斥锁解锁。

条件变量 (Condition Variable)
对于许多应用场合,互操作是不够用的。线程可能需要等待某个条件P为真,才能继续执行。解决办法是条件变量(Condition Variable)。概念上一个条件变量就是一个线程队列(queue),其中的线程正等待某个条件变为真。每个条件变量 c 都关联着一个断言 Pc .当一个线程等待一个条件变量,该线程不算占用了该监控器,因而其他线程也可以进入到该监控器执行,改变监控器的状态,通知条件变量c其关联的断言 Pc 在当前状态下为真。

因此对条件变量存在两种主要操作:
wait c 被一个线程调用,以等待断言 Pc 被满足后该线程可恢复执行。线程挂在该条件变量上
等待时,不被认为是占用了监控器。
signal c (有时也写作 notify c) 被一个线程调用,以指出断言 Pc 现在为真。
当一个通知(signal)发给了一个有线程处于等待中的条件变量,则有至少两个线程将要占用该
监控器:
1. 发出通知的线程。
2. 等待条件变量断言的某个线程。

条件变量的两种实现:
1.阻塞式条件变量 (Blocking Condition Variable) ,把优先级给了被通知线程。发出通知(signaling) 的线程必须等待被通知(signaled)的线程放弃占用监控器。这种方式也被称为通知且急迫等待(signal-and-urgent-wait)管程。

2.非阻塞式条件变量 (Nonblocking Condition Variable) , 把优先级给了发出通知的线程。也被称为通知且继续(signal-and-continue)条件变量。发出通知的线程不会失去监视器的占用权。被通知的线程将会被移入到一个准备争抢监视器的队列。非阻塞式条件变量经常把 signal 操作称作 notify ,也常用 notify all 操作把该条件变量关联的队列上的所有线程移入准备争抢监视器的队列。

Spurious wakeup
假唤醒是 POSIX Threads 与 Windows API 使用条件变量时可能发生的复杂情形。一个挂在条件变量上的线程被 signalted ,正在等待条件仍有可能是不成立的。假唤醒指的是即使没有线程 signalted 该条件变量,挂在该条件变量上的线程却被唤醒。因此,应该用 while 循环包围条件变量等待操作。

stolen wakeups
被偷走的唤醒是 POSIX Threads 与 Windows API使用条件变量时,线程调用 g_cond_signal 时,另一个线程已经获取了 mutex 使得期望的条件不再满足,因此被唤醒的线程面临着条件不成立。因此应该用 while 循环包围条件变量等待操作。

互斥锁(Mutual exclusion,缩写Mutex)
互斥锁是一种用于多线程语言编程中,防止两条线程同时对同一公共资源(比如全局共享变量)进行读写机制。该目的通过将代码切分成一个一个的临界区域(critical section)达成。临界区域指的是一块对公共资源进行访问的代码,并不是一种机制或算法。一个程序、进程、线程可以拥有多个临界区域,但是并不一定会应用互斥锁。

需求:
1.不准永远耽搁一个要求进入临界区域的线程,造成死锁或是饥饿发生。
2.若没有任何线程处于临界区域时,任何要求进入临界区域的线程必须立刻得到允许。
3.不能对线程的相对速度与处理器的数目做任何假设。
4.线程只能在临界区域内停留有限的时间。
5.任何时间只允许一个线程在临界区运行。
6.在临界区通知的线程,不准影响其他线程运行。

乱序执行
计算机工程领域,乱序执行 (错序执行 out-of-order-execution ,简称 OoOE 或 OOE)是一种应用在高性能微处理器中来利用指令周期以避免特定类型的延迟消耗范式。在这种范式中,处理器在一个由输入数据可用性所决定的顺序中执行指令,而不是由程序的原视数据所决定。在这种方式下,可以避免因为获取吓一跳程序指令所引发的处理器等待,取而代之的处理下一条可以立即执行的指令。

CAS
比较并交换 (compare and swap , CAS)是原子操作的一种。可用于在多线程编程中实现不被打断的数据交换操作 从而避免在多线程同时修改某一个数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致的问题。该操作通过将内存中的数据替换为新的值。CAS 操作基本 CPU 提供的原子操作指令实现。对于 x86 处理器,可通过在汇编指令前缀增加 LOCK 前缀来锁定系统总线,使系统总线在汇编指令执行时无法访问相应的内存地址。而各个编译器根据这个特点实现了各自的原子操作函数。比如,C语言 C11 的头文件 。由 GNU 提供了对应的 _sync 系列函数完成原子操作。C++ 11 , STL 提供了 atomic 系列函数。Java ,sun.misc.Unsafe 提供了 compareAndSwap 系列函数。

自旋锁
自旋锁是计算机科学用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这个过程中一直保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直到显示释放自旋锁。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。因此OS的实现在很多地方往往使用自旋锁。显然,单核 CPU 不适合使用自旋锁,这里的单核指的是单核单线程的 CPU 。因为在同一时间只有一个线程是出于运行状态,假设运行线程 A 发现无法获取锁,只能等待解锁,但因为 A 自身不挂起,所以那个持有锁的线程 B 没有办法进入运行状态,只能等到 OS 分配给 A 的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。获取、释放自旋锁,实际上是读写自旋锁的存储内存或寄存器。因此这种读写操作必须是原子的。通常用 test-and-set 等原子操作实现。

死锁 deadlock
P1 , P2 两个 process 都需要资源才能继续运行。P1 拥有资源 R2 还需要额外资源 R1 才能运行。P2 拥有资源 R1 还需要额外资源 R2 才能运行。两边都在互相等待对方放弃自己持有的资源而没有一个可以执行。

当两个以上的计算单元,双方都在等待对方停止运行,以获取系统资源,但是没有一方提前退出时,就称为死锁。在多任务操作系统中,当一个或者多个进程等待系统资源,而资源又被进程本身或其他进程占用时,就形成了死锁。如果系统中只有一个进程,当然不会出现死锁。如果每个进程仅需求一种系统资源,也不会产生死锁。死锁的四个条件:
1.禁止抢占(no preemption):系统资源不能被强制从一个进程中退出。
2.持有和等待(hold and wait):一个进程可以在等待时持有系统资源。
3.互斥(mutual exclusion):资源只能同时分配给一个行程,无法多个行程共享。
4.循环等待(circular waiting):一系列进程互相持有其他进程所需要的资源。
死锁只有在四个条件同时满足时发生,预防死锁必须至少破坏其中一项。

活锁 livelock
活锁与死锁类似,死锁是进程都在等待对方先释放资源。活锁则是进程彼此释放资源又同时占用对方释放的资源,当此情况持续发生时,尽管资源的状态不断改变,但是每个进程都无法获取所需要的资源,使得事情没有任何进展。举个例子:
死锁:两个人在同一条道路上互不相让,都在等待对方先让开。活锁:两个人在同一条道路上,互相让路,但是在让路的时候都恰好站到了同一侧,再次让开,又站到了同一侧。同样的状况不断的重复下去导致双方都无法通过。


相关内容回顾

《带你了解缓存一致性协议 MESI》

讲解 CPU 缓存一致性原理


《内存屏障究竟是个什么鬼?》

内存屏障原理...


《每日阅读之计算机系统总线》

理解系统总线是如何工作的...


《走进 Java Volatile 关键字》

Java Volatile 关键字举例串联往期内容知识...








本文分享自微信公众号 - 黑帽子技术(SNJYYNJY2020)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章