徹底理解多線程生產者消費者問題(含MFC、vs2017代碼動畫演示)

目錄

一、項目簡介

二、前驅知識(生產者消費者總結、進程同步問題)

1.單生產者-單消費者-一個buffer

2.單生產者-單消費者-多個buffer

3.單生產者-多消費者-多BUFFER

4.多生產者-多消費者-多個buffer

三、代碼(c++ thread、MFC多線程)

1.c++thread的學習

2.MFC多線程的學習主要歸納如下(一開始用的c++thread後來改用mfc自帶類庫了):

四、具體模塊介紹

五、項目總結


一、項目簡介

0.項目下載地址(供參考):https://download.csdn.net/download/qq_39861376/11996017

電子書C++併發編程實戰中文(c++thread)資源下載地址:

1.開發環境:vs2017+mfc

2.實現功能:實現對單生產者-單消費者-一個 BUFFER,單生產者-單消費者-多個 BUFFER,多生產者-多消費者-多個BUFFER

3.項目截圖:

                                                                         圖1.3.1 界面基本介紹

                                                      圖1.3.2 動態過程(紅色爲有空間,綠色爲有產品)


二、前驅知識(生產者消費者總結、進程同步問題)

           生產者-消費者模式是一個十分經典的多線程併發協作的模式,弄懂生產者-消費者問題能夠讓我們對併發編程的理解加深。所謂生產者-消費者問題,實際上主要是包含了兩類線程,一種是生產者線程用於生產數據,另一種是消費者線程用於消費數據,爲了解耦生產者和消費者的關係,通常會採用共享的數據區域,就像是一個倉庫,生產者生產數據之後直接放置在共享數據區中,並不需要關心消費者的行爲;而消費者只需要從共享數據區中去獲取數據,就不再需要關心生產者的行爲。

         一個比較生動的例子:什麼是生產者與消費者問題?舉個例子,我們去喫自助餐,在自助餐的一個公共區域放着各種食物,消費者需要就自行挑選,當食物被挑沒的時候,大家就等待,等候廚師做出更多再放到公共區域內供大家挑選;當公共區域食物達到一定數量,不能再存放的時候,此時沒有消費者挑選,廚師此時等待,等公共區域有地方再存放食物時,再開始生產。這就是一個生產者與消費者問題。

根據這個例子,主要分爲以下幾種情況:

1.單生產者-單消費者-一個buffer

        一個生產者一個消費者可以理解爲 :工廠只有一個存儲空間,工廠生產了一個零件,生產完工廠就滿了,然後通知購買零件的人來倉庫取,購買者取完之後倉庫就空了,然後通知工廠可以繼續生產(規定工廠生產完之後沒有地方放就被浪費掉了)。

所以一個生產者一個消費一個buffer模型的規則可以概括爲:

         a.初始狀態:buffer內有空間,沒產品

         b.規則:       當buffer爲空時,生產者可以生產產品,生產後設置buffer有產品。

                              當buffer有產品時,消費者可以去buffer取產品,消費完後設置buffer有空間。

         c.PV操作: 

                                          

productor

while(true)
{
	//P操作 P操作就是-1
	判斷buffer是否有空間
	if(have)
	{
	   生產
	   ....
	   生產完
	   //V操作 V操作就是+1
	   設置buffer有產品
	}
		
}

consume
while(true)
{
	//p操作
	判斷buffer是否有產品
	if(have)
	{
		消費
		...
		消費完
		//V操作
		設置buffer有空間
	}
}

2.單生產者-單消費者-多個buffer

               一個生產者-一個消費者-多個buffer可以看作:工廠同樣生產產品,只不過和單個buffer不一樣的是,工廠存產品的空間變大了,以5個爲例,當工廠生產完一個產品,工廠不會因爲沒有空間存產品而停止生產,而是會繼續尋找下一個空間,在下一個空間繼續生產產品,存儲產品。與此同時,工廠會同時告訴購買者工廠現在有貨了,購買者收到信息就會來工廠取產品,就在這個時間,並行發生了,生產者一邊生產產品,消費者可以同時消費已經生產好的產品。

