目录
1. POSIX信号量
1.1. 信号量的接口介绍:
1.1.1. sem_init (初始化信号量)
1.1.2. sem_destroy (销毁信号量)
1.1.3. sem_wait (等待信号量) P操作
1.1.4. sem_post (发布信号量) V操作
1.2. 信号量的理解:
1.3. 信号量demo
1.3.1. 单生产单消费
1.3.2. 多生产多消费
1.3.3. 多生产多消费的意义在哪里?
1.3.4. 信号量本质是一把计数器,那么该计数器的意义是什么?
1. POSIX信号量
POSIX信号量和System V信号量作用相同,都是用于同步操作,达到无冲突的访问资源共享的目的。 单POSIX可以用于线程间同步。
我们已经对互斥锁、条件变量有了一定的理解。
在以前的多线程场景下,共享资源是被整体使用的,虽然我们可以通过互斥锁保证临界资源的安全性以及通过条件变量保证访问临界资源合理性的问题,但是,由于锁的特性,导致多执行流访问临界资源是串行执行的,一定程度上抑制了多线程的并发能力。
假设一种场景,临界资源可以被分为若干个子资源,那么我们可不可以通过一定的方式让不同的线程访问不同的子资源,使多线程并发访问临界资源呢?
答案是:可以。这种方式就是通过信号量来实现。
1.1. 信号量的接口介绍:
1.1.1. sem_init (初始化信号量)
man 3 sem_init
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
Link with -pthread.
参数解释:
sem_t 是 POSIX 标准中定义的信号量类型。
pshared: 值为0时,该信号量将被线程共享;值为非0时,该信号量将被进程共享。
value: 表示信号量的初始值。
返回值:成功,返回0;失败,返回相应的错误码 (errno)。
1.1.2. sem_destroy (销毁信号量)
man 3 sem_destroy
#include <semaphore.h>
int sem_destroy(sem_t *sem);
Link with -pthread.
在使用信号量前,通常会调用函数 sem_init 来初始化信号量,并在不需要使用信号量时调用 sem_destroy 函数来销毁信号量。
1.1.3. sem_wait (等待信号量) P操作
man 3 sem_wait
#include <semaphore.h>
int sem_wait(sem_t *sem);
Link with -pthread.
在调用 sem_wait 函数时,它会尝试对指定的信号量进行等待操作。
如果当前信号量的值大于 0,sem_wait 函数会将该值减一并立即返回;
如果当前信号量的值为 0,sem_wait 函数会阻塞当前线程,直到信号量的值变为大于 0 为止,然后将该值减一并返回。
补充:
int sem_trywait(sem_t *sem);
sem_trywait 函数尝试对信号量进行等待操作,与 sem_wait 不同的是:
如果信号量的值大于 0,sem_trywait 会将该值减一并立即返回;
如果信号量的值为 0,sem_trywait 不会阻塞线程,而是立即返回一个错误码。
如果信号量的值为 0,sem_trywait 返回的错误码是 EAGAIN(资源暂时不可用),表示当前无法获取信号量并立即返回,而不会阻塞线程。这样可以在不需要阻塞线程的情况下快速检查信号量的状态
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
sem_timedwait 函数也用于等待信号量达到可用状态,但与 sem_wait 不同的是,sem_timedwait 允许设定一个超时时间,如果在超时时间内信号量仍然未可用,函数将返回一个错误码。
通过指定超时时间,可以避免线程永久阻塞,即使信号量的值一直未达到可用状态。如果在超时时间内获取到了信号量,sem_timedwait 函数会将信号量的值减一并返回成功;如果超时时间到达而未获取到信号量,函数会返回错误码表示超时。
1.1.4. sem_post (发布信号量) V操作
man 3 sem_post
#include <semaphore.h>
int sem_post(sem_t *sem);
Link with -pthread.
sem_post 函数用于增加信号量的值;
当调用 sem_post 函数时,有两种情况:
如果有其他线程在等待该信号量,它将使其中的一个等待线程继续执行;
如果没有线程在等待该信号量,它仅仅是简单地增加该信号量的值。
这种机制使得线程之间可以通过信号量来同步和控制资源的访问。
1.2. 信号量的理解:
首先,什么叫做临界资源呢?
在多线程场景下,当一个资源被多个执行流共享时,通过某种方式让该资源在任意一个时刻只能被一个执行流访问,那么该资源我们称之为临界资源。
而为了保证临界资源的安全性,我们以前是通过互斥量解决的,但是,此时的临界资源时被整体使用的。
因此,如果一个共享资源可以被分为若干个子资源,我们就可以让不同的执行流并发访问不同的临界资源,进而提高了多线程的并发能力。只有当访问同一个子资源时,我们再进行同步或者互斥操作。
可是问题就来了
1、 线程怎么知道一共有多少个资源? 还剩多少个资源呢?
2、 线程怎么知道它一定可以获得某个资源呢?
为了解决上面的问题,我们首先谈谈电影院的例子。
一个要进入电影院看电影的人,是不是这个人真正坐到电影院某个位置上,就代表着这场电影中这个座位是你的呢? 答案:不是。
实际上,看电影首先要买票,而票上对应的座位才是属于你的 (在这场电影期间),哪怕你没有去看,这个座位依旧是属于你的 (在这场电影放映期间)。
换言之,买票的本质:对座位的预定机制,而座位就是一种资源,即对资源的预定机制。
因此,信号量的本质:其是一个计数器。这个计数器的值就代表着当前资源的个数。因此,线程可以通过信号量的值确定现在一共有多少个资源,还剩多少个资源。
那么线程怎么确定它一定能获得某个资源呢?
访问临界资源的时候,必须先申请信号量资源 (进行sem--操作,预定资源, P操作), 使用完毕信号量资源 (sem++,释放资源, V操作)。
因此线程可以通过申请信号量的方式确定自己能否获得某个临界资源,只要信号量满足条件,那么该线程一定可以获得某个临界资源。
因此,我们对信号量的总结就是:
信号量是一个计数器,这个计数器是描述临界资源中子资源的数目;
信号量这种机制本质上就是对临界资源当中特定的子资源进行预定的机制。
1.3. 信号量demo
上面说了信号量的操作以及概念,但我们依旧需要用代码来理解信号量,将采用单生成单消费和多生产多消费两个demo来说明信号量。
1.3.1. 单生产单消费
在条件变量中,我们已经实现了阻塞队列式的生产者消费者模型,而今天,我们将采用环形队列式的生产者消费者模型。
分析过程:
在学习队列中,我们已经学习了它的实现,我就不一一赘述了,但要说明一点:
环形队列的一个重要问题就是,如何解决判空和判慢的问题;
我们会发现,环形队列的判空和判慢条件是一致的,因此为了解决该问题,我们以前就提出两种方案:
其一:牺牲一个存储单元。
在循环队列中,为了区分队列为空和队列为满的情况,我们可以牺牲一个存储单元,即队列中的一个位置不存储数据,这样队列中的元素个数就比队列的最大容量少一。具体实现方式是:
判空:当队头位置等于队尾位置时,队列为空。
判满:当队尾位置的下一个位置等于队头位置时,队列为满。
其二:使用一个计数器。
另一种解决方案是使用一个计数器来记录队列中元素的个数。具体步骤如下:
维护一个计数器count,初始值为0。
入队时,每次将元素入队后,count加一。
出队时,每次将元素除队后,count减一。
判空:当count为0时,队列为空。
判满:当count等于队列的最大容量时,队列为满。
上面就是我们以前的解决思路,当然,我们今天的主题就不能再是这些了,我们今天的主题可是用信号量来解决这些问题的。
场景如下:
一个消费者线程,一个生产者线程;
假设该环形队列一共有N个存储单元。
一个生产位置,一个消费位置。
最开始,生产位置和消费位置都是最开始的地方 (此时环形队列为空)。
假设消费线程不消费数据,生产者一直在生产,当生产满了, 生产线程回到最开始的位置。
对于环形队列而言,如果生产位置和消费位置一致,那么我们认为该队列为空。
但是我们又发现,如果生产线程回到了最开始的位置,且和消费位置一致,那么也有可能该环形队列满了。
因此,此时就会出现问题 。
如图所示:
首先,该环形队列是生产者生产数据的场所,也是消费者消费数据的场所,换言之,该环形队列是生产者线程、消费者线程共享的资源。因此,我们必然要考虑该资源的安全问题 (互斥问题),以及执行流访问临界资源合理性的问题 (同步问题)。
当环形队列如果为空,我们希望消费者线程阻塞,而非继续执行 (因为没有数据资源可以被消费)。
同理,当环形队列为满时,我们希望生产者线程阻塞 (因为此时没有空间资源可以被用来生产数据)。
当消费位置和生产位置一致,在以前我们是通过 (a. 计数器 b. 牺牲一个存储单元)来解决的,但今天,在以环形队列为基础的生产者消费者模型中,我们发现,即使消费位置和生产位置一致,但此时生产者线程和消费者线程的行为是不一致的。
如果此时队列为满,那么生产者线程会被挂起阻塞;
如果此时队列为空,那么消费者线程会被挂起阻塞。
那么,如果生产者线程和消费者线程指向了环形结构的同一个位置 (那么此时该队列一定为空 or 为满):此时生产和消费就要有互斥或者同步问题。
如何体现互斥与同步呢?
当环形队列为空的时候,那么此时只有生产者线程可以执行,消费者线程不可被运行,应被挂起阻塞。
当环形队列满的时候,那么此时只有消费者线程可以被执行,生产者线程应被挂起阻塞。
我们知道,当生产位置和消费位置一致时,这种情况,是少数情况,换言之,大部分情况下,生产位置和消费位置不一致 (环形队列不为满也不为空),也就是说,此时环形队列既有空间资源,也还有数据资源,可以供消费者线程和生产者线程同时被调度,进而提高了多线程的并发能力。
结论:
当生产和消费指向同一个位置,具有互斥同步关系就可以了。
而当生产和消费不指向同一个位置, 此时消费者线程和生产者线程可以并发执行
因此我们编码的期望就是:
生产者不能将消费者套圈。换言之,生产者生产数据时,最多将环形队列填满,满了之后,就不能再继续生产,因为此时该环形队列的空间资源已经为0了 (无法成功申请空间资源信号量)。
消费者不能超过生产者。当消费者消费的是生产者位置的数据时,此时,这个环形队列的数据资源实际上已经为0了,如果越过生产者位置继续消费,那么消费者很有可能获得的就是垃圾数据或者陈旧数据,因此,我们要保证消费者不能超过生产者。
我们发现,当生产位置和消费位置一致时,只有两种情况:
其一:当环形队列为空时,一定要先让生产者线程先运行,消费者线程应被阻塞。
其二:当环形队列为满时,一定要先让消费者线程先运行,生产者线程应被阻塞。
其他情况下:生产者线程和消费者线程可以并发访问,因为此时既有空间资源也有数据资源。
上面如何编码实现呢?
从上面我们也可以看出,再循环队列中存在着两种资源:空间资源,数据资源。
生产者线程: 最关注的是环形队列的空间资源。简而言之,环形队列中还剩多少空间供我生产数据。 我们用信号量 _SpaceSem表示;那么该信号量的起始值是多少呢?就是环形队列的大小。
消费者线程:最关注的是环形队列中的数据资源。即还有多少资源供我消费?我们用信号量DataSem 表示,该信号量的起始值是0。
接下来我们要分析,生产 (push) 和消费 (pop) 这两个过程:
生产:第一步不是直接去生产数据,而是应该首先申请空间资源信号量 (看是否有空间资源供生产者生产数据),也就是申请信号量 P(_SpaceSem),即_SpaceSem--,当申请成功后,我们就可以在特定位置生产数据 (这个特定位置后面解释)。当生产完数据后,空间资源并没有发生改变 (因为申请空间信号量本质上就是--了空间信号量),但是数据资源却增多了一个。以因此我们要进行 V(_DataSem),即_DataSem++;
同理,
消费:第一步也不是直接去消费环形队列中的数据,而是应该首先申请数据资源信号量 (看是否有数据资源供消费者消费),也就是申请数据信号量 P(_DataSem),即_DataSem--,当申请成功后,我们就可以消费特定的数据,当我拿走该数据后,此时该位置上的数据就已经成为无效数据了,因此空间信号量应该++;进行 V(_SpaceSem);
当有了上面的理解后,我们就可以理解下面的场景了:
生产者线程和消费者线程,最开始,环形队列没有数据;
如果消费者线程先被CPU调度,消费者线程要消费数据,那么首先要申请信号量 (数据资源信号量),但是此时数据资源信号量为0,因此无法申请成功,导致消费者线程自身被挂起。那么CPU接下来就开始调度生产者线程,生产者要生产数据,因此要先申请空间资源信号量,申请成功,在特定位置生产数据。
假设此时消费者线程不来消费数据,生产者继续生产,当生产者生产到了消费者的位置,那么也就是说,此时的环形队列已经满了 (空间资源为0,数据资源为环形队列的大小),那么如果此时CPU先调度生产者线程,由于生产首先要申请信号量 (空间资源信号量),可是此时条件不满足,因此生产者线程自身会被挂起阻塞。接下来,CPU就会顺理成章的调度消费者线程 ,因此要先申请数据资源信号量,申请成功,消费特定数据。
因此,如果生产位置和消费位置一致,但我们是能够通过不同的信号量来辨别的,生产满了,空间信号量为0,生产者就不能再生产了;消费者消费完了数据,资源信号量为0,消费者也不能在消费了,因此当生产位置和消费位置一致时,我们是能够保证生产者和消费者是互斥与同步的。
而如果生产位置和消费位置不一致,那么换言之,此时环形队列既不为空也不为满,那么也就是说,此时的空间资源信号量和数据资源信号量都不为0,那么此时消费者和生产者都可以获取到相应的信号量,进而成功访问,从而可以达到并发访问的效果。
有了上面的理解,我们对编码如下:
在这里对信号量做了一个简单封装,sem.hpp,代码如下:
#pragma once
#include <semaphore.h>
class Sem
{
public:Sem(int value){sem_init(&_sem, 0, value);}// 申请信号量, P操作,具有原子性void P(){sem_wait(&_sem);}// 发布信号量, V操作,具有原子性void V(){sem_post(&_sem);}~Sem(){sem_destroy(&_sem);}
private:sem_t _sem;
};
RingQueue.hpp,代码如下:
#ifndef __RING_QUEUE_HPP__
#define __RING_QUEUE_HPP__
#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>
#include "sem.hpp"const int QUEUE_SIZE = 5;namespace Xq
{template<class T>class RingQueue{public:RingQueue(size_t size = QUEUE_SIZE):_que(size),_size(size),p_step(0),c_step(0),_SpaceSem(size) // 空间资源信号量,_DataSem(0) // 数据资源信号量{}// 生产者向循环队列生产数据void push(const T& val){// 第一步不是直接像循环队列生产数据// 而是应该先申请信号量// 由于push所对应的线程角色是生产者// 而生产者关心的是空间资源// 空间资源不满足,生产者会被阻塞_SpaceSem.P(); // 对空间资源信号量P操作成功,代表空间资源-1//生产数据_que[p_step++] = val;p_step %= _size;// 当生产数据成功后,此时潜台词就是数据资源+1// 因此,我们需要对数据资源信号量进行V操作_DataSem.V();}// 消费者从循环队列中获取数据void pop(T* val){// 与push同理// pop第一步并不是直接从环形队列中获取数据// 由于pop对应的线程角色是消费者, 关注的是数据资源// 而应该现申请数据资源信号量 (P操作)_DataSem.P();// 对数据资源信号量P操作成功,代表数据资源-1// 消费数据*val = _que[c_step++];c_step %= _size;// 当消费成功后,此时空间资源+1// 即我们要对空间资源信号量进行V操作_SpaceSem.V();}private:std::vector<T> _que; // 用vector模拟循环队列size_t _size; // 队列的大小size_t p_step; // 生产位置size_t c_step; // 消费位置// 根据我们的理解链, 我们需要两个信号量// _SpcaeSem 用以表示空间资源的个数Sem _SpaceSem;// _DataSem 用以表示数据资源的个数Sem _DataSem;};
}
#endif
RingQueue.cc代码如下:
#include "RingQueue.hpp"void* product(void* arg)
{Xq::RingQueue<int>* Rque = static_cast<Xq::RingQueue<int>*>(arg);// 构建数据或者任务对象,一般是可以从外部获得(例如网络)// 不要忽略它的时间消耗问题,在这里只是简单示例.int x = 10;while(true){Rque->push(x);std::cout << "生产: " << x << std::endl;x++;}return nullptr;
}void* consume(void* arg)
{Xq::RingQueue<int>* Rque = static_cast<Xq::RingQueue<int>*>(arg);while(true){int val;// 从环形队列中获取任务或者数据Rque->pop(&val);// 随后要进行一定的数据处理,这个过程会消耗一定的时间std::cout << "消费: " << val << std::endl;sleep(1);}return nullptr;
}int main()
{Xq::RingQueue<int>* Rque = new Xq::RingQueue<int>();pthread_t prod, con;pthread_create(&prod, nullptr, product, static_cast<void*>(Rque));pthread_create(&con, nullptr, consume, static_cast<void*>(Rque));pthread_join(prod, nullptr);pthread_join(con, nullptr);delete Rque;return 0;
}
现象如下:
由于,我们消费逻辑故意sleep了,因此我们看到的现象就是, 当第一次生产者将队列填满后。当消费者消费一个数据,生产者生产一个数据。
1.3.2. 多生产多消费
那么多生产多消费如何实现呢?
在以前我们谈生产者消费者模型时,我们就说过,它有三种关系。
而多生产多消费于单生产单消费相比较,我们发现多生产多消费本质上就是多了两种关系,即生产者和生产者的关系 (互斥)、消费者和消费者的关系 (互斥)。
因此,多生产多消费本质上就是解决上面多出来的这两种关系,如何解决呢? 加锁解决。
生产者们一把锁、消费者们一把锁。
那么生产者们的临界资源是什么呢?
答案是: 生产者的下标,即上面代码中的 p_step;
那么消费者们的临界资源是什么呢?
答案是:消费者的下标,即上面代码中的 c_step;
但在此之前,我们还要探讨一个问题:
首先,我们肯定的是,加锁保护是必要的。可是,是先加锁,还是先申请信号量呢?
答案是:一般情况下,我们是先申请信号量,后加锁。
原因有两个:
其一:申请/释放信号量本质上就是原子性操作。因此没有必要把该操作放入加锁和解锁之间 (加锁的粒度越小越好) 。
其二:如果我们先加锁,后申请信号量,带来的第一个问题就是,在加锁和解锁之间,执行流会串行执行。换言之,此时申请信号量就只有一个执行流,而我们为什么要使用信号量呢? 不就是为了提高访问临界资源的并发能力吗?即尽快地将临界资源中的子资源派发给不同的线程,可是现在导致申请信号量只有一个执行流了 (串行执行),这显然违背了我们的初衷。
但是如果我们是先申请信号量,后加锁,那么带来的好处就是,临界资源中的子资源会尽快地被众多线程得到 (提高了线程获取临界资源的并发能力),然后再去申请锁,串行执行任务 (生产 or 消费数据),当任务执行完后,释放锁,其他线程在竞争锁资源。 这样我们既提高了线程的并发能力,也降低了串行执行的粒度。
就好比去电影院看电影,最理想的方案是大家在网上并发买票,而不会是大家挤在电影院的门口,一个一个的买票,然后进入电影院,因为这样的效率太低了 (并发能力被限制,串行执行粒度太大)。
有了上面的理解,我们编写代码就信手拈来了。
我们采用RAII风格的加锁方式,LockGuard.hpp 代码如下:
#pragma once
#include <pthread.h>
namespace Xq
{class mutex{public:mutex(pthread_mutex_t* mtx):_mtx(mtx){}void lock(){pthread_mutex_lock(_mtx);}void unlock(){pthread_mutex_unlock(_mtx);}~mutex(){}private:pthread_mutex_t *_mtx;};class lock_guard{public:lock_guard(pthread_mutex_t* mtx):_mtx(mtx){_mtx.lock();}~lock_guard(){_mtx.unlock();}private:mutex _mtx;};
}
更改后的 RingQueue.hpp 代码如下:
#ifndef __RING_QUEUE_HPP__
#define __RING_QUEUE_HPP__#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>
#include "sem.hpp"
#include "LockGuard.hpp"const int QUEUE_SIZE = 5;
namespace Xq
{template<class T>class RingQueue{public:RingQueue(size_t size = QUEUE_SIZE):_que(size),_size(size),p_step(0),c_step(0),_SpaceSem(size) // 空间资源信号量,_DataSem(0) // 数据资源信号量{pthread_mutex_init(&_Pmtx, nullptr);pthread_mutex_init(&_Cmtx, nullptr);}// 生产者向循环队列生产数据void push(const T& val){// 第一步不是直接像循环队列生产数据// 而是应该先申请信号量// 由于push所对应的线程角色是生产者// 而生产者关心的是空间资源// 空间资源不满足,生产者会被阻塞_SpaceSem.P(); // 对空间资源信号量P操作成功,代表空间信号量-1/** 经过我们的分析,我们已经得到了先申请信号量* 在加锁的思路, 在这里我们用RAII风格的锁*/{Xq::lock_guard guard(&_Pmtx);//生产数据_que[p_step++] = val;p_step %= _size;// 当生产数据成功后,此时潜台词就是数据资源+1// 因此,我们需要对数据资源信号量进行V操作}_DataSem.V();}// 消费者从循环队列中获取数据void pop(T* val){// 与push同理// pop第一步并不是直接从环形队列中获取数据// 而应该现申请数据资源信号量 (P操作)_DataSem.P();// 对数据资源信号了P操作成功,代表数据资源-1// 同理{Xq::lock_guard guard(&_Cmtx);// 消费数据*val = _que[c_step++];c_step %= _size;// 当消费成功后,此时空间资源+1// 即我们要对空间资源信号量进行V操作}_SpaceSem.V();}~RingQueue(){pthread_mutex_destroy(&_Pmtx);pthread_mutex_destroy(&_Cmtx);}private:std::vector<T> _que; // 用vector模拟循环队列size_t _size; // 队列的大小size_t p_step; // 生产位置size_t c_step; // 消费位置// 需要两个信号量// _SpcaeSem 用以表示空间资源Sem _SpaceSem;// _DataSem 用以表示数据资源Sem _DataSem;pthread_mutex_t _Pmtx; // 生产者们的锁pthread_mutex_t _Cmtx; // 消费者们的锁};
}
#endif
1.3.3. 多生产多消费的意义在哪里?
看了上面的代码之后,可能有人会有疑惑,不对啊,你这个多生产多消费好像有问题: "当你加锁之后,你的生产和消费不是串行执行了吗? 你如何体现你的并发能力呢?"
不要狭隘的认为,把任务或者数据放入交易场所就是生产,从交易场所读取数据和任务就是消费。
实际上,生产和消费的本质包括了数据的获取、放置以及提取、处理的整个过程,其中涉及了生产者和消费者在获取、处理数据时的并发执行。
因此事实上,数据或者任务生产前这个过程和获得数据或者任务之后的处理过程,才是最耗费时间的 (一般情况下)。
生产的本质: 私有的任务 ---> 公共空间中。
消费的本质: 公共空间的任务 ---> 私有。
因此,虽然把任务或者数据放入交易场所和从交易场所读取数据和任务是串行执行的,但是在生产者获取任务这个过程中以及消费者处理任务这个过程是并发执行的。
因此生产者消费者模型的核心是:并发的生产任务和数据;并发的消费任务和数据。
1.3.4. 信号量本质是一把计数器,那么该计数器的意义是什么?
信号量本质是一把计数器,计数器的意义是什么?
计数器本质上是表征临界资源的个数的。
我们回想一下以前的阻塞队列式的生产者消费者模型,我们是怎么获取临界资源的呢?
·申请锁 ---> 先判断 (检测条件是否就绪)再访问临界资源 (条件就绪) ---> 释放锁 ---> 本质是我们并不清楚临界资源的情况!
而信号量要提前预设资源的情况,而且在 PV 变化过程中,我们可以在外部根据计数器的变化来了解临界资源的状态。
因此计数器的意义:可以不用进入临界区,就可以得知临界资源情况,甚至可以减少临界区内部的判断,提高了系统的效率和性能。因此,计数器在信号量中扮演了起到了预设临界资源情况、外部了解临界资源状态以及优化临界区访问的作用。·
就好比我们下面的代码:
void push(const T& val)
{_SpaceSem.P(); // P操作{Xq::lock_guard guard(&_Pmtx); // RAII风格的加锁_que[p_step++] = val;p_step %= _size;}_DataSem.V(); // V操作
}
如果我们此时没有申请信号量,上面的问题是不是就是我们以前的问题呢?
当获得到了锁之后,我们必须要先检测临界资源是否就绪 (不检测就会产生访问临界资源不合理的问题)。
而如果我们申请了信号量,那么我们就避免了检测临界资源就绪的这个过程,因为,信号量是对临界资源的预定机制,你只要获得了信号量,一定可以保证你这个线程能访问到临界资源。
换句话说,如果当要访问的临界资源不就绪, 你一定是在加锁和解锁外被挂起的,确切的说,是在P操作被挂起的。而不会影响临界区中的代码。只要你这个线程成功申请到了信号量,一定可以访问到临界资源,而不用做任何的判断。
而这就是我们对信号量的理解。