目录
[3.1 互斥锁的理解](#3.1 互斥锁的理解)
[3.2 互斥锁的接口](#3.2 互斥锁的接口)
[4.1 条件变量的理解](#4.1 条件变量的理解)
[4.2 条件变量的使用](#4.2 条件变量的使用)

一、引言
在上一篇文章里,我们聊了线程是如何被创建、取消、等待和分离的,这些都挺重要。但是说到线程,还有很重要的一组话题就是线程的同步与互斥。本文将介绍互斥锁、条件变量和生产者消费者模型,最后基于阻塞队列来实现多生产者多消费者模型。
二、基础概念
在学习线程的同步与互斥之前,我们需要理解和线程息息相关的一些概念。
1)临界资源:一次仅允许一个执行流访问的共享资源。
2)临界区:访问共享资源的代码段。
3)互斥:任何时候,互斥保证只有一个执行流进入进入临界区访问临界资源。
4)原子性:不会被任何调度机制打断的操作,做就做完,要么不做。
在多线程并发执行的过程中,临界资源是需要被保护起来的,确保一次只能有一个执行流访问,为的是保护临界资源。互斥锁就起到了保护的作用。
三、互斥锁
3.1 互斥锁的理解

多个执行流在进入临界区前,需要去竞争互斥锁,哪个进程拿到了这把锁,这个进程就可以进入临界区访问共享资源。没有竞争到锁的线程就会被阻塞,等待锁资源释放后再去竞争锁。
3.2 互斥锁的接口
1)初始化
互斥锁初始化的方式有两种,取决于我们定义的是全局的互斥锁还是局部的互斥锁。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态初始化
- 通过宏来初始化全局的互斥锁,这个宏不能用来初始化局部的互斥锁,不需要自己手动释放,系统会自己回收。
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
- pthread_mutex_t *mutex:指向要初始化的互斥锁的指针。
- const pthread_mutexattr_t *attr:指向互斥锁属性的指针,一般设为空,表示使用默认属性。
- 返回值:0表示成功,非0表示错误。
2)销毁
#include <pthread.h>
int pthread_mutex_destroy(pthead_mutex_t *mutex);
- pthead_mutex_t *mutex:指向要销毁的互斥锁的指针。
- 返回值:0表示成功,非0表示失败。
3)加锁和解锁
#include <pthread.h>
int pthread_mutex_lock(pthead_mutex_t *mutex);
int pthread_mutex_unlock(pthead_mutex_t *mutex);
- pthead_mutex_t *mutex:指向互斥锁的指针。
- 返回值:0表示成功,非0表示失败。
在调用 pthread_mutex_lock函数时,如果没有竞争到锁,那么该进程就会被阻塞。
四、条件变量
4.1 条件变量的理解
条件变量(Condition Variable)是一种同步机制,用于线程间的通信。它允许线程在某个条件不满足时进入等待状态,直到其他线程通知条件可能已改变。条件变量通常与互斥锁(Mutex)配合使用,因为检测资源是否就绪本身就是在访问共享资源,访问共享资源自然就需要互斥锁来保护,所以我们通常会看到互斥锁和条件变量一起使用。
4.2 条件变量的使用
1)初始化
条件变量的初始化和互斥锁的初始化很类似,也有两种方式。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;/ /静态初始化
- 用来初始化全局的条件变量或者静态条件变量(static修饰),也不需要自己手动销毁。
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
- pthread_cond_t *cond:指向要初始化的条件变量的指针。
- pthread_condattr_t *attr:指向条件变量属性的指针,一般设为空表示使用默认属性。
- 返回值:0表示成功,非0表示失败。
用来初始化一个局部的条件变量。
2)销毁
int pthread_cond_destroy(pthread_cond_t *cond);
- pthread_cond_t *cond:指向要销毁的条件变量的指针。
- 返回值:0表示成功,非0表示失败。
销毁一个条件变量。
3)等待
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
- pthread_cond_t *cond:指向要等待的条件变量的指针。
- pthread_mutex_t *mutex:指向互斥锁的指针。
- 返回值:0表示成功,非0表示失败。
线程一旦调用这个函数,就会到对应的条件变量下等待,等待条件满足,其他线程会唤醒它。这个函数的第二个参数需要解释一下,之所以要把互斥锁传入,是因为不能持有锁就去等待了。这样会导致其他线程得不到锁,一直阻塞,而持有锁的线程有需要其他线程唤醒,这就导致死锁了。这个函数会将传入的互斥锁进行释放,让给其他线程。
4)唤醒
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
- pthread_cond_t *cond:指向条件变量的指针。
- 第一个函数:唤醒一个在该条件变量下等待的线程。
- 第二个函数:唤醒所有在该条件变量下等待的线程。
- 返回值:0表示成功,非0表示失败。
至此,线程同步与互斥的手段我们已经明白了。
五、生产者消费者模型