所以一個生產者一個消費多個buffer模型的規則可以概括爲:

         a.初始狀態:empty表示buffer內是否有空間(empty>0表示有空間),full表示是否有產品(full>0表示有產品)  

         b.規則:       當buffer爲空時,生產者可以生產產品,生產後設置buffer有產品(fill+1)。

                              當buffer有產品時,消費者可以去buffer取產品,消費完後設置buffer有空間(empty+1)。

         c.PV操作:   

productor

while(true)
{
	//P操作 P操作就是-1
	判斷buffer是否有空間
	if(have)
	{
	   生產
	   ....
	   生產完
            in=(in+1)%buffer_size
	   //V操作 V操作就是+1
	   設置buffer有產品
	}
		
}

consume
while(true)
{
	//p操作
	判斷buffer是否有產品
	if(have)
	{
		消費
		...
		消費完
                out=(out+1)%buffer_size
		//V操作
		設置buffer有空間
	}
}

3.單生產者-多消費者-多BUFFER

                單生產者-多消費者-多BUFFER模式與上邊的單消費者相比,不同的是多個消費者之間要存在競爭,多個消費者不能消費同一個產品,這裏舉兩個例子,

1.假如生產者生產的比較慢,消費者消費的比較快

      這個問題可以理解爲生產者只要一生產就會有消費者去消費,生產者和消費者之間並不存在衝突,只有一個生產者,生產者也不會有衝突,但是有問題的是這麼多消費者,到底誰去消費那個產品呢?所以消費者之間就存在了競爭的關係,誰先到搶到消費的機會誰先消費,其他消費者就只能回去繼續等待(相當於阻塞)。把這個機會比作鎖(mutex),也就是誰先得到鎖設就會得到這個機會,就可以進行消費。          

 2.假如生產者生產的比較快,消費者消費的比較慢

        生產者生產的比較快,這個時候就存在有很多個商品然後供消費者去消費,當然最開始只有一個產品的時候和上一種情況是一樣的,但是隨着時間的遞增可能會存在兩種情況,產品數量>消費者數量,消費者數量>產品數量

        對於產品數量>消費者數量,首先要保證,a.多個消費者不能消費同一個產品,同時要保證,b.多個消費者可以同時去消費,而不用去等待。(關於b,網上有很多方法不管是通過一個鎖還是生產者和消費者各一個鎖,都不能根本上解決這個問題,例如以下P、V操作)

        上圖其實只是不同時間只有一個生產者或者一個消費者在同時進行,並沒有多個生產者,多個消費者在同時進行。而且生產者和消費者去生產和消費的位置是通過in和out去決定的,第一個沒生產完釋放鎖,就不會有第二個生產者去生產。

        對於消費者數量>產品數量,也要保證上述兩點,但是不同的是,這時候並不是所有消費者都在消費,有的線程已經被阻塞了。

解決方案:

1.(自己寫的lj方案,下邊通過老師的指點又發現了新方案,但是先留有一個疑問,如果查找函數的查找方法比較好的話,可能比下一個方法更快,因爲這也類似於人的方式去查找)通過查表的方式解決,兩個信號量保持不變empty和full,但是並沒有維護in和out兩個下標,而是每個生產者每個消費者都去遍歷全部buffer,得到可以生產的位置或者可以消費的位置,然後去設置狀態位生產或消費,對於一個buffer同時只能有一個人獲得🔒,對於其他的線程,如果沒獲得鎖,就應該重新查表,直到查到一個合適的位置自己獲得🔒(遞歸),本項目通過灰太狼和喜羊羊的動畫形式體現了這一點,這樣的話每次多個消費者可以同時去消費,但是這樣就相當於每個buffer都有一個🔒,一個🔒控制一個buffer,在一個生產者或消費者在進行工作的時候,其他同類都不能對這個子buffer進行操作,但是查表也是一種時間上的消耗。

下邊給出解決方案的簡單代碼,全部代碼可以下載。

 

 

