C++ 多線程學習筆記(6):讀者-寫者問題模擬

一、介紹說明

  • 語言:C++11
  • 題目:讀者-寫者問題模擬
  • 背景:
    • 2個讀者5個寫者,操作一個共享的數據區(用一個string字符串代表)
    • 寫者和其他寫者、讀者是互斥的
    • 讀者和其他寫者是互斥的,和其他讀者是不互斥的
  • 編程思路
    • 做一個臨界資源類,包含讀者寫者共同共享數據區,和對這個數據的讀寫操作
    • 利用C++11提供的 mutex 類,用 “使用成員函數指針作爲線程函數” 的方法建立多個讀者寫者線程
    • 爲了自動生成讀者的數據,給每個寫者一個私有數據區,並單獨開一個數據生成線程,此線程不斷生成隨機字符串填入寫者的私有數據區中。當某個寫者拿到寫 “ 讀者-寫者共享數據區” 的權限後,從其私有數據區中取出數據生成線程生成的隨機字符串寫入。
  • 進程同步分析
    • 從寫者角度看
      • 寫者和其他寫者、讀者都是互斥的
      • 臨界資源是 “讀者-寫者共享數據區”,給它設置一個寫互斥量Wmutex:=1
    • 從讀者角度看
      • 讀者和寫者之間互斥(可以用上面的 Wmutex 處理)
      • 讀者和讀者之間不互斥
      • 關鍵在於,要知道當前有沒有讀者在讀,否則沒法確定 signal(Wmutex) 的時機。因此我們可以設置一個計數值RCount:=0 描述當前訪問臨界資源的讀者個數,這個值可以被所有讀者互斥訪問,設置一個互斥信號量 Rmutex 來控制讀者的互斥訪問
    • 針對寫者的數據生成線程
      • 數據生成線程不斷訪問寫者的私有數據區,向其中填入隨機數據
      • 在寫者寫 “讀者-寫者共享數據區” 時,寫線程要訪問寫者的私有數據區
      • 因此每個寫者的私有數據區是臨界資源,數據生成線程和寫線程應該互斥地訪問,設置互斥信號量 GENmutex 來控制

二、使用的語法現象

  • 利用C++11標準的 thread 類創建線程

    • 使用成員函數指針創建線程
      • std::thread mytobj(&類名::成員函數名, &類對象, 參數列表); 這行代碼,以指定類對象的指定函數作爲線程的起始函數,創建一個子線程
    • .join()方法
      • 這是thread 類的一個方法,其作用是阻塞主線程,讓主線程等待子線程執行完畢,然後子線程和主線程匯合,再往下執行,以防主線程先於子線程結束,導致子線程被操作系統強制結束
  • 互斥量mutex

    • mutex是一個類對象,提供了多個對互斥量(信號量)的操作
    • lock() 方法:給互斥量 “加鎖”,相當於P操作
    • unlock()方法:給互斥量 “解鎖”,相當於V操作
  • this_thread命名空間

    • this_thread::sleep_for(時間):令此線程睡眠一段時間,期間不參與互斥量爭用
    • this_thread::get_id():獲取當前線程的id
  • 其他

    • std::lock_guard類:用這個類的對象,可以代替lockunlock操作,可以避免忘記寫unlock。原理是在這個對象構造時lock(),在它析構時unlock()。這次的作業裏沒用到

    • std::lock()函數:用這個函數,可以同時給多個互斥量加鎖。當某處需要同時請求多個互斥量時,此函數從第一個互斥量開始嘗試上鎖,如果lock()成功,就繼續嘗試下一個互斥量;一旦有一個互斥量鎖不上,立即釋放已經鎖住的所有互斥量,從第一個互斥量開始重新嘗試。相當於課上的AND信號量集

    • 一個技巧

      • 這樣寫可以同時lock()多個信號量,並自動unlock()
      • 下面代碼的92~102行可以用這個方法改進
    //用lock類同時鎖倆信號量
    std::lock(my_mutex1, my_mutex2);	
    //用lock_guard對象來unlock,adopt_lock用來避免重複lock
    std::lock_guard<std::mutex> threadGuard1(my_mutex1, std::adopt_lock);
    std::lock_guard<std::mutex> threadGuard2(my_mutex2, std::adopt_lock);		

三、代碼

//讀者寫者問題,請在release狀態下執行,因爲debug狀態要求線程A進行的lock必須由線程A來unlock,
//而讀者寫者問題中,某個讀者對寫互斥量的lock可能是由其他讀者unlock的。如果在debug狀態運行,會報錯unlock of unowned mutex(stl::mutex)

#include "pch.h"	//vs2017自帶的空編譯頭
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
#include <cstdlib>
#include <ctime>
#include <string>
#include <cstdio>
#include <windows.h>

