这篇文章给大家带来线程同步与互斥的学习!!!
首先抛出一个问题:线程互斥,它是对的,但是它合理(任何场景)吗??? 答:不一定合理
我们已经直到同步是什么了,那么如何实现同步与互斥呢? 答:条件变量
下面代码看看就好,后面会讲
pthread_mutex_lock()
if (YES/NO)
{
pthread_cond_wait()
}
// ....做其他事情
pthread_cond_signal() // 或者唤醒其他线程, 也可以在主线程判断唤醒
pthread_mutex_unlock(); // 解锁
初始化条件变量有二种方法
第一种方法:静态分配
#include
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
第二个方法:动态分配
#include
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrictattr);
销毁条件变量:
#include
int pthread_cond_destroy(pthread_cond_t *cond)
#include
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
#include
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
使用同步与互斥实现多线程间轮询运行,在主线程唤醒被条件变量阻塞的线程
#include
#include
#include
#include
#include
using namespace std;
// 定义条件变量
pthread_cond_t pc;
// pthread_cond_t pc = PTHREAD_COND_INITIALIZER; // 静态初始化
// 定义互斥锁
pthread_mutex_t pm;
// pthread_mutex_t pm = PTHREAD_MUTEX_INITIALIZER; // 静态初始化
// 定义全局退出变量 -- volatile保持内存可见性
volatile bool quit = false;
// 定义一个方法集合
typedef void (*Func)();
vector<Func> handler;
void Print()
{
cout << "hello Print" << endl;
}
// 线程执行函数
void *CallThread(void *args)
{
// 线程分离,线程退出后自动释放资源,主线程不用等待
pthread_detach(pthread_self());
string name = static_cast<const char *>(args);
while (!quit)
{
// 执行这个函数,说明特定的条件变量没有就绪,执行该函数的线程会被阻塞挂起等待
pthread_cond_wait(&pc, &pm);
cout << name << ": " << pthread_self() << ", run..." << endl;
// 执行方法集合的全部方法
for (auto &f : handler)
{
f();
}
}
return nullptr;
}
int main()
{
// 加载函数方法 -- 第二个是lambda表达式(底层是仿函数operator())
handler.push_back(Print);
handler.push_back([]()
{ cout << "hello lambda" << endl; }
);
// 初始化条件变量和互斥锁
pthread_mutex_init(&pm, nullptr);
pthread_cond_init(&pc, nullptr);
// 线程创建
pthread_t t1, t2, t3;
pthread_create(&t1, nullptr, CallThread, (void *)"Thread1");
pthread_create(&t2, nullptr, CallThread, (void *)"Thread2");
pthread_create(&t3, nullptr, CallThread, (void *)"Thread3");
while (true)
{
char ch = '\0';
cout << "请输入一个字符y/n: ";
cin >> ch;
if (ch == 'y')
{
// 该函数可以让条件变量就绪,唤醒单个被阻塞挂起线程
pthread_cond_signal(&pc);
sleep(1);
}
else
{
quit = true;
// 唤醒所有被阻塞挂的线程
pthread_cond_broadcast(&pc);
break;
}
}
// 释放条件变量和互斥锁
pthread_cond_destroy(&pc);
pthread_mutex_destroy(&pm);
return 0;
}
从运行结果图可以看出线程是按顺序轮询执行的…
为什么输入n后,唤醒全部线程后,最后不是打印三次方法集合的信息呢?
因为全部线程被唤醒后,又会重新去竞争锁(条件变量需要重新申请锁),只有一个线程可以竞争成功,没有竞争成功的线,会重新判断!quit,!quit为false,退出执行函数
这个线程执行完方法集合发现!quit为false不能进入循环,就退出了,届时,全部线程就已经退出了,只有一个线程执行了方法集合!!!
按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false)
{
pthread_mutex_unlock(&mutex);
// 解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过 -- 发生线程切换
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
条件变量使用规范
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
概念
生产者消费者模式就是通过一个容器(链表或队列)来解决生产者和消费者的强耦合问题
生产者和消费者彼此之间不直接通讯,而是通过阻塞队列来进行通讯
所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取
阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的
如果还是不理解的话,请看下面的例子
消费者线程有多个,消费者之间是什么关系呢?
供应商线程有多个,供应商之间是什么关系呢?
消费者和供应商之间又是什么关系呢?
总结:⭐⭐⭐⭐⭐
生产者消费者模型优点:解耦、支持并发、支持忙闲不均
BlockingQueue:
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构
其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素(这里要设置条件判空,设置条件变量)
当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
实现方式:
主要问题:消费者和生产者的三种关系要控制好
唤醒各自线程的时机,不要乱加锁,可能造成死锁或一直等待的问题
blockqueue.hpp
#include
#include
#include
#include
using namespace std;
const int DfCap = 5; // 默认容量大小
namespace Mybq
{
template <typename T>
class BlockQueue
{
public:
BlockQueue(uint32_t cap = DfCap)
: _cap(cap), _bq()
{
pthread_mutex_init(&mutex, nullptr);
pthread_cond_init(&conCond, nullptr);
pthread_cond_init(&proCond, nullptr);
}
// 生产者生产
void push(const T &val)
{
// 加锁 -- 保护临界资源(队列)
lockQueue();
// 循环判断阻塞队列是否为满 -- 防止伪唤醒 -> proPendwait调用失败、多线程竞争锁或系统原因等等...
while (isFull())
{
// 等待的时候,自动释放mutex锁
proPendwait(); // 阻塞等待,等待被唤醒
// 等待完后,是在临界区醒的,重新申请mutex锁
}
// 加载资源 -> 解锁
_bq.push(val);
unlockQueue();
// 生产者唤醒消费者,因为生产者已经把资源放到队列里面了(条件变量就绪)
weakupJCon();
}
// 消费者消费
T pop()
{
lockQueue();
// 防止伪唤醒
while (isEmpty())
{
// 阻塞等待,等待被唤醒
conPendWait();
}
// 删除资源->解锁
T val = _bq.front();
_bq.pop();
unlockQueue();
// 消费者唤醒生产者,因为队列的资源没有了(条件变量就绪)
weakupPro();
return val;
}
~BlockQueue()
{
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&conCond);
pthread_cond_destroy(&proCond);
}
private:
// 判断阻塞队列是否为空,是否为满
bool isFull()
{
return _bq.size() == _cap;
}
bool isEmpty()
{
return _bq.empty();
}
//-------------------------------------------
// 互斥锁加锁解锁
void lockQueue()
{
pthread_mutex_lock(&mutex);
}
void unlockQueue()
{
pthread_mutex_unlock(&mutex);
}
//--------------------------------------------
// 在条件变量下阻塞等待,等待被唤醒
void proPendwait()
{
// 1. 条件变量在阻塞等待线程的时候,会自动释放mutex互斥锁!!!
// 释放锁:因为阻塞等待的线程占用着锁,其他线程不能申请 -- 没有锁了意味着不能访问临界资源
pthread_cond_wait(&proCond, &mutex);
// 2. 当阻塞结束(唤醒),返回时,pthread_cond_wait,会自动帮你重新获取mutex锁,最后才返回
}
void conPendWait()
{
pthread_cond_wait(&conCond, &mutex);
}
//--------------------------------------------
// 条件变量就绪,被唤醒阻塞等待的程序
void weakupPro()
{
// 消费者唤醒生产者,因为队列的资源没有了(消费者调用)
pthread_cond_signal(&proCond);
}
void weakupJCon()
{
// 生产者唤醒消费者,因为生产者已经把资源放到队列里面了(生产者调用)
pthread_cond_signal(&conCond);
}
private:
uint32_t _cap; // 阻塞队列容量大小
queue<T> _bq;
pthread_mutex_t mutex; // 阻塞队列互斥锁
pthread_cond_t conCond; // 消费者条件变量
pthread_cond_t proCond; // 生产者条件变量
};
}
test.cpp
#include "blockqueue.hpp"
#include
#include
// 生产者线程执行函数
void *CallProducer(void *args)
{
Mybq::BlockQueue<int>* bqp = static_cast< Mybq::BlockQueue<int>*>(args);
while (true)
{
// 生产数据 -- [0, 100]
int data = rand() % 100 + 1;
bqp->push(data);
cout << "Producer data sucess, data: " << data << endl;
sleep(2);
}
return nullptr;
}
// 消费者线程执行函数
void *Callconsumer(void *args)
{
Mybq::BlockQueue<int>* bqp = static_cast< Mybq::BlockQueue<int>*>(args);
while (true)
{
// 消费数据
int data = bqp->pop();
cout << "Consumer data sucess, data: " << data << endl;
}
return nullptr;
}
int main()
{
srand((unsigned int)time(nullptr)); // 随机数种子
Mybq::BlockQueue<int> bq;
// 多线程测试
pthread_t producer; // 生产者线程
pthread_t consumer; // 消费者线程
// &bq将阻塞队列的地址传给线程执行函数的参数
pthread_create(&producer, nullptr, CallProducer, &bq);
pthread_create(&consumer, nullptr, Callconsumer, &bq);
// 等待线程
pthread_join(producer, nullptr);
pthread_join(consumer, nullptr);
return 0;
}
总结