int query_table(int buffer[][2],int model)//查表函數,生產者查表返回可以生產的一個buffer,消費者查表返回一個可以消費的區域下標
{
	if (model)//model==1,生產者
	{
		for (int i = 0; i < BUFFER_SIZE; i++)
		{
			if (buffer[i][0] == 0 && buffer[i][1]==0) //無人操作且無產品
			{
				return i;
			}
		}
	}
	else //消費者
	{ 
		for (int i = 0; i < BUFFER_SIZE; i++)
		{
			if (buffer[i][0]==0 && buffer[i][1]==1) //無人操作且有產品
			{
				return i;
			}
		}
	}
	return -1; //都無可用空間
}
int query_until(int res,int temp, CRect btnRT, CMYWMFCDlg *pMyDialog)  //遞歸調用
{
	//res爲上一次查表的差值,應該先過去再查表 temp爲要移動的人物編號
	int new_res = query_table(mybuffer,1);
	if (new_res > res)
	{
		//向前移動過去
		int flag = new_res - res;
		if (temp == 0)//sel==0  移動美羊羊1號
		{
			for (int i = 0; i < 20; i++)
			{
				//pMyDialog->bufferb0.SetFaceColor(RGB(0, 0, 0));
				pMyDialog->m_one.GetClientRect(&btnRT);  //one 是美羊羊的 控件變量
				pMyDialog->m_one.ClientToScreen(btnRT);
				pMyDialog->ScreenToClient(btnRT);
				pMyDialog->m_one.MoveWindow(btnRT.left + 4*flag, btnRT.top, btnRT.Width(), btnRT.Height(), TRUE);
				Sleep(50);
			}
		}
		else
		{
			for (int i = 0; i < 20; i++)
			{
				pMyDialog->m_one1.GetClientRect(&btnRT);  //one 是美羊羊的 控件變量
				pMyDialog->m_one1.ClientToScreen(btnRT);
				pMyDialog->ScreenToClient(btnRT);
				pMyDialog->m_one1.MoveWindow(btnRT.left + 4* flag, btnRT.top, btnRT.Width(), btnRT.Height(), TRUE);
				//pMyDialog->m_one1.MoveWindow(btnRT.left + 100, btnRT.top, btnRT.Width(), btnRT.Height(), TRUE);
				Sleep(50);
			}
		}
	}
	else if (new_res < res && new_res!=-1) //解決-1的情況
	{
		int flag = res-new_res;
		if (temp == 0)//sel==0  移動美羊羊1號
		{
			for (int i = 0; i < 20; i++)
			{
				pMyDialog->m_one.GetClientRect(&btnRT);  //one 是美羊羊的 控件變量
				pMyDialog->m_one.ClientToScreen(btnRT);
				pMyDialog->ScreenToClient(btnRT);
				pMyDialog->m_one.MoveWindow(btnRT.left - 4 * flag, btnRT.top, btnRT.Width(), btnRT.Height(), TRUE);
				Sleep(50);
			}
		}
		else
		{
			for (int i = 0; i < 20; i++)
			{
				pMyDialog->m_one1.GetClientRect(&btnRT);  //one 是美羊羊的 控件變量
				pMyDialog->m_one1.ClientToScreen(btnRT);
				pMyDialog->ScreenToClient(btnRT);
				pMyDialog->m_one1.MoveWindow(btnRT.left - 4 * flag, btnRT.top, btnRT.Width(), btnRT.Height(), TRUE);
				Sleep(50);
			}
		}
	}
	else
	{
		return res;
	}
	//移動完之後要查表,如果一樣就返回,不一樣就遞歸
	int new_res2 = query_table(mybuffer, 1);
	if (new_res2==new_res)
	{
		return new_res2;
	}
	else
	{
		query_until(new_res, temp, btnRT, pMyDialog); //遞歸調用,直到查到不變的值
	}
}