using namespace std;

//寫者類
class writer
{
private:
	string data;		//寫者準備的數據(數據生成線程的臨界資源)
	thread *Wthread;	//寫線程指針
	thread GENthread;	//數據生成線程


public:
	mutex GENmutex;		//數據生成互斥量

	//構造函數
	writer()
	{
		cout << "構造" << endl;
		GENthread = thread(&writer::genData, this);	//創建一個數據生成線程
	}

    //關聯寫線程
	void setThread(thread *p)
	{
		Wthread = p;
	}
	
    //設置子線程爲join方式
	void join()
	{
		Wthread->join();
		GENthread.join();
	}

	//生成寫者的數據,十個隨機大寫字母
	void genData()
	{
		while (1)
		{
			GENmutex.lock();

			data.clear();
			for (int i = 0; i < 10; i++)
				data.insert((string::size_type)0, 1, rand() % 26 + 'A');

			GENmutex.unlock();
		}
	}

	//獲取寫者數據
	string getData()
	{
		return data;
	}
};


//讀者-寫者 臨界資源類
class CriticalResource
{
private:
	mutex Wmutex;	//寫互斥量,臨界資源空閒
	mutex Rmutex;	//讀互斥量,RCount訪問空閒
	int RCount;		//讀者計數值

	string str;		//臨界資源(讀者寫者共享數據區)

public:
	//用構造函數賦初值
	CriticalResource() {	RCount = 0;	}

	//寫線程
	void Write(writer *w)
	{
		while (1)
		{
			//請求空閒的臨界資源,加鎖
			Wmutex.lock();

			//寫入隨機生成的數據
			w->GENmutex.lock();		//先申請訪問寫者的臨界資源data
			str = w->getData();		//寫入臨界資源
			w->GENmutex.unlock();	//釋放寫者臨界資源互斥量GENmutex

			cout << endl << "寫者" << this_thread::get_id() << "寫入:" << str <<"----------------------------------"<< endl;
			
			//解鎖,釋放寫互斥量
			Wmutex.unlock();
			//隔一段隨機時間請求一次
			this_thread::sleep_for(chrono::seconds(rand() % 3));
		}
	}

	//讀線程
	void Read()
	{ 
		while (1)
		{
			//請求訪問RCount,加鎖
			Rmutex.lock();
			//如果當前沒有瀆者,請求空閒的臨界資源(避免干擾寫者)
			if (RCount == 0)
				Wmutex.lock();
							
			//讀者數+1
			RCount++;

			//釋放RCount訪問互斥量
			Rmutex.unlock();

			//讀
			cout << "讀者" << this_thread::get_id() << "讀取:" << str << ",當前有" << RCount << "個讀者正在訪問" << endl;

			//請求訪問RCount,加鎖
			Rmutex.lock();

			//讀者數+1
			RCount--;

			//臨界資源空閒了,寫者可以寫了,釋放寫互斥量
			if (RCount == 0)
				Wmutex.unlock();

			//釋放RCount訪問互斥量
			Rmutex.unlock();

			//隔一段隨機時間請求一次
			this_thread::sleep_for(chrono::seconds(rand() % 3));
		}
	}
};


