目录
一、信号量的基本概念
信号量(Semaphore)是一种用于控制多个进程对共享资源进行访问的同步机制。它通常用于解决进程间的互斥和同步问题,确保资源在某一时刻只能被一个进程访问(互斥),或者多个进程能够按照某种顺序访问资源(同步)。
- 二元信号量:
- 二元信号量是最简单的信号量形式,其值只能为0或1。
- 当信号量的值为1时,表示资源可用,进程可以访问该资源。
- 当信号量的值为0时,表示资源已被占用,进程需要等待。
- 二元可重复信号量:
- 与二元信号量不同,二元可重复信号量允许多个进程同时访问资源,但其值仍然只能为0或1。
- 这里的“可重复”指的是信号量可以被多次“申请”(即P操作)和“释放”(即V操作),但每次操作后信号量的值仍然保持在0或1之间。
- 实际上,这种信号量更接近于一种标志位,用于表示资源是否可用,而不是限制资源的访问次数。
- 多元信号量:
- 多元信号量的值可以大于1,表示允许多个进程同时访问资源。
- 当信号量的值大于0时,表示还有可用的资源,进程可以申请访问。
- 每当一个进程访问资源时,信号量的值减1;当进程释放资源时,信号量的值加1。
二、信号量的操作
信号量的操作主要包括创建、初始化、等待(P操作)和发送(V操作)。
- 创建信号量:
- 在使用信号量之前,需要先创建它。
- 在POSIX标准中,可以使用
sem_init
函数来初始化一个未命名的信号量,或者使用semget
函数来创建一个命名的信号量集。
- 初始化信号量:
- 初始化信号量时,需要指定信号量的初始值。
- 对于未命名的信号量,
sem_init
函数的第三个参数用于指定信号量的初始值。 - 对于命名的信号量集,
semget
函数创建信号量集后,还需要使用semctl
函数来初始化每个信号量的值。
- 等待信号量(P操作):
- 等待信号量是指进程尝试获取对共享资源的访问权。
- 如果信号量的值大于0,则进程成功获取资源,信号量的值减1。
- 如果信号量的值为0,则进程进入等待状态,直到其他进程释放资源并增加信号量的值。
- 在POSIX标准中,可以使用
sem_wait
函数来等待信号量。
- 发送信号量(V操作):
- 发送信号量是指进程释放对共享资源的访问权。
- 进程释放资源后,信号量的值加1。
- 如果有等待该信号量的进程,则唤醒其中一个进程。
- 在POSIX标准中,可以使用
sem_post
函数来发送信号量。
三、信号量的代码示例
基础
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
sem_t semaphore;
void* thread_function(void* arg)
{
// 等待信号量
sem_wait(&semaphore);
// 访问共享资源
printf("Thread %ld is accessing the resource.\n", (long)arg);
// 模拟资源访问时间
sleep(1);
// 释放信号量
sem_post(&semaphore);
return NULL;
}
int main() {
pthread_t threads[5];
// 初始化信号量,初始值为1
sem_init(&semaphore, 0, 1);
// 创建多个线程
for (long i = 0; i < 5; i++)
{
pthread_create(&threads[i], NULL, thread_function, (void*)i);
}
// 等待所有线程完成
for (int i = 0; i < 5; i++)
{
pthread_join(threads[i], NULL);
}
// 销毁信号量
sem_destroy(&semaphore);
return 0;
}
在这个示例中,我们创建了一个初始值为1的信号量,并创建了5个线程。每个线程在访问共享资源之前都会等待信号量,访问完成后释放信号量。由于信号量的初始值为1,因此只有一个线程能够同时访问资源,其他线程需要等待。
进阶样例:环形队列--生产者消费者模型
//.hpp文件
#ifndef _RINGQUEUE_HPP_
#define _RINGQUEUE_HPP_
#include <iostream>
#include <semaphore.h>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>
using DataType = int;
static int DEFAULT = 5;
template<typename T>
class RingQueue
{
private:
void P(sem_t& sem)
{
sem_wait(&sem);
}
void V(sem_t& sem)
{
sem_post(&sem);
}
public:
RingQueue(int maxcap = DEFAULT)
: _MaxCap(maxcap), _ringQueue(maxcap), _p_index(0), _c_index(0)
{
// 初始化信号量
sem_init(&_Space_sem, 0, _MaxCap);
sem_init(&_Data_sem, 0, 0);
}
void Push(const T &data)
{
// 先申请信号量预定空间资源
P(_Space_sem);
_ringQueue[_p_index] = data;
_p_index = (++_p_index) % _MaxCap;
V(_Data_sem);
}
void Pop(T *OutData)
{
// 先申请信号量预定data资源
P(_Data_sem);
*OutData = _ringQueue[_c_index];
_c_index = (++_c_index) % _MaxCap;
V(_Space_sem);
}
~RingQueue()
{
// 销毁信号量
sem_destroy(&_Space_sem);
sem_destroy(&_Data_sem);
}
private:
std::vector<T> _ringQueue;
int _MaxCap;
int _p_index; // 生产者下标
int _c_index; // 消费者下标
sem_t _Space_sem; // 生产者信号量
sem_t _Data_sem; // 消费者信号量
};
#endif
四、信号量在进程同步中的应用
信号量在进程同步中有着广泛的应用,特别是在解决生产者-消费者问题和哲学家就餐问题时。
- 生产者-消费者问题:
- 生产者-消费者问题是一个经典的同步问题,其中生产者进程生产数据并将其放入缓冲区,消费者进程从缓冲区中取出数据并处理。
- 为了确保生产者和消费者之间的正确同步,可以使用信号量来控制对缓冲区的访问。
- 通常,会使用两个信号量:一个用于控制空缓冲区的数量(生产者等待),另一个用于控制满缓冲区的数量(消费者等待)。
- 哲学家就餐问题:
- 哲学家就餐问题是一个涉及多个进程(哲学家)和多个资源(叉子)的同步问题。
- 每个哲学家需要两把叉子才能吃饭,而叉子数量有限且分布在桌子上。
- 为了避免死锁和饥饿问题,可以使用信号量来控制对叉子的访问。
- 通常,会为每把叉子分配一个信号量,并在哲学家尝试拿起或放下叉子时进行相应的等待和发送操作 done