DWORD WINAPI Producer4(CMYWMFCDlg *pMyDialog)  //調用
{
	pMyDialog->bufferb0.ShowWindow(true);
	pMyDialog->bufferb1.ShowWindow(true);
	pMyDialog->bufferb2.ShowWindow(true);
	pMyDialog->bufferb3.ShowWindow(true);
	pMyDialog->bufferb4.ShowWindow(true);
	CRect btnRT;
	int temp = sel;
	sel++;
	//通過查找的方式去進行,存儲結構設置爲一個buffer[i][2]的二維數組。
	//buffer[i][0]表示是否有人在操作(==1爲在操作),buffer[i][1]表示是否有產品(==1爲有產品)。
	//[i][0]爲優先項,不管是否有產品或者有空間,只要[i][0]==1,就不考慮。
	//這樣的話相當於每一個線程都是獨立的線程,不需要依賴於前一個線程。
	while (run)
	{
		endpro = true;
		//查表
		//去指定的表,然後設定值
		//操作(賦值、取值)
		//根據不同的動作修改表中的值
		
		//查表函數 query_table
		int res = query_table(mybuffer,1);
		if (res==-1)
		{
			Sleep(1000);  //先休息再去查表
		}
		else //有可用空間
		{
			//設置佔用此空間,但是如果兩個線程同時查到這個表裏都符合條件都去修改這個值,怎麼辦,怎麼看到底是哪一個線程搶佔了???(將時間細片化多判斷幾次)
			//最新的解決上邊的問題的方案是 加鎖
			//先走路過去
			
			ProductWalk(pMyDialog,temp,res,btnRT);
			//走路結束
			//再查一次表,可能會解決一些問題   //僞喚醒?
			int f = 0;
			WaitForSingleObject(h_Mutex,INFINITE);  //這塊互斥必須要阻塞一個線程,不然就出事兒了(防止兩個線程對同一個buffer操作)
			if (query_table(mybuffer,1) ==res)
			{
				//存不存在兩個線程同時進入了這裏,進這裏說明沒變的要停住,變了的要去滑動
				f = 1;
				mybuffer[res][0] = 1;//變爲不可操作
				buffer[res] = res;
				mybuffer[res][1] = 1;//設置有產品
				//Sleep(1000); //爲了觀察出變化,直接進的話應該最後再修改buffer的顏色,如果立即修改了說明是另一個線程 把buffer修改了,這種情況是錯誤的
				mybuffer[res][0] = 0;//釋放
			}
			ReleaseMutex(h_Mutex);
			if(query_table(mybuffer, 1) !=res && f==0) //變了呢? 重新定位或者找差值直接去差值那裏去,到了差值應該再 “查表” 一次,直到不變爲止。
			{
				//其他所有的沒有直接找到位置的,都要在這裏等鎖,然後上一個位置確定好之後,其他人去找話可以繼續生產的位置,如果不上鎖呢?其他人可能都會去一個位置進行生產
				//這裏也讓繼續生產,可能要用到一個遞歸函數 query_until
				//mutex
				WaitForSingleObject(h_Mutex1,INFINITE);
				res = query_until(res,temp,btnRT,pMyDialog);//還要有控制層,上一次查表的結果還是res沒變
				mybuffer[res][0] = 1;
				ReleaseMutex(h_Mutex1);
				//mutex
				buffer[res] = res;
				mybuffer[res][1] = 1;
				mybuffer[res][0] = 0;

			}
			ConvertBtnGreen(pMyDialog,res);
			//生產完畢,可以設置可以操作標誌,並讓生產者回家,生產者要有標誌位(res)找到回家的方向
			ProductWalkBack(pMyDialog,temp,res,btnRT);
		}
		endpro = false;
	}
	return 0;
}

2.參考上課記得筆記,還是採用in和out環形隊列的方式,每次只鎖in和(out+1)%buffer_size(單生產者所以生產者不用鎖)就可以,(一開始總覺得會出事兒就沒用),沒想到那個問題如此簡單得被解決了。生產者和消費者近乎同時的生產和消費,且不會發生衝突。

4.多生產者-多消費者-多個buffer

這個問題同上


三、代碼(c++ thread、MFC多線程)

1.c++thread的學習

主要是 看書和看網課,用的最多的應該是unique_mutex和std::condition_variable和wait這三個函數,就模擬了P、V操作。

主要參考:

B站網課,老師雖然講的囉嗦,但是講的很生動明白,不會犯困:https://www.bilibili.com/video/av48611530?p=1

c++ thread的書籍(建議網課看一會之後再看,同樣通俗易懂,而且書裏理論和代碼都講):

2.MFC多線程的學習主要歸納如下(一開始用的c++thread後來改用mfc自帶類庫了):

