同步
在使用互斥时我们会发现一个问题,如果一个线程频繁地使用互斥锁来占用临界资源,会严重影响别的线程的效率,而同步就是在保证数据安全的情况下让多线程的执行具有一定的顺序性,以此提高运行效率。
条件变量
创建条件变量跟创建互斥锁类似,在全局定义一个pthread_cond_t类型的变量即可。
接口
初始化
- 动态初始化
pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
- 静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
销毁
int pthread_cond_destroy(pthread_cond_t *cond);
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒所有线程
int pthread_cond_signal(pthread_cond_t *cond); // 唤醒某个进程(阻塞队列中的第一个)
案例
在这个例子共创建了两类进程,第一类是一个控制进程(master),第二类是三个工作进程(worker),master每秒释放一次一个信号让其中一个worker工作
const int NUM = 3;
pthread_mutex_t mtx;
pthread_cond_t cond;
void* ctrl(void* args)
{
string name = (char*)args;
while (true)
{
// 唤醒在条件变量下等待的一个线程
// 在条件变量等待队列中的第一个线程
pthread_cond_signal(&cond);
sleep(1);
}
}
void* work(void* args)
{
int number = *(int*)args;
delete (int*)args;
while (true)
{
pthread_cond_wait(&cond, &mtx);
cout << "worker: " << number << " is working···" << endl;
}
}
int main()
{
pthread_mutex_init(&mtx, nullptr);
pthread_cond_init(&cond, nullptr);
pthread_t master;
pthread_t worker[NUM];
pthread_create(&master, nullptr, ctrl, (void*)"master");
for (int i = 0; i < NUM; i++)
{
int *number = new int(i);
pthread_create(worker + i, nullptr, work, (void*)number);
}
for (int i = 0; i < NUM; i++)
{
pthread_join(worker[i], nullptr);
}
pthread_join(master, nullptr);
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&cond);
return 0;
}
从运行情况中我们会发现worker每次被唤醒是按照一定顺序的,从此我们可以推断出其实条件变量内部存在一个等待队列
生产者消费者模型
让我们来思考一下去超市买东西的例子,我们去超市买的东西其实是超市从供货商进的,那么为什么消费者不能自己去供货商买东西呢,供货商为什么不能自己承担销售的任务呢?归根结底为的是减少交易成本和提高效率,因为供货商往往在比较偏僻的地方,消费者不希望每次购物都跑这么远的路;而对于供货商来说,他们每天会生产大量的货物,消费者每次购买商品的数量往往是很小的,这样的销售不但麻烦而且销量也远远不能达到供货商的预期,不同于普通消费者,超市具有收集需求,他每次都会进大量的货,供货商只需进行几次交易就可以出售大量的商品。另外,当供货商的生产出现问题时,因为超市内有大量的存货,因此在短期内消费者的消费并不会收到影响,也就是说超市将生产消费环节进行了“解耦”,这也是提高效率的一个方面。
- 供货商 vs 供货商:竞争 互斥
- 消费者 vs 消费者:竞争 互斥
- 供货商 vs 消费者:互斥 同步
在这个模型里包含的只有生产者、消费者这2个角色间的这3种关系,超市只是一个交易场所(临界资源)。
BlockingQueue
现在模拟实现一个BlockingQueue
namespace ssj_blockqueue
{
const int default_cap = 5;
template <class T>
class BlockQueue
{
private:
queue<T> _bq;
int _cap;
pthread_mutex_t _mtx;
// 当生产满了的时候,就应该不要生产了(不要竞争锁了),而应该让消费者来消费
// 当消费空了,就不应该消费(竞争锁),应该让生产者来进行生产
pthread_cond_t _is_full; // _bq满,消费者在该条件变量下等待
pthread_cond_t _is_empty; // _bq满,消费者在该条件变量下等待
private:
bool IsFull()
{
return _bq.size() == _cap;
}
bool IsEmpty()
{
return _bq.size() == 0;
}
void LockQueue()
{
pthread_mutex_lock(&_mtx);
}
void UnlockQueue()
{
pthread_mutex_unlock(&_mtx);
}
void ProducterWait()
{
// pthread_cond_wait
//1. 调用的时候,会首先自动释放_mtx,然后再挂起自己
//2. 返回的时候,会首先自动竞争锁,获取到锁之后,才能返回
pthread_cond_wait(&_is_empty, &_mtx);
}
void ConsumerWait()
{
pthread_cond_wait(&_is_full, &_mtx);
}
void WakeupConsumer()
{
pthread_cond_signal(&_is_full);
}
void WakeupProducter()
{
pthread_cond_signal(&_is_empty);
}
public:
BlockQueue(int cap = default_cap)
: _cap(cap)
{
pthread_mutex_init(&_mtx, nullptr);
pthread_cond_init(&_is_empty, nullptr);
pthread_cond_init(&_is_full, nullptr);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mtx);
pthread_cond_destroy(&_is_empty);
pthread_cond_destroy(&_is_full);
}
public:
void Push(const T &in)
{
LockQueue();
while (IsFull()) // 使用循环避免伪唤醒和挂起失败
{
ProducterWait();
}
_bq.push(in);
if (_bq.size() > _cap / 2) WakeupConsumer();
UnlockQueue();
}
void Pop(T *out)
{
LockQueue();
while (IsEmpty())
{
ConsumerWait();
}
*out = _bq.front();
_bq.pop();
if (_bq.size() < _cap / 2) WakeupProducter();
UnlockQueue();
}
};
}