生產者與消費者模型
產生原因: 生產者生產的數據過快,消費者處理數據,但是生產者和消費者的速度並不均衡,導致生產的速度或者數據處理的速度根本提不起來。舉個栗子:一個線程從網卡上抓取數據流量,一個線程進行流量分析,分析肯定特別慢,如果你從網卡上抓取的數據流量很慢的話,不代表流量產生的就慢,抓的特別慢就說明丟包了,意味着你抓取流量數據的時候抓取的不完整,不完整說明你的數據分析沒有任何意義。你只能將數據丟掉,那麼就遺失數據。
爲什麼要使用生產者消費者模型??
生產者消費者模式就是通過一個容器來解決生產者和消費者的強耦合問題。生產者和消費者彼此之間不直接通訊,而通過阻塞隊列來進行通訊,所以生產者生產完數據之後不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據,而是直接從阻塞隊列裏取數據,阻塞隊列就相當於一個緩衝區,平衡了生產者和消費者的處理能力。這個阻塞隊列就是用來給生產者和消費者解耦的。
關於耦合性的解釋:
耦合性(Coupling),也叫耦合度,是對模塊間關聯程度的度量。耦合的強弱取決於模塊間接口的複雜性、調用模塊的方式以及通過界面傳送數據的多少。模塊間的耦合度是指模塊之間的依賴關係,包括控制關係、調用關係、數據傳遞關係。模塊間聯繫越多,其耦合性越強,同時表明其獨立性越差( 降低耦合性,可以提高其獨立性)。
生產者與消費者模型的介紹:
生產者與消費者模型:(不管什麼模型都是針對於特定場景下的解決方案)
典型場景: 任務處理中既有數據生產,又有數據處理的這種場景
三個重要成員:
- 生產者: 生產數據
- 緩衝區(隊列):緩存數據,且線程安全(自帶同步與互斥,有數據就取,沒有就等待產生)
- 消費者:消費數據
生產者消費者模型的優點:
- 支持忙閒不均。 生產者生產的快,可以存儲到緩衝區隊列中,等待多啓動幾個線程一起去消費數據,利用數據。同理,當生產者生產的數據慢,緩衝區隊列中麼有數據,那麼消費者線程則阻塞等待有數據的產生。
- 支持併發。 有多個生產者或者多個消費者的時候,處理速度能達到非常快。畢竟中間的緩衝區時線程安全的。
- 解耦合。封裝就是一種解耦合,減小關聯性,修改只修改一個,不會對另一個造成影響,生產者如果直接將數據交給消費者的話,之間的關聯性很大,而現在有了隊列,他們只與隊列打交道,並不直接溝通,數據處理與數據生產直接沒有直接關係。
生產者與消費者模型可以總結爲: 一個場所,兩類角色,三種關係
一個場所:在緩衝區隊列中
兩類角色:消費者與生產者線程
三種關係:
1. 生產者與消費者: 具有同步、互斥關係(放完數據,纔可以去取數據)
2. 生產者與生產者: 互斥關係(特定的數據只能一個線程生產)
3. 消費者與消費者: 互斥關係(一個資源只能一個線程去取)
生產者與消費者模型的代碼實現(手撕代碼):
BlockingQueue–阻塞隊列
在多線程編程中阻塞隊列(Blocking Queue)是一種常用於實現生產者和消費者模型的數據結構。其與普通隊列區別在於,當隊列爲空時,從隊列獲取元素的操作將會被阻塞,直到隊列中放入了元素;當隊列滿時,往隊列裏存放數據的操作也會阻塞,直到元素從隊列中取出(基於不同的線程來說的)
其實就是對互斥鎖 與 條件變量 的 應用
使用C++封裝一個線程安全隊列,也叫做阻塞隊列,放滿數據了就不能再放了
實現生產者與消費者模型的注意事項:
- 構造函數的作用:確定隊列的最大容量數,以及條件變量與互斥鎖的初始化
- 析構函數的作用: 銷燬互斥鎖與條件變量
- 入隊操作: 提供給生產者的接口—數據入隊
- 出隊操作: 提供接消費者的接口—數據出隊
- 因爲隊列本身是一個臨界資源,所以要對它的操作都要進行加鎖操作(pthread_mutex_lock(&metex));
- 如果隊列爲空,消費者等待,接下來解鎖(一個wait()接口完成這兩步,wait()就是解鎖+等待+加鎖的步驟),完成出隊,必須用while循環,支持併發的時候必須得判斷。
- 如果隊列滿了才wait()讓生產者等待,讓生產者入隊
- 當隊列中有數據生產者喚醒消費者,告訴消費者有資源進行訪問;當每個數據出隊之後,都會喚醒生產者,告訴生產者可以生產數據了。
define MAX_QUE 10
class BlockQueue
{
//可以動態增長的隊列,非安全的
//而我們現在要實現的是阻塞的線程安全隊列,當我們放滿了數據,那麼數據就不能往裏面放了,
//我們向其中添加的節點個數是有限的,所以我們要定義一個最大的節點限制capacity作爲上限
std::queue<int> _queue;
int _capacity; //最大的結點數量限制,達到容量表示隊列滿了
//以上兩個是爲了實現隊列而賦予的成員變量
//下面的是基於線程安全的角度實現的變量
//實現同步操作:沒有數據的時候消費者要等待,數據滿了的時候生產者要等待,
//所以需要條件變量,條件變量使用兩個,因爲不同的角色要等待在不同的條件變量上,所以要使用兩個不同的條件變量
pthread_cond_t _cond_productor //生產者等待隊列
pthread_cond_t _cond_consumer //消費者等待隊列
pthread_mutex_t _mutex; //定義互斥鎖來保證線程安全,來實現互斥
public:
BlockQueue(int que_num = MAX_QUE):_capacity(que_num) //構造函數:確定隊列的最大容量數,以及條件變量與互斥鎖的初始化
{
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_cond_consumer,NULL);
pthread_cond_init(&_cond_productor,NULL);
}
~BlockQueue() //析構函數:銷燬互斥鎖與條件變量
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond_consumer);
pthread_cond_destroy(&_cond_productor);
}
bool QueuePush(int &data) //入隊操作: 提供給生產者接口---數據入隊
{
pthread_mutex_lock(&_mutex); // queue是臨界資源 需要加鎖保護
while(queue.size() ==capacity) //若隊列中的數據已經滿了,則把生產者添加到生產者等待隊列中
{
pthread_cond_wait(&_cond_productor,&_mutex);
}
_queue.push(data); //數據入隊
pthread_mutex_unlock(&_mutex); // 臨界區臨界資源queue保護完畢
pthread_cond_signal(&_cond_consumer); //解鎖完畢 喚醒消費者等待隊列上的消費者,隊列裏面有數據,通知消費者可以讀取了
return true;
}
bool QueuePop(int &data)
{
pthread_mutex_lock(&_mutex); //加鎖
while (_queue.empty()) //如果隊列爲空
{
pthread_cond_wait(&_cond_consumer,&_mutex);//讓消費者等待到消費者的等待隊列
}
data = _queue.front();
_queue.pop(); //數據出隊
pthread_mutex_unlock(&_mutex);//解鎖
pthread_cond_signal(&_cond_productor);// 喚醒生產者隊列上的生產者,可以生產數據了
return true;
};