【Linux】线程同步
📝前言:
上篇文章我们讲解了【Linux】线程互斥,这篇文章我们来讲讲Linux——线程同步
🎬个人简介:努力学习ing
📋个人专栏:Linux
🎀CSDN主页 愚润求学
🌄其他专栏:C++学习笔记,C语言入门基础,python入门基础,C++刷题专栏
目录
- 一,同步定义
- 二,条件变量
- 1. 基本介绍
- 2. 接口
- 2.1 初始化和销毁
- 初始化
- 销毁
- 2.2 等待和通知
- 等待
- 通知(唤醒)
- 三,使用经典规范
- 1. 等待代码
- 2. 唤醒代码
- 四,生产者消费者模型
- 1. 基本介绍
- 2. 使用示例
一,同步定义
在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源。我们可以回忆一下管道,管道的读写行为是具有顺序性的,这就是一种同步。
为什么需要同步?
当两个线程(一个读端,一个写端)同时访问一块内存的时候,为了保证线程安全性,需要加锁。但是,如果此时这块内存写端还没有写入内容,读端一直占据着锁(频繁申请,且每次都竞争到锁),写端就无法写入内容,读端也无法读到内容。
为此,我们可以让读端没读到内容的时候就进入等待(把锁给释放),然后唤醒写端(让写端拿锁)
如果有多个读端,又一个读端“离锁最近”一直在读,其他读端读不到怎么办?
这时候我们可以设计一个“等待队列”,让每个读端申请完一次锁以后,就要重新进入队列的末尾开始重新排队。
条件变量就具有这样的唤醒 + 等待队列的功能
二,条件变量
1. 基本介绍
解决线程间因共享资源状态变化而需要互相通知的问题,且保证公平性。
作用
- 让线程等待某个条件满足(如共享资源可用、数据准备完成等)。
- 当条件满足时,通知等待的线程继续执行。
关键操作
- 等待(Wait):线程阻塞自己,放入指定条件变量的等待队列,等待条件变量被唤醒。
- 通知(Signal/Broadcast):其他线程在条件满足时唤醒等待的线程。
与互斥锁的配合
- 条件变量本身不具备锁的功能,需配合互斥锁使用,确保对共享资源状态的检查和修改是原子操作。
2. 接口
基本上和mutex
的接口一样。
2.1 初始化和销毁
初始化
静态分配
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_t
:条件变量cond
的类型
动态分配
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
cond
:要初始化的条件变量attr
:nullptr
销毁
int pthread_cond_destroy(pthread_cond_t *cond)
2.2 等待和通知
等待
cond_wait
包含两步操作:解锁 + 等待(类比--
是3条汇编)
为什么条件变量要配合锁使用?
如果解锁 + 等待
是两个动作,不是原子的,则会出现以下问题:
- 线程 A 条件不满足 → 解锁
- 线程 A 被切换,线程 B 唤醒线程 A(但是线程 A 还未进入等待,收不到这个唤醒,唤醒被错过)
- 切回线程 A ,A 进入等待,但接受不到唤醒信号了,变成死锁
常用接口
无限期阻塞等待(只有唤醒才醒来)
int pthread_cond_wait(
pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
cond
:在哪个条件变量上等待mutex
:对应的互斥锁- 在进入等待的时候会释放锁,等待被唤醒后要重新申请锁
- 申请成功,就会接着运行后面的语句
- 申请不成功,就会在“锁上”阻塞(直到拿到锁)
- 被唤醒就是在临界区被唤醒,然后继续往后执行
时间片等待(超时了就醒来)
int pthread_cond_timedwait(
pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime
);
- 绝对超时时间(如果到这个时间还没有被唤醒,就醒来)
通知(唤醒)
全部唤醒
int pthread_cond_broadcast(pthread_cond_t *cond);
如果cond
里面没有能唤醒的,则什么都不做。
只唤醒条件变量的队头
int pthread_cond_signal(pthread_cond_t *cond);
三,使用经典规范
1. 等待代码
pthread_mutex_lock(&mutex); // 访问临界区先加锁
while (条件为假) // 判断条件是否满足,实现同步
pthread_cond_wait(cond, mutex); // 条件不满足:释放锁 + 等待
访问临界资源; // 修改共享资源(条件可能再次变为不满足)
pthread_mutex_unlock(&mutex); // 访问完临界区解锁
为什么用 while
而不是 if
?
虚假唤醒:
pthread_cond_wait
执行错误(没有进入等待),然后返回pthread_cond_wait
一下被唤醒多个线程,但是一个线程执行完以后,条件再次变为不满足。此时其他线程过了if
判断且被唤醒,再次向下执行,就会出现错误。所以要循环重新检查条件。
2. 唤醒代码
在另一个线程内:
pthread_mutex_lock(&mutex); // 代表也是在加锁的临界区内
访问临界资源(条件); // 使条件为真
pthread_cond_signal(&cond); // (当条件为真,且有可以唤醒的)唤醒一个等待的线程
pthread_mutex_unlock(&mutex);
四,生产者消费者模型
1. 基本介绍
生产者消费者模式就是通过⼀个容器(交易场所)来解决生产者和消费者的强耦合问题。生产者往交易场所生产数据,消费者从交易场所获取数据,生产者的生产不需要等待消费者处理完数据。从而实现生产和消费的并发运行。
生产者与消费者关系:
- 生产者和生产者:互斥
- 消费者与消费者:互斥
- 生产者和消费者:互斥 + 同步
特点:
- 3 个要素:生产者,消费者,一个交易场所(临界资源)
- 3 个关系:(上面提到的生产者与消费者的关系)
- 2 种角色:生产者和消费者(分别由线程承担)
- 1 个交易场所:以特定结构构成的一种“内存”(临界资源)
优势
- 支持生产和消费解耦
- 忙闲不均
- 提高效率
2. 使用示例
我们实现一个基于BlockingQueue的生产者消费者模型
BlockingQueue核心要点:
- Pop 的时候,如果队列里面没东西,则阻塞等待生产者Push
- Push 的时候,如果队列满了,则阻塞等待消费者Pop
// BlockQueue.hpp
#include
#include
#include
#include
template <typename T>
class BlockQueue
{
private:
bool Empty()
{
return blockqueue.size() == 0;
}
bool Full()
{
return blockqueue.size() == _cap;
}
public:
BlockQueue(int cap)
: _prod_wait_num(0), _coms_wait_num(0), _cap(cap)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_full_cond, nullptr);
pthread_cond_init(&_empty_cond, nullptr);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_full_cond);
pthread_cond_destroy(&_empty_cond);
}
void Push(const T &task)
{
pthread_mutex_lock(&_mutex);
while (Full())
{
_prod_wait_num++;
pthread_cond_wait(&_full_cond, &_mutex);
_prod_wait_num--;
}
blockqueue.push(task); // 修改临界资源
std::cout << "生产者: " << pthread_self() << " 生产了一个任务" << std::endl;
if (_coms_wait_num > 0)
pthread_cond_signal(&_empty_cond);
pthread_mutex_unlock(&_mutex);
}
T Pop()
{
pthread_mutex_lock(&_mutex);
// 等待
while (Empty())
{
_coms_wait_num++;
// 如果唤醒,就在临界区被唤醒,然后继续往下执行
pthread_cond_wait(&_empty_cond, &_mutex);
_coms_wait_num--;
}
T task = blockqueue.front();
blockqueue.pop();
std::cout << "消费者: " << pthread_self() << " 处理了一个任务" << std::endl;
// 唤醒
if (_prod_wait_num > 0)
pthread_cond_signal(&_full_cond);
pthread_mutex_unlock(&_mutex);
return task;
}
private:
std::queue<T> blockqueue;
pthread_mutex_t _mutex;
pthread_cond_t _full_cond;
pthread_cond_t _empty_cond;
int _cap; // 设置容量上限
int _prod_wait_num;
int _coms_wait_num;
};
// Main.cpp
#include "BlockQueue.hpp"
#include
#include
#include
#include "Task.hpp"
void* Prod(void* args)
{
BlockQueue<task_t>* ptr = static_cast<BlockQueue<task_t>*>(args);
while(true)
{
// sleep(1);
ptr->Push(Download);
}
return nullptr;
}
void* Coms(void* args)
{
BlockQueue<task_t>* ptr = static_cast<BlockQueue<task_t>*>(args);
while(true)
{
task_t task = ptr->Pop(); // 获取任务
// cout 输出也会有并发问题,暂时放在Pop里面
task();
}
return nullptr;
}
int main()
{
BlockQueue<int> super(3);
// 单生产者,单消费者
pthread_t p[1], c[1];
pthread_create(&p[0], nullptr, Prod, &super); // 生成者
pthread_create(&c[0], nullptr, Coms, &super); // 消费者
pthread_join(p[0], nullptr);
pthread_join(c[0], nullptr);
// // 多生产者,多消费者
// pthread_t p[2], c[2];
// pthread_create(&p[0], nullptr, Prod, &super); // 生成者
// pthread_create(&c[0], nullptr, Coms, &super); // 消费者
// pthread_create(&p[1], nullptr, Prod, &super); // 生成者
// pthread_create(&c[1], nullptr, Coms, &super); // 消费者
// pthread_join(p[0], nullptr);
// pthread_join(c[0], nullptr);
// pthread_join(p[1], nullptr);
// pthread_join(c[1], nullptr);
return 0;
}
当然,我的代码还有很多未处理的BUG,仅供参考。
运行结果(单生产者和单消费者):
生成者快,消费者慢:先生产一堆,然后消费一个生产一个。
生产者满,消费者慢,生产一个消费一个。
🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!