https://blog.csdn.net/qscftqwe/article/details/156202943
上节课链接,强烈推荐看一下球求拉!
一.POSIX信号量(线程和进程信号量)
POSIX 信号量和 System V 信号量作用相同,都是用于同步操作 ,达到无冲突地访问共享资源的目的。
POSIX 信号量既可用于线程间同步 (无名信号量,pshared=0),也可用于进程间同步 (有名信号量或 pshared≠0 的无名信号量);而 System V 信号量仅用于进程间同步。
进一步了解信号量
- 信号量的本质是一个计数器 ,用于描述可用资源的数量。
- 申请信号量(如
sem_wait)会原子地判断并减少计数值,从而间接判断资源是否就绪。 - 当信号量初值为 1 时,称为二元信号量 ,其行为类似于互斥锁。
二.信号量的操作
1.sem_init
cpp
// 初始化一个未命名信号量(可用于线程或进程间同步)
// pshared 为 0 时用于线程间 pshared 非 0 且支持时用于进程间
#include <semaphore.h>
int sem_init(
sem_t *sem,
int pshared,
unsigned int value
);
// 参数:
// sem - 指向信号量变量
// pshared - 0 表示线程间共享 非 0 表示进程间共享(需在共享内存中)
// value - 信号量初始值(通常为 0 或 1)
// 返回值:
// 成功:返回 0
// 失败:返回 -1 并设置 errno
// 案例:
// 初始化一个二元信号量 初始可用
sem_init(&sem, 0, 1);
2.sem_destroy
cpp
// 销毁一个未命名信号量 释放其占用的资源
// 调用前应确保没有线程或进程在等待该信号量
#include <semaphore.h>
int sem_destroy(sem_t *sem);
// 参数:
// sem - 指向已初始化的信号量
// 返回值:
// 成功:返回 0
// 失败:返回 -1 并设置 errno
// 案例:
sem_destroy(&sem);
3.sem_wait
cpp
// 对信号量执行 P 操作(申请资源)
// 若信号量值 > 0 则减 1 并立即返回
// 若信号量值 == 0 则阻塞当前线程 直到其他线程调用 sem_post
#include <semaphore.h>
int sem_wait(sem_t *sem);
// 参数:
// sem - 指向已初始化的信号量
// 返回值:
// 成功:返回 0
// 失败:返回 -1 并设置 errno
// 案例:
sem_wait(&sem);
4.sem_post
cpp
// 对信号量执行 V 操作(释放资源)
// 将信号量值加 1 并唤醒一个等待该信号量的线程(如有)
#include <semaphore.h>
int sem_post(sem_t *sem);
// 参数:
// sem - 指向已初始化的信号量
// 返回值:
// 成功:返回 0
// 失败:返回 -1 并设置 errno
// 案例:
sem_post(&sem);
三.环形队列的生产消费模型