int main()
{
	srand((int)time(0));

	vector <thread> readerThreads;
	vector <thread> writerThreads;
	writer W[20];	//最多20個寫者

	CriticalResource CR;

	//創建5個寫線程,子線程入口是CriticalResource類函數Wiite
	for (int i = 0; i < 2; i++)
	{
		writerThreads.push_back(thread(&CriticalResource::Write, &CR, &W[i]));
		W[i].setThread(&writerThreads.back());
	}

	//創建5個讀線程,子線程入口是CriticalResource類函數Read
	for (int i = 0; i < 5; i++)
		readerThreads.push_back(std::thread(&CriticalResource::Read, &CR));

	//所有線程都設置成join模式,主線程要等待子線程結束才能退出,防止主線程提前退出
	for (auto iter = readerThreads.begin(); iter != readerThreads.end(); ++iter)
		iter->join();

	for (int i = 0; i < 2; i++)
		W[i].join();

	return  0;
}


  • 這個程序是死循環運行的,這裏截取了一段輸出
  寫者2604寫入:WPIACTWNOU----------------------------------
  
  寫者26320寫入:JFRPIAIZXX----------------------------------
  讀者27172讀取:JFRPIAIZXX,當前有3個讀者正在訪問
  讀者28956讀取:JFRPIAIZXX,當前有2個讀者正在訪問
  讀者27504讀取:JFRPIAIZXX,當前有1個讀者正在訪問
  讀者1512讀取:JFRPIAIZXX,當前有2個讀者正在訪問
  讀者32716讀取:JFRPIAIZXX,當前有1個讀者正在訪問
  
  寫者2604寫入:LXPNIQLOHB----------------------------------
  
  寫者26320寫入:YAZTWLTIGL----------------------------------
  讀者28956讀取:YAZTWLTIGL,當前有1個讀者正在訪問
  讀者27172讀取:讀者27504讀取:YAZTWLTIGL,當前有2個讀者正在訪問
  YAZTWLTIGL,當前有1個讀者正在訪問
  讀者1512讀取:YAZTWLTIGL,當前有2個讀者正在訪問
  讀者32716讀取:YAZTWLTIGL,當前有1個讀者正在訪問
  
  寫者2604寫入:KTLXOJFAMF----------------------------------
  讀者28956讀取:KTLXOJFAMF,當前有1個讀者正在訪問
  讀者27504讀取:KTLXOJFAMF,當前有2個讀者正在訪問
  讀者27172讀取:KTLXOJFAMF,當前有1個讀者正在訪問
  
  寫者26320寫入:GTCWCHIFON----------------------------------
  
  寫者2604寫入:WZHLSHRWFH----------------------------------
  讀者1512讀取:WZHLSHRWFH,當前有2個讀者正在訪問讀者28956讀取:WZHLSHRWFH,當前有5個讀者正在訪問
  
  讀者32716讀取:WZHLSHRWFH,當前有3個讀者正在訪問
  讀者27172讀取:WZHLSHRWFH,當前有2個讀者正在訪問
  讀者27504讀取:WZHLSHRWFH,當前有1個讀者正在訪問
  
  寫者26320寫入:ZEFCTZRYDQ----------------------------------
  
  寫者2604寫入:RZATXQKCBK----------------------------------
  讀者讀者28956讀取:RZATXQKCBK,當前有5個讀者正在訪問
  1512讀取:RZATXQKCBK,當前有4個讀者正在訪問
  讀者27172讀取:RZATXQKCBK,當前有3個讀者正在訪問
  讀者27504讀取:RZATXQKCBK,當前有2個讀者正在訪問
  讀者32716讀取:RZATXQKCBK,當前有1個讀者正在訪問
  
  寫者26320寫入:TFLBUPLSTF----------------------------------
  讀者1512讀取:TFLBUPLSTF,當前有2個讀者正在訪問
  讀者32716讀取:TFLBUPLSTF,當前有1個讀者正在訪問
  • 結果分析

    • 由於寫者向控制檯打印的那行代碼不在lock()區域內,有可能被打斷,可以看到有些讀者的數據數據被打斷了
    • 可以看到兩個寫者不斷寫入數據,每次寫入後,直到下一次寫入數據之前,所有讀者讀取的數據都和最近寫入的一致,而且總是在沒有讀者時纔會發生寫入,符合讀者-寫者問題要求
    • 可以看出,各個子線程的運行是不可預測的

四、遇到的問題

  • 如果用的是VS,一定要在release狀態下執行,因爲debug狀態要求線程A進行的lock必須由線程A來unlock,而讀者寫者問題中,某個讀者對寫互斥量的lock可能是由其他讀者unlock的。如果在debug狀態運行,會報錯unlock of unowned mutex(stl::mutex) 這個問題調試了很久

  • 所有的子線程的.join()方法調用,應當統一放在主線程最後,否則在第一個.join()位置主線程就會被阻塞,子線程結束前,後面的其他代碼都不能執行。

  • 一開始沒有設置單獨的數據生成線程,而是在寫線程中現場準備隨機數。但是在加上 this_thread::sleep_for 延時後,我發現以下問題

    • 經過隨機延時,每個寫者進程發起請求的時機不同,按理說,應當看到控制檯上不時出現一個寫者的打印提示,並且相鄰兩個打印提示中寫者寫入的數據應該不同(因爲每個線程裏都是寫入前臨時隨機生成的數據)。但事實上控制檯的打印數據是 ”分組突發“ 形式的,往往是半天沒有打印,然後一下打印好多行。根據打印的線程id,可以確定這些提示是由不同的寫進程打印的,但它們打印的寫入內容卻有很多重複

    • 這個問題查了挺久的,沒有解決,也不知道爲什麼會這樣,我懷疑可能是編譯器針對cout做了什麼優化導致"分組突發"現象,而數據重複問題可能是cout語句裏直接打印共享數據str造成的?可能cout的時候,不是立即去內存取變量值的,而是做了優化用了之前的值?總之不是很確定

    • 最後決定不要在寫線程裏做數據生成了,生成的數據也最好不要被其他線程覆蓋,於是給寫者增加了私有數據區和數據生成線程,解決了上述問題。雖然"分組突發"現象依然存在,但是可以確保不同寫者寫入的數據是不同的了

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