文章目录
一. 什么是POSIX信号量?
POSIX
和System V
都是可移植的操作系统接口标准,它们都定义了操作系统应该为应用程序提供的接口标准。
- POSIX信号量和System V信号量作用相同,都是用于同步和互斥操作,以达到无冲突的访问共享资源目的。
- System V版本的信号量只适用于实现进程间的通信,而POSIX版本的信号量主要用于实现线程之间的通信。
信号量(信号灯)本质是一个是用来对临界资源进行更细粒度地描述和管理的计数器。
二. 为什么要有POSIX信号量?
POSIX信号量主要用于实现线程间的同步。
三. POSIX信号量实现原理
信号量的结构如下:
count
:记录还有多少小块的临界资源未被使用。queue
:当count为0时,其它未申请到信号量的线程的task_struct地址会被放到信号量等待队列中阻塞挂起。
信号量的PV操作
- P操作:我们把申请信号量得操作称为P操作,申请信号量的本质就是申请获得整块临界资源中某小块资源的使用权限,当申请成功时临界资源中小块资源的数目应该减一,因此P操作的本质就是让
count
- -。 - V操作:我们将释放信号量称为V操作,释放信号量的本质就是归还临界资源中某块资源的使用权限,当释放成功时临界资源中资源的数目就应该加一,因此V操作的本质就是让
count
++。
申请不到信号量的线程被阻塞挂起
当count
为0时,表示不允许其它线程再访问临界资源,这时其它申请信号量的线程会被阻塞到该信号量的等待队列中,直到其它线程释放信号量。
四. POSIX信号量接口函数
1. 创建、初始化信号量
信号量的类型是sem_t
,我们可以根据这个类型自己定义信号量对象:
定义出信号量对象之后,必须用sem_init()函数来初始化这个信号量:
参数说明:
- sem:信号量对象的地址。
- pshared:0表示线程间共享,非零表示进程间共享。
- value:信号量初始值,即count的大小。
函数说明:该函数主要用于设置信号量对象的基本属性。
2. 销毁信号量
函数说明:只需传入信号量对象的地址即可销毁该信号量。
3. 等待(申请)信号量
函数说明:传入信号量对象的地址用于申请该信号量,调用成功返回0,count- -;失败返回-1,count值不变。
4. 发布(释放)信号量
函数说明:传入信号量对象的地址用于释放该信号量,调用成功返回0,count++;失败返回-1,count值不变。
五. 信号量的应用
1. 二元信号量模拟互斥锁
当count = 1
时,说明整块临界资源作为一个整体使用而没有被切分管理,那么这个信号量对象就相当于是一把互斥锁,称为二元信号量。
下面我们用二元信号量模拟互斥锁完成黄牛抢票代码:
- 在主线程中创建4个新线程去抢10张票。
- 此时票是临界资源,我们用二元信号量对其进行保护。
- 每个新线程抢票之前都要先申请二元信号量,没有申请到线程被阻塞挂起。
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
using namespace std;
// 封装一个自己的信号量类
class MySem
{
public:
// 构造函数,用于初始化信号量对象,默认线程间通信,只需传入需要设置的count得值即可
MySem(size_t num)
{
sem_init(&_sem, 0, num);
}
// 析构函数,销毁信号量对象
~MySem()
{
sem_destroy(&_sem);
}
// 申请信号量
void P()
{
sem_wait(&_sem);
}
// 释放信号量
void V()
{
sem_post(&_sem);
}
private:
// 成员变量是一个信号量对象
sem_t _sem;
};
// 定义的全局对象
int count = 10;// 票数设为10张
MySem sem(1);// 一元信号量
// 新线程执行的抢票逻辑
void* GetTickets(void* arg)
{
while(true)
{
size_t id = (size_t)arg;
sem.P();
if(count > 0)
{
usleep(1000);
cout<<'['<<"thread "<<id<<']'