在多线程编程中,我们经常遇到这样的场景:有多个相同类型的资源(比如一块内存被分成多个缓冲区、一个电影院有多个座位、一个环形队列有多个空位)。如果我们只用互斥锁,一次只能让一个线程访问整个资源池,效率很低。 更好的做法是:允许线程同时访问不同的子资源,只要它们不冲突。
信号量(Semaphore) 正是为此而生的同步工具。它像一个计数器,记录着当前可用资源的数量,线程在使用资源前必须先"预订"(P操作),用完后"归还"(V操作)。
一、信号量基本概念
1.1 信号量是什么

信号量是一个非负整数计数器,它表示某种资源的可用数量 。每个线程在访问资源前,必须先执行 P操作 (等待,减1),如果计数器为0则阻塞;访问结束后执行 V操作(释放,加1),并唤醒等待的线程。

1.2 为什么需要信号量
-
互斥锁 :一次只允许一个线程进入临界区 ,适合保护整体资源(比如一个队列、一个变量)。
-
信号量 :**允许多个线程同时访问不同部分的资源,只要资源数量足够。**例如:一个数组有10个槽位,最多允许10个生产者同时往不同槽位写数据。
信号量本质是对资源数量的预订机制:你不需要等到真正使用时才检查条件,而是在访问前就通过信号量"预订"了一份资源。
1.3 P/V 操作的原子性
-
**
sem_wait(P):**将信号量值减1,如果结果 < 0 则阻塞。减1操作是原子的,不会被打断。 -
**
sem_post(V):**将信号量值加1,如果有线程阻塞则唤醒一个。加1操作也是原子的。
正是这种原子性,保证了多个线程同时申请资源时不会出现"超额预订"。
1.4 二元信号量 vs 互斥锁
-
当**信号量初始值为 1 时,**它就成了二元信号量,功能与互斥锁完全一致:同一时刻只有一个线程能通过 P 操作。
-
但互斥锁有所有权概念(只能由加锁的线程解锁),而信号量没有这个限制,任何线程都可以 V 操作。
二、环形队列生产者消费者模型
2.1 为什么用环形队列?

环形队列(Ring Buffer)用 固定大小数组 + 模运算实现,空间利用率高,没有动态内存分配。
使用信号量,我们可以优雅地解决这个问题------用两个信号量分别记录空位数量和数据数量。
2.2 设计思想
定义两个信号量:
sem_blank:初始值为队列容量N,表示空位的个数。生产者每次生产前要 P(blank)。
sem_data:初始值为 0,表示已有数据的个数。消费者每次消费前要 P(data)。生产者和消费者各自维护自己的下标(
p_step和c_step),移动时取模。关键结论:
队列不为空且不为满时,生产者和消费者可以同时进行(因为它们操作不同位置)。
队列为空或为满时,两者必须同步(生产者先走或消费者先走)。
信号量自动保证了这些约定:空位为0时生产者阻塞,数据为0时消费者阻塞。
2.3 约定验证

| 约定 | 信号量如何保证 |
|---|---|
| 空时,生产者先运行 | sem_data 为0,消费者 P(data) 阻塞,生产者 P(blank) 成功 |
| 满时,消费者先运行 | sem_blank 为0,生产者 P(blank) 阻塞,消费者 P(data) 成功 |
| 生产者不能超过消费者一圈 | sem_blank 最多为 N,当生产者领先一圈时,sem_blank 为0,生产者阻塞 |
| 消费者不能超过生产者 | sem_data 最多为 N,当消费者领先时 sem_data 为0,消费者阻塞 |
三、POSIX 信号量接口
#include <semaphore.h>
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
// pshared: 0 表示线程间共享,非0表示进程间共享
// value: 初始资源数量
// 销毁信号量
int sem_destroy(sem_t *sem);
// P操作:等待,减1
int sem_wait(sem_t *sem);
// V操作:发布,加1
int sem_post(sem_t *sem);
⚠️ 注意:sem_wait 可能被信号中断,通常我们忽略返回值或循环调用。
四、单生产单消费demo
4.1 Makefile
ring_cp:Main.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f ring_cp
4.2 Sem.hpp
#include <iostream>
#include <pthread.h>
#include <semaphore.h>
namespace SemModule
{
const int defaultvalue = 1;
class Sem
{
public:
Sem(unsigned int sem_value = defaultvalue)
{
sem_init(&_sem, 0, sem_value);
};
void P()
{
int n = sem_wait(&_sem);
(void)n;
}
void V()
{
int n = sem_post(&_sem);
(void)n; // 原子的
}
~Sem()
{
sem_destroy(&_sem);
};
private:
sem_t _sem;
};
}
4.3 Main.cc
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "RingQueue.hpp"
void *consumer(void *args)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
while (true)
{
// 1.消费任务
int t = 0;
rq->Pop(&t);
// 2.处理任务 -- 处理任务的时候,这个任务已经被我们拿到了线程上下文中了,不属于队列
std::cout << "消费者拿到了一个数据" << t << std::endl;
}
}
void *productor(void *args)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
int data = 1;
while (true)
{
// sleep(2);
// 1.获得任务
std::cout << "生产了一个任务: " << data << std::endl;
// 2.生产任务
rq->Equeue(data);
data++;
}
}
int main()
{
// 申请阻塞队列
RingQueue<int> *rq = new RingQueue<int>();
// 构建生产和消费者
pthread_t c[1], p[1];
pthread_create(c, nullptr, consumer, rq);
pthread_create(p, nullptr, productor, rq);
pthread_join(c[0], nullptr);
pthread_join(p[0], nullptr);
return 0;
}
4.4 RingQueue.hpp
#pragma once
#include <iostream>
#include <vector>
#include "Sem.hpp"
using namespace SemModule;
static const int gcap = 5; // for debug
template <typename T>
class RingQueue
{
public:
RingQueue(int cap = gcap)
: _cap(cap),
_rq(cap),
_blank_sem(cap),
_p_step(0),
_data_sem(0),
_c_step(0)
{
}
void Equeue(const T &in)
{
// 生产者
//1.申请信号量,空位置信号量
_blank_sem.P();
//2.生产
_rq[_p_step] = in;
//3.更新下标
_p_step++;
//4.维持环形特性
_p_step %= _cap;
_data_sem.V();
}
void Pop(T *out)
{
// 消费者
//1.申请信号量,数据信号量
_data_sem.P();
//2.消费
*out = _rq[_c_step];
//3.更新下标
++_c_step;
//4.维持环形特性
_c_step %= _cap;
_blank_sem.V();
}
~RingQueue() {}
private:
std::vector<T> _rq;
int _cap;
// 生产者 -- 关注空的资源
Sem _blank_sem; // 空位置
int _p_step;
// 消费者
Sem _data_sem; // 数据
int _c_step;
};
生产者快 / 消费者慢


