概述
文章目录
- 线程同步
- 条件变量
- 条件变量相关接口
- 生产者消费者模型基本理论
- 基于阻塞队列的生产者消费者模型
- 再次理解生产者消费者模型
- 总结生产者消费者模型
线程同步
什么是线程同步?
首先先了解一下:互斥锁带来的一个问题:就是线程饥饿现象:就是多个线程长时间访问不到共享资源,不得已使得线程的执行流得以推进执行,这种现象就是线程饥饿现象!
有线程饥饿线程的原因就是互斥锁带来的问题!
有一种场景是,当一个线程A获得锁时候,进入临界区,访问结束后,释放锁,该线程A大概率的竞争能力比其他线程更加强,有可能重新获得锁,再次访问临界区!而其他线程可能竞争力不够强,长时间无法获得锁,只有一个线程A获得锁,进入临界区,释放锁!又再次获得锁...;
这种线程导致的其他线程的执行流无法推进,导致无法进入临界区,处于饥饿现象!
为了解决这种现象:我们搞出一个东西,使得刚释放锁的线程,不能够立马再次获得锁!这就是等待队列,让释放锁的线程,进入等待队列等待,这样其他线程就有机会竞争锁的资源了!
而线程同步就是:
在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步;
条件变量
条件变量是解决多线程同步的一种方式!
对于临界资源,我们不仅仅是想访问它,我们还想知道临界资源被访问的状态!
要知道临界资源的状态如何,这就可以通过条件变量知晓!
条件变量相关接口
1.条件变量的创建和销毁!
2.条件变量等待
该函数就是在某个条件变量cond下等待;等待共享资源的状态是否就绪;
- 唤醒通知等待线程
当某个线程在等待的时候,它是如何知道该资源的状态如何了呢?那就是通过另一个线程唤醒在该条件变量cond等待的线程!
使用上面的接口,来完成一个线程,控制其他线程运行的程序!
ctr线程:控制5个work线程的运行!
#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>
using namespace std;
#define NUM 5
pthread_mutex_t mutex;
pthread_cond_t cond;
//让ctr线程去控制work线程,让其定期运行
void* ctr(void* argc)
{
while(true)
{
//唤醒在该条件cond下等待的线程,唤醒哪一个呢?
//唤醒的是在cond条件变量队列里等待的第一个线程!
pthread_cond_signal(&cond);
cout<<"ctr say : begin work::";
sleep(1);
}
return (void*)0;
}
void* work(void* argc)
{
int number = *(int*)argc;
while(true)
{
//让该线程在cond条件下等待其他线程给他发送指令才开始工作
pthread_cond_wait(&cond,&mutex);
cout<<"work["<<number<<"] is working..."<<endl;
}
return (void*)0;
}
int main()
{
//初始化条件变量和互斥锁
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
//创建五个工作线程
pthread_t workerThread[NUM];
pthread_t ctrTrhead;
for (int i = 0; i < NUM; i++)
{
int* num = new int(i);
pthread_create(&workerThread[i], NULL,work,(void*)num);
}
//创建一个控制线程
pthread_create(&ctrTrhead,NULL,ctr,NULL);
//释放线程资源
for(int i=0;i<NUM;i++)
{
pthread_join(workerThread[i], NULL);
}
pthread_join(ctrTrhead,NULL);
//摧毁条件变量和互斥锁
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
我们观察结果!
通过结果我们发现:确实是一个线程控制了其他线程去工作!并且,我们发现woker线程虽然不知道是哪个线程先开始运行,但是我们却知道他们的运行顺序确实一样的!
如上面的线程:work线程创建时候是0 1 2 3 4 的顺序创建的,但是调用却是 2 3 0 1 4,并且往后的调用顺序都是 2 3 0 1 4;
这个现象说明:条件变量里面必定存在一个等待队列,而这个pthread_cond_wait(&cond,&mutex);
操作就是把调用它的线程,放入条件变量的等待队列中!
假如把上面的ctr线程里的pthread_cond_signal(&cond);
改成 pthread_cond_broadcast(&cond);
那么就是从ctr唤醒一个线程,变成唤醒全部线程,那么执行结果就是如下图!全部线程都开始执行了!
`
生产者消费者模型基本理论
生产者消费者模型我们要关注三个点:
3个关系:
生产者和生产者:竞争关系(互斥);
消费者和消费者:竞争关系(互斥);
生产者和消费者:竞争关系(互斥),协同关系(同步);
2个角色:
生产者和消费者两个角色;(对应代码就是两种线程的执行流,注意这里是两种,不是两个;这说明生产者可以有多个消费者也可以有多个);
1个缓冲区:
这个缓冲区就是内存空间,或者STL容器等;
为何要使用生产者消费者模型?
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之
间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接
扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲
区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
基于阻塞队列的生产者消费者模型
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构;其与
普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元
素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是
基于不同的线程来说的,线程在对阻塞队列进行操作时会被阻塞);
为了便于理解:这里演示的是单生产者和单消费者的模型;所以说,在这里我们只需要处理生产者和消费者之间的同步和互斥关系就行!
这里两个文件;
文件1:BlockQueue.hpp
:封装阻塞队列的头文件;
文件2:cp_test.cc
:主函数运行两个线程(生产者和消费者)的文件;
首先文件1的代码:如下
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
template <class T>
class BlockQueue
{
private:
std::queue<T> _block_queue; //阻塞队列
int _capacity; //队列的元素容量
pthread_mutex_t _mutex; //保护临界资源的锁(容器和容量)
//当生产者生产的容器满了,就不应该再生产了(就是不要再竞争锁的资源了),而让消费者来消费
//当消费者消费的容器空了,就不应该再消费了(就是不要再竞争锁的资源了),而让生产者来生产
pthread_cond_t _is_full; //_block_queue满了,消费者就应该在该条件变量等对方通知来消费
pthread_cond_t _is_empty; //_block_queue空了,生产者就应该在该条件变量等对方通知来生产
public:
BlockQueue(const int capcity = 5) : _capacity(capcity)
{
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_is_empty, nullptr);
pthread_cond_init(&_is_full, NULL);
}
private:
bool isFull() const
{
return _capacity == _block_queue.size();
}
bool isEmpty() const
{
return _block_queue.size() == 0;
}
void lockBlockQueue()
{
pthread_mutex_lock(&_mutex);
}
void unlockBlockQueue()
{
pthread_mutex_unlock(&_mutex);
}
//生产者等待阻塞队列为空,才可以继续生产
void producerWait()
{
//该函数pthread_cond_wait(&_is_empty,_mutex);内部是干了两个动作:
//调用时候,先释放锁的资源,再把调用该函数的线程挂起
//返回时候,该线程先主动去和其他线程竞争锁的资源,当竞争成功后,才可以返回到该函数
pthread_cond_wait(&_is_empty, &_mutex);
}
//唤醒生产者可以开始生产数据
void wakeUpProducer()
{
pthread_cond_signal(&_is_empty);
}
//消费者等待阻塞队列满了,才可以继续消费
void consumerWait()
{
pthread_cond_wait(&_is_full, &_mutex);
}
//唤醒消费者可以开始消费数据
void wakeUpConsumer()
{
pthread_cond_signal(&_is_full);
}
public:
/*
** 一般而言
** const T& 表示是输入参数
** T& 表示输入输出参数
** T* 表示输出参数
*/
//但是我们的生产和消费阻塞队列里的数据,是需要保证数据的安全性(通过加锁来解决)
//原因就是STL容器它本身就是不保证数据安全的,线程不安全的,也就是多线程访问情况下,会不安全
//向阻塞队列生产数据
void push(const T &in)
{
//加锁
lockBlockQueue();
//生产数据前判断阻塞队列是否为满
/*
** 当我们对条件变量进行检测的时候,我们需要使用循环的方式检测
** 保证退出循环一定是条件不满足的时候退出的
*/
while (isFull())
{
//生产者满了,就让它去等待,等待什么?等待到队列为空,才可以继续生产
producerWait();
}
_block_queue.push(in);
//当上面操作完成,说明生产者生产数据成功!那么生产者就可以通知消费者来消费了
//只有生产者知道消费者什么时候要消费
wakeUpConsumer();
//解锁
unlockBlockQueue();
}
//从阻塞队列获取数据,获取数据保存在out变量中
void pop(T *out)
{
//加锁
lockBlockQueue();
//消费数据前判断阻塞队列是否为空
/*
** 当我们对条件变量进行检测的时候,我们需要使用循环的方式检测
** 保证退出循环一定是条件不满足的时候退出的
*/
while(isEmpty())
{
consumerWait();
}
*out = _block_queue.front();
_block_queue.pop();
//当上面操作完成,说明消费者消费数据成功!那么消费者就可以通生产者者来生产了
//只有消费者知道什么时候生产者要生产
wakeUpProducer();
//解锁
unlockBlockQueue();
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_is_empty);
pthread_cond_destroy(&_is_full);
}
};
补充说明一下:
为什么需要封装STL的队列容器?直接拿来用不就好了吗?
不可以直接用STL容器来完成生产者和消费者两个线程的模型,因为STL的容器时没有保证线程安全的,也就是生产者和消费者两个线程访问STL的队列容器时候,是会异步访问的;所以为了保护多线程访问STL的容器安全,必须对其进行封装!
阻塞队列的共享资源都有那些?
在该模型下:共享资源就是
std::queue< T > _block_queue
和int _capacity
;
为了保护共享资源的访问,我们引入了pthread_mutex _mutex
锁,目的是多线程(生产者和消费者)访问共享资源(_block_queue)时候,能够安全的操作!
同时为了保证数据的顺序访问,且保证线程饥饿现象不得已出现,我们要保证多线程都可以合理的获得共享资源的访问权;我们引入了两个条件变量:is_full 和 is_empty;
is_full :表示生产者线程在生产数据满时候,就需要等待消费者线程去消费;
is_empty:表示消费者线程消费数据为空时候,就需要等待生产者去生产数据;
对于pthread_cond_wait(&is_full,&_mutex)
函数,即条件变量等待函数,它内部的实现原理是:
线程调用该函数时候,首先第一件是就是释放锁的资源,然后把该线程挂起!
当该函数返回时候:首先是去竞争锁的资源,然后竞争成供后,再返回到调用该函数的线程;
为什么需要线程调用该函数时候,首先第一件是就是释放锁的资源,然后把该线程挂起?
就是因为当我们调用该函数时候,都是在临界区里面调用的,假如这个等待函数,没有设计一把锁的话,那么当这个函数调用成功,就会带着锁走被挂起,这个锁就是锁住临界区的锁,一旦带着锁走被挂起,那么其他线程都根本没有机会进入临界区了,因为锁都被挂起还没被释放,其他线程就是根本没机会获得锁进入临界区,其他线程没机会进入临界区,也就没机会唤醒被挂起的线程,这就会导致一个死锁的问题;
为什么需要当该函数返回时候:首先是去竞争锁的资源,然后竞争成供后,再返回到调用该函数的线程;
因为当该函数返回的时候,是在临界区中的,该函数返回了就需要继续往下执行代码,下面的代码也是临界区的代码,假如不给自己上锁,那么就会有其他线程进入临界区,这样会导致数据错乱的问题;
当我们生产者生产了数据,消费者怎么知道有数据可以消费了呢?同时当我们消费者消费数据了后,生产者又怎么知道可以去继续生产数据了呢?
要使得双发都知道接下来要干什么。那么就必须需要对方通知去做自己的事!
只有生产者才知道消费者什么时候才应该消费数据;
只有消费者才知道生产者什么时候应该生产数据;
以上就是两种角色的相互通知告知的过程!这个过程就体现再代码层次就是调用
pthread_cond_signal(&_is_full);
和 pthread_cond_signal(&_is_empty);
函数;
cp_test.cc
的代码:生产者和消费者两个函数
#include "BlockQueue.hpp"
#include <ctime>
#include <cstdlib>
#include <unistd.h>
void *consumer(void *args)
{
BlockQueue<int> *block_queue = (BlockQueue<int> *)args;
while (true)
{
int data = 0;
block_queue->pop(&data);
std::cout << "consumer::" << data << std::endl;
}
return (void *)0;
}
void *producer(void *args)
{
BlockQueue<int> *block_queue = (BlockQueue<int> *)args;
while (true)
{
int data = rand() % 20 + 1;
std::cout << "producer::" << data << std::endl;
block_queue->push(data);
}
return (void *)0;
}
int main()
{
srand((long long)time(nullptr));
BlockQueue<int> *block_queue = new BlockQueue<int>();
pthread_t consumer_tid;
pthread_t producer_tid;
//创建两个线程一个消费者和生产者
//传入的产生block_queue就是生产者和消费者关联的容器
pthread_create(&consumer_tid, NULL, consumer, (void *)block_queue);
pthread_create(&producer_tid, NULL, producer, (void *)block_queue);
//释放两个线程的资源
pthread_join(consumer_tid, NULL);
pthread_join(producer_tid, NULL);
return 0;
}
当上面代码执行起来:结果就是:生产者不断的生产数据,消费者不断的消费数据!
由于上面结果太快!我们尝试一下:控制执行速度!
我们先尝试让生产者先睡两秒,
也就是说消费者会先获得CPU资源,开始消费,但是一开始生产者还没开始生产呢。所以该消费者线程就会被挂起来了;等生产者睡完两秒后开始生产数据,消费者就可以消费了;
我们可以看到现象是:生产者和消费者交替生产和消费!
我们可以让消费者睡两秒,然后让生产者先生产!那么看到的现象肯定是生产者先把阻塞队列生产满了,然后就阻塞等待消费者来消费数据!
所以我们看到的现象大概就是这样生产者先生产一批货,然后消费者消费一个,然后生产者再生产一个,消费者再消费。。。。。。
再次理解生产者消费者模型
对于生产者和消费者模型,我们并不是简简单单的传输一些整数就可以!我们还可以传输一些任务,通过生产者不断生产,消费者不断拿生产者生产的任务去做处理!
我们还必须知道:
生产和消费传输数据只是第一步!
生产者生产的数据从哪里来?生产者拿到数据据耗时吗?
消费者拿到数据后要怎么处理?消费者处理数据耗时吗?
这个当然是耗时的动作!这个耗时的动作会带来什么问题?就会使得当我们消费者拿到数据后,在处理时候,因为处理时花时间的,那么此时生产者线程就可以有机会被调度,去拿到数据进行生产;这也就达到了一种目的:生产不断生产消费不断消费的过程;
从生产者的角度思考,因为生产者生产的产品时候的数据来源也是耗时的,所以消费者也是有时间调度来获取产品然后开始消费,然后进行处理的;
所以我们搞多一个例子:
这个例子就是生产者去生产 任务,消费者去拿到任务进行处理的过程;
我们这个任务是一个计算任务;
生产者拿到数据后,往阻塞队列放入计算的任务;消费者从阻塞队列拿到任务后,开始处理任务;
#include "BlockQueue.hpp"
#include <ctime>
#include <cstdlib>
#include <unistd.h>
#include<string>
#pragma once
#include<iostream>
//计算任务的类
class Task
{
private:
int _x;
int _y;
char _op; //操作符 +-*/%
public:
Task() {}
Task(int x, int y, int op) : _x(x), _y(y), _op(op)
{
}
int run()
{
int res = 0;
switch (_op)
{
case '+':
res = _x + _y;
break;
case '-':
res = _x - _y;
break;
case '*':
res = _x * _y;
break;
case '/':
res = _x / _y;
break;
case '%':
res = _x % _y;
break;
default:
std::cout << "bug!!!" << std::endl;
break;
}
std::cout<<"当前消费者线程:"<<pthread_self()<< "正在处理任务:"
<<_x<<_op<<_y << "="<<res<<std::endl;
return res;
}
~Task() {}
};
void *consumer(void *args)
{
BlockQueue<Task> *block_queue = (BlockQueue<Task> *)args;
while (true)
{
//1.消费者,首先要解决的是:获取到任务
Task t;
block_queue->pop(&t);
//2.获取任务后,要解决的是任务如何处理的问题
t.run();
}
return (void *)0;
}
void *producer(void *args)
{
BlockQueue<Task> *block_queue = (BlockQueue<Task> *)args;
std::string ops = "+-*/%"; //操作符
while (true)
{
//1.生产任务:首要解决的是,数据从哪儿来生产产品(任务)
//在这里我们用随机数来解决该数据来源问题
int x = rand() % 20 +1;
int y = rand() % 10 +1;
char op = ops[rand() % 5];
Task t(x,y,op);
//2.有了数据就可以生产产品了
block_queue->push(t);
std::cout << "当前生产者线程:"<<pthread_self()
<<"正在生产任务"<<x<<op<<y<<"=?"<<std::endl;
}
return (void *)0;
}
int main()
{
srand((long long)time(nullptr));
Task t;
BlockQueue<Task> *block_queue = new BlockQueue<Task>();
pthread_t consumer_tid;
pthread_t producer_tid;
//创建两个线程一个消费者和生产者
//传入的产生block_queue就是生产者和消费者关联的容器
pthread_create(&consumer_tid, NULL, consumer, (void *)block_queue);
pthread_create(&producer_tid, NULL, producer, (void *)block_queue);
//释放两个线程的资源
pthread_join(consumer_tid, NULL);
pthread_join(producer_tid, NULL);
return 0;
}
测试结果如下:
当然上面的代码还可以继续修改:比如我加多几个生产者线程,或者消费者线程,实现多生产多消消费的现象,这都是没问题的;
而且我们生产者现在就被高度抽象化,可以搞很多任务,任务比如说是:请求登录页面,请求连接等任务,都可以丢到阻塞队列,让消费者去拿来处理!
总结生产者消费者模型
对于生产消费者模型:
我们不要仅仅就关心,生产者把任务丢到阻塞队列中,消费者从阻塞队列中拿任务;
还要关心生产者生产任务的数据来源要花费时间,还要关心消费者拿到任务处理花费的时间;
因为很有可能,当生产者在准备数据来生产任务的时候,这个时间段是耗时的,那么消费者线程就可以获得抢夺CPU资源去执行消费者的事情;同时当消费者在拿到任务去处理任务时候,也是耗时的,那么生产者就可以在这段时间抢夺CPU资源去生产任务!
这里是引用
最后
以上就是糊涂戒指为你收集整理的【Linux】多线程同步--基于阻塞队列的生产者消费者模型(条件变量解决)线程同步条件变量条件变量相关接口生产者消费者模型基本理论基于阻塞队列的生产者消费者模型再次理解生产者消费者模型总结生产者消费者模型的全部内容,希望文章能够帮你解决【Linux】多线程同步--基于阻塞队列的生产者消费者模型(条件变量解决)线程同步条件变量条件变量相关接口生产者消费者模型基本理论基于阻塞队列的生产者消费者模型再次理解生产者消费者模型总结生产者消费者模型所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复