并发控制的本质是协调多个线程,在需要的时候适当的协调线程的执行顺序,使得结果具有正确性。
此处所介绍的并发主要是关于互斥和同步。其所解决的问题则在 concurrent-bug 一节中进行介绍。
此处照本宣科的介绍几种概念。
临界区(critical section):表示一段代码段,同一时间只能有一个线程访问进入该代码段。这里明确的限制了数量为一个。
互斥访问:就是描述对临界区进行访问的行为。互斥强调多个线程对同一临界区的访问控制,同一时间只能有一个线程进入临界区,不强调线程进入顺序。一般通过锁实现,这种实现下,不关心到底是哪个线程持有了锁,进入了临界区,重点是只有限制数量的线程进入了临界区。
同步:是协调多个线程之间执行顺序的行为。强调多个线程在并发执行时的先后顺序,例如:线程 A 在执行 a 操作前,一定要在线程 B 执行完 b 操作之后。从这个角度看,互斥本身也是一种同步:A 线程先进入临界区,所有非 A 线程一定要在非 A 线程退出临界区之后再进入临界区。
锁:是一种实现互斥访问的方法。锁作为一个资源实体,代表着一段临界区资源。只有持有这把锁的线程,才能访问临界区。
semaphore:代表一部分资源,当资源量不为 0 时,即可继续执行。
条件变量:一种同步的方法,通过 wait/notify 操作,协调线程之间执行的先后次序。
下面说说自己的理解。
理论上锁和 semaphore,两者似乎完全一样 ,不同的地方在于,(锁只有一个?)限制同一时间只有一个线程进行访问。而 semaphore 则可以持有给定数量的资源,只要资源不为 0,都可以继续执行。线程获取到锁之后,这把锁必须由它自己释放。而信号量不一定非要由当前线程进行释放。
但是实践上,lock 和 semaphore 之间是比较模糊的。lock 又分为乐观锁,排他锁或者种种分类,同一时间可以有多个线程持有锁。(读写锁)。在这个意义上,semaphore 和锁似乎是一样的。比如 java juc 中 semaphore 和 readwriteLock 以及 reentrantlock 都是基于 AQS 实现的。
也有观点将进程间的锁和进程内的锁进行区分,我认为这引入了不必要的复杂性。
而条件变量意义比较明确。持有同一个条件变量,任何一个 等待 wait 的条件变量,是在等待一个条件变量的 notify 之后才能继续执行。是语义明确的等待某事发生。
条件变量的条件是抽象的,比如 queueNotEmpty 这个条件,等待这个条件,只是在语义上判断并等待,而条件变量本身可能并不需要关心。这取决于实现。
notEmpty.await{ queueSize > 0 }
临界区也是一种资源,那么也可以通过 semaphore 来表示。
语义区分:
从定义上来看,条件变量一次 await 线程就暂停,必须等待到下一个通知,通知过去就没了,既然当前线程已经暂停,那么必然只能依靠其它线程进行 signal。
而 lock 和 semaphore 的语义是抢夺资源(锁),一定有线程能得到。抢到之后继续执行,之后由抢到的线程将其释放。
但有观点认为, 锁只能由拿到锁的线程只能到锁的线程进行释放。而信号量可由其他线程进行释放。我认为是有点不正确的。
concurrency - What is the difference between lock, mutex and semaphore? - Stack Overflow
在实践上,条件变量通常会和锁绑定。但是锁并不锁定的对条件变量的访问,而是对共享资源的访问?c++ - Why does a condition variable need a lock (and therefore also a mutex) - Stack Overflow
c - Why do pthreads’ condition variable functions require a mutex? - Stack Overflow
有观点认为锁是用来保证条件数据的,另外的观点认为锁是用来保护条件变量的。
锁可以保证,条件变量的 signal 和 await 操作是原子的。
semaphore 则不需要和锁绑定,当 semaphore 和锁一起使用的时候,锁锁定的可能是共享数据,比如 queue。但是一个线程安全的 queue,就不再需要额外的锁。
实践上,如果条件变量不与锁进行绑定。就有可能出现,先 notify 再 wait 的操作,wait 等待一个还没有发生的事件。
但是在 juc 中,其实先 notify 再 wait 的操作是可行的?park 和 unpark?
semaphore 信号量#
信号量
同步则是在互斥的基础上,
信号量控制了一个等待队列,信号量持资源的数量
但是临界区是一种资源吗?
obviously 是的
那么临界区的问题是什么呢?
semaphore 的问题是什么
在我所见过的大多数教程中,互斥访问是
Semaphore#
semaphore 有等待队列,有 wait, 也有 signal,但是从语义上来说是可以实现临界区的互斥访问的。但是 semaphore 一般会在同步的章节进行介绍。
锁#
同步#
锁和同步主要目的是保证批量操作原子性,(底层实现会同时保证顺序性和可见性)。
lock
锁的目的是让临界区限额,不记名,不关系具体是哪个线程获得了锁。只要保证只有一个线程获得了锁就可以。
同步的目的是由一个线程通知另外一个线程,指明了具有某种条件可以继续执行。
条件变量,每个条件变量关联一个锁,是因为我们不能等待已经发生的事情。
如果没有锁,直接 await 和 notify,如果先执行 notify,那么 await 就是无意义的。
等待条件成立