生产者慢 / 消费者快


生产者/ 消费者 同步


五、单生产单消费demo
在上面代码的基础上,修改了mutex.hpp 、main.cc 、RingQueue.hpp
5.1 Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>
namespace MutextModule
{
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_mutex, nullptr);
}
void Lock()
{
int n = pthread_mutex_lock(&_mutex);
(void)n;
}
void Unlock()
{
int n = pthread_mutex_unlock(&_mutex);
(void)n;
}
~Mutex()
{
pthread_mutex_destroy(&_mutex);
}
pthread_mutex_t *Get()
{
return &_mutex;
}
private:
pthread_mutex_t _mutex;
};
class LockGuard
{
public:
LockGuard(Mutex &mutex) : _mutex(mutex)
{
_mutex.Lock();
};
~LockGuard()
{
_mutex.Unlock();
};
private:
Mutex &_mutex;
};
}
5.2 main.cc
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "RingQueue.hpp"
void *consumer(void *args)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
while (true)
{
// 1.消费任务
int t = 0;
rq->Pop(&t);
// 2.处理任务 -- 处理任务的时候,这个任务已经被我们拿到了线程上下文中了,不属于队列
std::cout << "消费者拿到了一个数据" << t << std::endl;
}
}
void *productor(void *args)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
int data = 1;
while (true)
{
// sleep(2);
// 1.获得任务
std::cout << "生产了一个任务: " << data << std::endl;
// 2.生产任务
rq->Equeue(data);
data++;
}
}
int main()
{
// 申请阻塞队列
RingQueue<int> *rq = new RingQueue<int>();
// 构建生产和消费者
pthread_t c[2], p[3];
pthread_create(c, nullptr, consumer, rq);
pthread_create(c + 1, nullptr, consumer, rq);
pthread_create(p, nullptr, productor, rq);
pthread_create(p + 1, nullptr, productor, rq);
pthread_create(p + 2, nullptr, productor, rq);
pthread_join(c[0], nullptr);
pthread_join(c[1], nullptr);
pthread_join(p[0], nullptr);
pthread_join(p[1], nullptr);
pthread_join(p[2], nullptr);
return 0;
}
5.3 RingQueue.hpp
#pragma once
#include <iostream>
#include <vector>
#include "Sem.hpp"
#include "Mutex.hpp"
using namespace SemModule;
using namespace MutextModule;
static const int gcap = 5; // for debug
template <typename T>
class RingQueue
{
public:
RingQueue(int cap = gcap)
: _cap(cap),
_rq(cap),
_blank_sem(cap),
_p_step(0),
_data_sem(0),
_c_step(0)
{
}
void Equeue(const T &in)
{
// 生产者
// 1.申请信号量,空位置信号量
_blank_sem.P();
{
LockGuard lockguard(_pmutex);
//_pmutex.Lock();
// 2.生产
_rq[_p_step] = in;
// 3.更新下标
_p_step++;
// 4.维持环形特性
_p_step %= _cap;
//_pmutex.Unlock();
}
_data_sem.V();
}
void Pop(T *out)
{
// 消费者
// 1.申请信号量,数据信号量
_data_sem.P();
{
LockGuard lockguard(_cmutex);
//_cmutex.Lock();
// 2.消费
*out = _rq[_c_step];
// 3.更新下标
++_c_step;
// 4.维持环形特性
_c_step %= _cap;
// _cmutex.Unlock();
}
_blank_sem.V();
}
~RingQueue() {}
private:
std::vector<T> _rq;
int _cap;
// 生产者 -- 关注空的资源
Sem _blank_sem; // 空位置
int _p_step;
// 消费者
Sem _data_sem; // 数据
int _c_step;
// 维护多生产多消费,2把锁
Mutex _cmutex;
Mutex _pmutex;
};