很好的資料:https://www.cnblogs.com/zqrferrari/archive/2010/07/07/1773113.html
//1.創建信號量,初始化信號量
HANDLE CreateSemaphore(
  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,  // SD
  LONG lInitialCount,                          // initial count
  LONG lMaximumCount,                          // maximum count
  LPCTSTR lpName                           // object name
)
//h_FullSemaphore = CreateSemaphore(NULL, 0, 1, NULL);
此函數可用來創建或打開一個信號量,先看參數說明:
 lpSemaphoreAttributes:爲信號量的屬性,一般可以設置爲NULL
 lInitialCount:信號量初始值,必須大於等於0,而且小於等於 lpMaximumCount,如果lInitialCount 的初始值爲0,則該信號量默認爲unsignal狀態,如果lInitialCount的初始值大於0,則該信號量默認爲signal狀態,
 lMaximumCount: 此值爲設置信號量的最大值,必須大於0
lpName:信號量的名字,長度不能超出MAX_PATH ,可設置爲NULL,表示無名的信號量。當lpName不爲空時,可創建有名的信號量,若當前信號量名與已存在的信號量的名字相同時,則該函數表示打開該信號量,這時參數lInitialCount 和 
lMaximumCount 將被忽略。

//2.V操作  相當於加一
ReleaseSemaphore函數用於對指定的信號量增加指定的值。   
	BOOL ReleaseSemaphore(
	HANDLE hSemaphore,
	LONG lReleaseCount,
	LPLONG lpPreviousCount
);
hSemaphore
[輸入參數]所要操作的信號量對象的句柄,這個句柄是CreateSemaphore或者OpenSemaphore函數的返回值。這個句柄必須有SEMAPHORE_MODIFY_STATE 的權限。
lReleaseCount
[輸入參數]這個信號量對象在當前基礎上所要增加的值,這個值必須大於0,如果信號量加上這個值會導致信號量的當前值大於信號量創建時指定的最大值,那麼這個信號量的當前值不變,同時這個函數返回FALSE;
lpPreviousCount
[輸出參數]指向返回信號量上次值的變量的指針,如果不需要信號量上次的值,那麼這個參數可以設置爲NULL;返回值:如果成功返回TRUE,如果失敗返回FALSE,可以調用GetLastError函數得到詳細出錯信息;

//3.進程的等待操作   P操作 相當於減一
WaitForSingleObject函數用來檢測hHandle事件的信號狀態,在某一線程中調用該函數時,線程暫時掛起,如果在掛起的dwMilliseconds毫秒內,
線程所等待的對象變爲有信號狀態,則該函數立即返回;如果超時時間已經到達dwMilliseconds毫秒,但hHandle所指向的對象還沒有變成有信號狀態,函數照樣返回。
DWORD WaitForSingleObject(
	HANDLE hHandle,
	DWORD dwMilliseconds
);
hHandle[in]對象句柄。可以指定一系列的對象,如Event、Job、Memory resource notification、Mutex、Process、Semaphore、Thread、Waitable timer等。
dwMilliseconds[in]定時時間間隔,單位爲milliseconds(毫秒).如果指定一個非零值,函數處於等待狀態直到hHandle標記的對象被觸發,或者時間到了。
如果dwMilliseconds爲0,對象沒有被觸發信號,函數不會進入一個等待狀態,它總是立即返回。如果dwMilliseconds爲INFINITE,對象被觸發信號後,函數纔會返回。

//4.鎖操作
HANDLE h_Mutex;
WaitForSingleObject(h_Mutex, INFINITE);   //加鎖
ReleaseMutex(h_Mutex);                    //解鎖
//5.創建線程
hThread[0] = CreateThread(NULL, 0, LPTHREAD_START_ROUTINE(Producer0), this, 0, NULL);
//mfc的CreateThread函數只能傳遞一個參數(必要時用結構體傳參數),有點小坑,而且傳遞的函數必須是類外的或者類的靜態成員函數,之前用c++ thread後來放棄了,可能是兼容性的問題。

四、具體模塊介紹

1.對於大多數人來講,可能覺得最難的或者最不好入手的不是多線程的問題,而是mfc控件的問題,因爲我就是,所以主要講一下mfc的控件使用和線程的聯繫問題:

這裏用到的控件最多的是picture control、button、listBox control、radio、static

