线程同步
线程互斥,解决了数据不一致的问题;
但是我们可以发现加了互斥锁,一个线程想要访问临界资源时,就要先申请锁,申请锁失败就只能等待其他进程访问完成,释放万锁才能访问临界资源。
那如果线程申请互斥量不成功,就会挂起等待;在互斥量释放后才能继续运行;
但是,如果一个线程申请互斥量成功后,频繁的释放和申请互斥锁;那其他线程申请不到互斥量,就会一直等待,造成:线程饥饿问题
假设现在存在一个消息队列
msg_queue
,线程produce
要向队列中写数据,而线程consume
要从队列中读取数据;
produce
要写数据时,如果consume
正在读数据,那就只能等待consume
读取完,produce
才能进行写入。
而同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避饥饿
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从⽽有效避免饥饿问题,叫做同步。
条件变量
要实现线程之间的同步,有很多种方法;这里来了解条件变量:
- 当一个线程互斥地访问某个变量时,可能在其他线程改变状态之前,只能等待。
- 例如:应该线程访问队列时,队列为空,它只能等待,其他线程将一个节点添加到队列中;才能继续向下执行
信号量:pthread_cond_t
1. 初始化条件变量
和互斥量一样,初始化条件变量可以使用PTHREAD_COND_INITIALIZER
来初始化;
也可以调用pthread_cond_init
来初始化:
c
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
参数一:指要初始化的条件量。
参数二:可以设置条件量的相关属性,
nullptr
表示默认。返回值:
初始化成功返回
0
,失败返回对应的错误码(非0)
2. 销毁条件变量
与互斥量一样,创建出的条件变量要销毁;
pthread_cont_destroy
c
int pthread_cond_destroy(pthread_cond_t *cond)
参数:传递要销毁的条件量;
销毁成功返回
0
,失败则返回对应错误码(非0
)
此外,调用pthread_cond_destroy
也要注意:
在调用pthread_mutex_destroy
时要注意:
- 使用
PTHREAD_COND_INITIALIZER
初始化的条件量不能销毁 - 对于要销毁的条件量,要保证后面不会再被使用
3. 等待
当某种条件不满足时,需要等待,就要调用pthread_cond_wait
进行等待;
c
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
这里,当条件不满足,需要线程等待时就需要调用pthread_cond_wait
让线程在条件变量等待
参数:
pthread_cond_t* cond
:在该条件变量等待
pthread_mutex_t* mutex
:互斥锁,线程等待之前可能申请了互斥量,调用pthread_cond_wait
让线程等待的同时释放信号量。
4. 唤醒
当条件不满足时,需要让线程等待;而等条件满足时也要唤醒线程让线程继续运行
c
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_broadcast
:唤醒cond
条件变量下的所有等待的线程。
pthread_cond_signal
:唤醒cond
条件变量下的等待的一个线程
生产者消费者
对于生产者消费者模型,简单来说就是生产者向仓库中放数据(生产)、消费者从仓库中取数据(消费)

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题;生产者和消费者不能直接通讯,而是通过阻塞队列来进行通讯。
生产者生产完数据后无序等待消费者来处理,直接放入阻塞队列中;消费者不需要通过生产者来获取数据,而是直接从阻塞队列中获取。
阻塞队列就相当于一个缓冲区,使得生产者生产数据和消费者处理数据能够同时进行。
在生成者和消费者模型中,存在生产者和消费者两种角色。
同时存在着三种关系:
- 生产者和消费者之间:互斥与同步关系
- 生产者和生产者之间:竞争关系、互斥关系
- 消费者和消费者之间:互斥关系
(简单来说就是,生产者生产数据和消费者获取数据存在一定的顺序;生产者之间需要竞争临界资源、一个生产者生产数据过程中其他生产者不能生成;消费者和消费者之间互斥)
最后,对于交易场所(阻塞队列)就是以特定结构构成的内存空间。
基于block_queue
实现生产者消费者模型
在多线程编程中,阻塞队列(blockqueue
)是一种常用于实现生产者和消费者模型的数据结构;
和普通队列的区别:
当队列为空时,从队列中获取数据的操作将会被阻塞,直到队列中放入了数据;
当队列为满时,向队列中存放数据的操作也会被阻塞,直到有元素从队列中取出,
要基于blockqueue
实现生产者消费者模型,这里思考一下:
首先,肯定要存在一个存放数据的队列,并且我们要知道队列中数据的个数以及队列的最大容量(判断队列是否为满/空)
其次,还要实现生产者于生产者之间的互斥,消费者和消费者之间的互斥;就势必要存在锁(互斥量)
- 当队列为满时,生产者线程就会阻塞等待,直到消费者线程获取数据之后,生产者才能继续生成;
- 当队列为空时,消费者线程就会阻塞等待,直到生产者线程生成数据之后,消费者才能继续消费。
要实现上述生产者生产和消费者消费按照一定顺序执行,那也势必存在生产者和消费者所对应的条件变量
最后,对于阻塞等待的生产者/消费者线程何时唤醒,这里可以对阻塞等待的生产者和消费者线程进行计数(当生产者生产完数据后,当前队列肯定是存在数据的,如果有消费者线程在阻塞等待,就可以将其唤醒;当消费者消费完数据后,当前队列肯定是不为满的,如果有生产者线程在阻塞等待,就可以将其唤醒。)
cpp
static int SIZE_MAX = 5; // 队列最大容量
template <class T>
class blockqueue
{
bool Full() { return _q.size() >= SIZE_MAX; }
bool Empty() { return _q.empty(); }
public:
blockqueue(int sz = SIZE_MAX) : _sz(sz), _psleep(0), _csleep(0)
{
pthread_mutex_init(&_produce_mutex, nullptr);
pthread_mutex_init(&_consume_mutex, nullptr);
pthread_cond_init(&_produce_cond, nullptr);
pthread_cond_init(&_consume_cond, nullptr);
}
~blockqueue()
{
pthread_mutex_destroy(&_produce_mutex);
pthread_mutex_destroy(&_consume_mutex);
pthread_cond_destroy(&_produce_cond);
pthread_cond_destroy(&_consume_cond);
}
private:
std::queue<T> _q; // 队列
pthread_mutex_t _mutex; // 互斥量
pthread_cond_t _produce_cond; // 生产者条件变量
pthread_cond_t _consume_cond; // 消费者条件变量
int _psleep; // 生产者阻塞等待的线程数
int _csleep; // 消费者阻塞等待的线程数
};
有了上述框架,那就来实现生产者生产数据和消费者消费数据:
1. 生产者生产
生产者要生产数据,如果现在有生产者线程正在生产或者消费者线程正在消费,为了保证数据的一致性,那该生产者线程就要阻塞等待。
通过代码实现就是:申请互斥量
申请锁成功,如果当前队列为满,则生产者不能继续生产要等待;如果队列不为满,则可以进行生产。
生成完数据之后,如果存在阻塞等待中的消费者线程,就可以将其唤醒。
cpp
void Produce(const T &data)
{
// 申请锁
pthread_mutex_lock(&_mutex);
while (Full())
{
// wait
_psleep++;
pthread_cond_wait(&_produce_cond, &_mutex);
_psleep--;
}
// 生产数据
_q / push(data);
if (_csleep > 0)
{
// 唤醒消费者线程
pthread_cond_signal(&_consume_cond);
}
pthread_mutex_unlock(&_mutex);
}
注意:这里判断队列是否为满,使用的是
while
而不是if
如果使用
if
:如果当前生产者线程阻塞等待被唤醒,它就会阻塞在_mutex
互斥量处,如果此时存在另一个生产者线程竞争互斥量成功,并且占用了最后一个空位置;此时队列为满,当前线程阻塞在_mutex
处,只要申请成功信号量就可以进行生产操作导致数据丢失。简单来说就是使用
while
保证生产者线程在执行生成操作时队列不为满。
2. 消费者消费
消费者要生产数据,如果现在有生产者线程正在生产或者消费者线程正在消费,为了保证数据的一致性,那该消费者线程就要阻塞等待。
通过代码实现就是:申请互斥量
申请锁成功,如果当前队列为空,则消费者不能继续消费要等待;如果队列不为空,则可以进行消费。
生成完数据之后,如果存在阻塞等待中的生产者线程,就可以将其唤醒。
cpp
void Consume(T *data)
{
// 申请锁
pthread_mutex_lock(&_mutex);
while (Empty())
{
// wait
_csleep++;
pthread_cond_wait(&_consume_cond, &_mutex);
_csleep--;
}
*data = _q.pop();
if (_psleep > 0)
{
//唤醒生产者线程
pthread_cond_signal(&_produce_cond);
}
pthread_mutex_unlock(&_mutex);
}
pthread_cond_wait
参数
了解了pthread_cond
系列参数,现在在来看pthread_cond_wait
的参数:
c
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
为什么调用pthread_cond_wait
阻塞等待时,需要参数mutex
?
通过上述代码也不难理解,当调用pthread_cond_wait
时,我们线程是可能申请了互斥量的;如果线程不释放该信号就去阻塞在该条件变量下,这就会导致非常多线程无法申请互斥量,等待该阻塞等待的线程释放互斥量。
所以调用pthread_cond_wait
需要参数mutex
,这样在该线程要阻塞等待之前,先释放互斥量mutex
;
在被唤醒时,该线程就行阻塞在互斥量mutex
处,等待申请互斥量。
cond
条件变量封装
这里简单封装一下条件变量
cpp
class cond
{
public:
cond()
{
pthread_cond_init(&_cond, nullptr);
}
~cond()
{
pthread_cond_destroy(&_cond);
}
// pthread_cond_wait()
void Wait(pthread_mutex_t &mutex)
{
pthread_cond_wait(&_cond, &mutex);
}
void Signal()
{
pthread_cond_signal(&_cond);
}
void Broadcast()
{
pthread_cond_broadcast(&_cond);
}
private:
pthread_cond_t _cond;
};
到这里本篇文章内容就结束了,感谢支持