如果没有缓冲区,那么将会是生产者生产数据,然后消费者从生产者处拿数据。这样设计的话,生产者和消费者之间的耦合度很高。如果生产者生产的数据超出了消费者消费的能力,那么生产者只能等待消费者。而加入一个缓冲区以后,只要缓冲区未满,生产者就可以持续生产数据,不用考虑消费者是否来得及消费。这样一来,生产者和消费者之间的耦合度就降低了很多。
上图虽然只画出了一个生产者和一个消费者,但是是支持多生产者多消费者并发的,我们下面的代码也是多生产者多消费者。
六、代码
cpp
#ifndef _BLOCKQUEUE_HPP_
#define _BLOCKQUEUE_HPP_
#include <pthread.h>
#include <queue>
const int default_size = 10;//默认大小
class BlockQueue {
public:
BlockQueue(int size = default_size)
: _capacity(size)
, _producter_wait_num(0)
, _consumer_wait_num(0)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_not_full, nullptr);
pthread_cond_init(&_not_empty, nullptr);
}
void push(int data) {
pthread_mutex_lock(&_mutex);
while (_queue.size() == _capacity) { //不用if的原因在于防止伪唤醒
_producter_wait_num++;
pthread_cond_wait(&_not_full, &_mutex);//生产者等待,等待队列不满
_producter_wait_num--;
}
_queue.push(data);
if (_consumer_wait_num > 0) {
pthread_cond_signal(&_not_empty);//唤醒消费者
}
pthread_mutex_unlock(&_mutex);
}
void pop(int &data) {
pthread_mutex_lock(&_mutex);
while (_queue.empty()) {//不用if的原因在于防止伪唤醒
_consumer_wait_num++;
pthread_cond_wait(&_not_empty, &_mutex);//消费者等待,等待队列不为空
_consumer_wait_num--;
}
data = _queue.front();
_queue.pop();
if (_producter_wait_num > 0) {
pthread_cond_signal(&_not_full);//唤醒生产者
}
pthread_mutex_unlock(&_mutex);
}
~BlockQueue() {
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_not_full);
pthread_cond_destroy(&_not_empty);
}
private:
std::queue<int> _queue;
pthread_mutex_t _mutex;
pthread_cond_t _not_full;
pthread_cond_t _not_empty;
int _capacity;//队列容量
int _producter_wait_num; // 生产者等待的数量
int _consumer_wait_num; // 消费者等待的数量
};
#endif // _BLOCKQUEUE_HPP_
运行程序后,你将看到生产者线程和消费者线程的输出,验证阻塞队列的正确性。生产者线程会依次将数据放入队列,消费者线程会依次从队列中取出数据。由于多线程的并发性,输出顺序可能不固定,但数据的生产与消费应该是匹配的。
七、结语
"路漫漫其修远兮,吾将上下而求索。"
上一篇链接:https://blog.csdn.net/bit_pan/article/details/151579387?spm=1001.2014.3001.5502
完~