concurrent

    1591
    最后修改于

    并发控制的本质是协调多个线程,在需要的时候适当的协调线程的执行顺序,使得结果具有正确性。

    此处所介绍的并发主要是关于互斥和同步。其所解决的问题则在 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 这个条件,等待这个条件,只是在语义上判断并等待,而条件变量本身可能并不需要关心。这取决于实现。

    kotlin
    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 就是无意义的。
    等待条件成立

    • 🥳0
    • 👍0
    • 💩0
    • 🤩0