认识信号量的帮助
- 环形队列中,起始和结束状态相同(
head == tail),难以区分空与满 ,可通过预留一个空位 或使用计数器 解决;若使用两个信号量(empty 和 full) ,可自然区分空满状态,无需额外判断。 head和tail重合时,队列可能为空或满,此时需通过同步机制(如信号量)控制生产与消费的顺序。- 在多生产者-多消费者场景 中,通常需要两把锁 :一把保护生产者对
tail的修改,一把保护消费者对head的修改;同时配合两个信号量(empty 和 full) ,信号量的 wait 操作应在加锁前,post 操作在解锁后。
四.实现环形队列的生产消费模型
4.1函数复习
1.pthread_mutex_lock
cpp
// 对互斥锁加锁 若已被其他线程持有 则阻塞当前线程
// 用于进入临界区 保证共享数据访问的原子性
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 参数:
// mutex - 指向已初始化的互斥锁
// 返回值:
// 成功:返回 0
// 失败:返回非 0 错误码
// 案例:
pthread_mutex_lock(&mutex);
2.pthread_mutex_unlock
cpp
// 对互斥锁解锁 唤醒其他等待该锁的线程
// 必须由持有锁的线程调用 否则行为未定义
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 参数:
// mutex - 指向已加锁的互斥锁
// 返回值:
// 成功:返回 0
// 失败:返回非 0 错误码
// 案例:
pthread_mutex_unlock(&mutex);
3.pthread_cond_wait
cpp
// 阻塞当前线程并自动释放关联的互斥锁 等待条件变量被唤醒
// 唤醒后会重新 acquire 锁 再返回(因此返回时仍持有锁)
#include <pthread.h>
int pthread_cond_wait(
pthread_cond_t *cond,
pthread_mutex_t *mutex
);
// 参数:
// cond - 指向条件变量
// mutex - 指向与该条件变量关联的互斥锁(必须已加锁)
// 返回值:
// 成功:返回 0
// 失败:返回非 0 错误码
// 案例:
pthread_cond_wait(&cond, &mutex);
4.pthread_cond_signal
cpp
// 唤醒等待该条件变量的**一个**线程(通常为等待队列中的第一个)
// 若无线程等待 则无操作
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
// 参数:
// cond - 指向条件变量
// 返回值:
// 成功:返回 0
// 失败:返回非 0 错误码
// 案例:
pthread_cond_signal(&cond);
5.pthread_mutex_init
cpp
// 初始化一个互斥锁 用于保护临界区
// 默认属性下锁是非递归的 同一线程重复加锁会导致死锁
#include <pthread.h>
int pthread_mutex_init(
pthread_mutex_t *mutex,
const pthread_mutexattr_t *attr
);
// 参数:
// mutex - 指向要初始化的互斥锁变量
// attr - 锁属性(通常设为 NULL 表示默认属性)
// 返回值:
// 成功:返回 0
// 失败:返回非 0 错误码
// 案例:
pthread_mutex_init(&mutex, NULL);
6.pthread_cond_init
cpp
// 初始化一个条件变量 用于线程间同步(常与互斥锁配合使用)
// 条件变量本身不存储状态 仅用于阻塞/唤醒等待特定条件的线程
#include <pthread.h>
int pthread_cond_init(
pthread_cond_t *cond,
const pthread_condattr_t *attr
);
// 参数:
// cond - 指向要初始化的条件变量
// attr - 属性(通常设为 NULL 表示默认)
// 返回值:
// 成功:返回 0
// 失败:返回非 0 错误码
// 案例:
pthread_cond_init(&cond, NULL);
7.pthread_mutex_destroy
cpp
// 销毁一个互斥锁 释放其占用的资源
// 锁必须处于未锁定状态 否则行为未定义
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 参数:
// mutex - 指向要销毁的互斥锁(必须已初始化)
// 返回值:
// 成功:返回 0
// 失败:返回非 0 错误码
// 案例:
pthread_mutex_destroy(&mutex);
8.pthread_cond_destroy
cpp
// 销毁一个条件变量 释放其占用的资源
// 调用前应确保没有线程在等待该条件变量
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
// 参数:
// cond - 指向要销毁的条件变量(必须已初始化)
// 返回值:
// 成功:返回 0
// 失败:返回非 0 错误码
// 案例:
pthread_cond_destroy(&cond);
9.pthread_create
cpp
// 创建一个新的线程 执行指定函数
// 新线程与调用线程共享进程地址空间(如全局变量、堆)
#include <pthread.h>
int pthread_create(
pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg
);
// 参数:
// thread - 指向 pthread_t 变量 用于保存新线程 ID
// attr - 线程属性(通常设为 NULL 表示默认属性)
// start_routine - 新线程要执行的函数(返回 void* 接收 void*)
// arg - 传递给 start_routine 的参数(可为 NULL)
// 返回值:
// 成功:返回 0
// 失败:返回非 0 错误码(不设 errno)
// 案例:
pthread_create(&tid, NULL, task, data);
10.pthread_join
cpp
// 等待指定线程终止 并回收其资源(类似进程的 wait)
// 调用者会阻塞 直到目标线程结束
#include <pthread.h>
int pthread_join(
pthread_t thread,
void **retval
);
// 参数:
// thread - 要等待的线程 ID
// retval - 若非 NULL 则接收线程的返回值(即 start_routine 的返回值)
// 返回值:
// 成功:返回 0
// 失败:返回非 0 错误码(如线程不可 join 或已 join)
// 案例:
pthread_join(tid, NULL);
4.2 完整代码
cpp
#include <iostream>
#include <queue>
#include <stdlib.h>
#include <pthread.h>
#include <time.h>
#include <unistd.h> // 用于线程休眠,避免打印刷屏
#define NUM 16 // 队列默认容量
// 阻塞队列:线程安全的生产者-消费者模型
class BlockQueue
{
std::queue<int> _q; // 存储数据的队列
int _cap; // 队列容量
pthread_mutex_t _lock; // 互斥锁:保护队列访问
pthread_cond_t _not_full; // 条件变量:队列满时生产者等待
pthread_cond_t _not_empty; // 条件变量:队列空时消费者等待
private:
void LockQueue()
{ // 加锁
pthread_mutex_lock(&_lock);//(1)
}
void UnLockQueue()
{ // 解锁
pthread_mutex_unlock(&_lock);//(2)
}
void ProductWait()
{ // 生产者等待队列有空间
pthread_cond_wait(&_not_full, &_lock);//(3)
}
void ConsumeWait()
{ // 消费者等待队列有数据
pthread_cond_wait(&_not_empty, &_lock);
}
void NotifyConsume()
{ // 通知消费者(队列有数据)
pthread_cond_signal(&_not_empty);//(4)
}
void NotifyProduct()
{ // 通知生产者(队列有空间)
pthread_cond_signal(&_not_full);
}
bool IsEmpty()
{ // 检查队列是否为空
return _q.empty();
}
bool IsFull()
{ // 检查队列是否已满
return _q.size() == _cap;
}
public:
BlockQueue(int _cap = NUM)
: _cap(_cap)
{ // 初始化队列
pthread_mutex_init(&_lock, NULL);//(5)
pthread_cond_init(&_not_full, NULL);//(6)
pthread_cond_init(&_not_empty, NULL);
}
void PushData(const int &data)
{ // 生产数据:阻塞直到队列有空间
LockQueue();
while (IsFull())
ProductWait(); // 等待队列有空间
_q.push(data);
NotifyConsume(); // 通知消费者
UnLockQueue();
}
void PopData(int &data)
{ // 消费数据:阻塞直到队列有数据
LockQueue();
while (IsEmpty())
ConsumeWait(); // 等待队列有数据
data = _q.front();
_q.pop();
NotifyProduct(); // 通知生产者
UnLockQueue();
}
~BlockQueue()
{ // 销毁同步原语
pthread_mutex_destroy(&_lock);//(7)
pthread_cond_destroy(&_not_full);//(8)
pthread_cond_destroy(&_not_empty);
}
};
// 消费者线程:从队列取数据并打印
void *consumer(void *arg)
{
BlockQueue *bqp = (BlockQueue *)arg;
int data;
for (;;)
{
bqp->PopData(data); // 阻塞等待数据
std::cout << "Consume data done: " << data << std::endl;
usleep(100000); // 休眠避免打印刷屏
}
}
// 生产者线程:生成并放入队列
void *producter(void *arg)
{
BlockQueue *bqp = (BlockQueue *)arg;
static int data = 1; // 从1开始,每次递增
for (;;)
{
bqp->PushData(data); // 阻塞等待队列空间
std::cout << "Product data done: " << data << std::endl;
++data; // 下一次生产下一个数字
usleep(50000); // 休眠避免刷屏
}
}
int main()
{
BlockQueue bq; // 创建默认容量16的阻塞队列
pthread_t c, p; // 消费者和生产者线程ID
pthread_create(&c, NULL, consumer, (void *)&bq); // 启动消费者//(9)
pthread_create(&p, NULL, producter, (void *)&bq); // 启动生产者
pthread_join(c, NULL); // 等待消费者线程结束(实际会永远运行)//(10)
pthread_join(p, NULL); // 等待生产者线程结束
return 0;
}
我会讲解其中两个函数,其余函数还请大家结合函数复习与注释进行观看!
cpp
void PushData(const int &data)
{ // 生产数据:阻塞直到队列有空间
LockQueue();
while (IsFull())
ProductWait(); // 等待队列有空间
_q.push(data);
NotifyConsume(); // 通知消费者
UnLockQueue();
}
void PopData(int &data)
{ // 消费数据:阻塞直到队列有数据
LockQueue();
while (IsEmpty())
ConsumeWait(); // 等待队列有数据
data = _q.front();
_q.pop();
NotifyProduct(); // 通知生产者
UnLockQueue();
}
这两个函数分别用于数据生成和消费。在生产者-消费者模型中,我们遵循"321原则"。但环形队列的不同之处在于:生产者和消费者可以并发执行(一方持续发送数据,另一方持续接收,只要不出现位置重叠就能持续并发)。当出现位置重叠时,就会触发互斥机制。
因此,当缓冲区满时,生产必须暂停;反之,当缓冲区空时,消费必须暂停。暂停的目的是为了唤醒对方进行生产或消费。
**五.**STl、智能制造和线程安全
STL和线程安全
STL容器不具备多线程安全
智能指针和线程安全
智能指针具有线程安全(因为一个不可以共享自然没有线程安全问题,一个采用引用计数也可以引发)
好了线程内容就差不多讲到这把,首先关于线程这部分我讲解的都是重点的部分,这节课主要重点是信号量操作搞明白,然后就是学一下基于环形队列的生产消费模型。