下邊一個個介紹在這個小程序中怎麼用的:

 1.1 picture control:

       主要功能:

       a.添加圖片

       其他網上說直接把bmp位圖放入資源文件裏,然後在資源管理器中右擊導入就可以,但是實測是不可以的:

選擇導入裏邊並沒有圖片,所以我採用了 直接將圖片拖到vs2017中使用截圖工具,然後ctrl+c複製選中的區域,然後按照上邊的方式,右擊資源管理,然後新建位圖,直接ctrl+v複製進去,ctrl+s保存就可以了,res會自動出現剛纔保存好的圖片

然後將picture contral加入到視圖中,然後右擊選擇屬性,type設爲bitmap,image設置爲剛纔生成 的那個圖片就可以了

       b.控制圖片的位置和移動

        直接右擊設置好的pic控件,添加變量,然後選擇控件變量,取名m_one,就可以通過這個控件名字去移動pic控件,網上方法很多,使用方法介紹的很好,就不詳細介紹,主要用MoveWindow函數。粘一段代碼

void ProductWalk(CMYWMFCDlg *pMyDialog, int temp, int res, CRect btnRT)
{
	if (temp == 0)//sel==0  移動美羊羊1號
	{
		if (res == 0)
		{
			for (int i = 0; i < 20; i++)
			{
				pMyDialog->m_one.GetClientRect(&btnRT);  //one 是美羊羊的 控件變量
				pMyDialog->m_one.ClientToScreen(btnRT);
				pMyDialog->ScreenToClient(btnRT);
				pMyDialog->m_one.MoveWindow(btnRT.left + 15, btnRT.top, btnRT.Width(), btnRT.Height(), TRUE);
				Sleep(50);
			}
		}
}

1.2 button(mfc button control 只要mfc的這個纔可以變顏色, 普通的button變不了哦)

button的顯示和隱藏,button的顏色變換

同樣對button設置控件變量

mfc的任何控件都可以通過 bufferb0.ShowWindow(true);來設置控件的隱藏和顯示,true爲顯示,false爲隱藏

//顏色變換 SetFaceColor ,但是如果不設置前邊的屬性有可能變不了顏色
void ConvertBtnRed(CMYWMFCDlg *pMyDialog, int res)
{
	switch (res)
	{
	case 0:
		pMyDialog->bufferb0.m_bTransparent = false;
		pMyDialog->bufferb0.m_bDontUseWinXPTheme = true;
		pMyDialog->bufferb0.EnableWindow(true);
		pMyDialog->bufferb0.SetFaceColor(RGB(255, 0, 0));
		break;
	case 1:
		pMyDialog->bufferb1.m_bTransparent = false;
		pMyDialog->bufferb1.m_bDontUseWinXPTheme = true;
		pMyDialog->bufferb1.EnableWindow(true);
		pMyDialog->bufferb1.SetFaceColor(RGB(255, 0, 0));
		break;
    }
}

1.3 listbox control的添加數據和清除數據

//1.添加數據
CString str; 
str.Format(_T("%d"), in);
LPCTSTR pStr1 = LPCTSTR(str);
m_product_list.AddString(pStr1 + "號"); //m_product_list爲listbox的控件變量

//2.清除數據
m_product_list.ResetContent();

1.4 radio

這個方法也有很多,這裏就不在總結了,最主要的是他們共享一個變量,然後根據變量的值判斷選擇了哪個,很簡單。

1.5設置mfc皮膚

SkinMagic:

下載後解壓:

將這幾個文件放到項目根目錄下(一般是存放cpp的目錄)

然後在工程中添加 SkinMagic.h

在xxxmfc.cpp和xxxmfcDlg.cpp中添加:

然後在xxxmfc.cpp的init中添加verify這兩句(corona爲皮膚文件,可以更改):

在xxxmfcDlg.cpp中添加:

運行就可以看到效果了


五、項目總結

通過這個項目更好的理解了生產者和消費者這個同步模型,更好了理解了同步和互斥,多線程的概念,同時也對mfc不在那麼陌生,寫這篇博客的目的一是給其他人一個比較好的教程吧,因爲都是自己踩得坑。第二個目的是記錄一下現在學的這個知識,希望考試之後課設還能記得,忘了的話還可以在回顧一下。祝大家都有個好成績。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章