一、介紹說明
- 語言: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
類:用這個類的對象,可以代替lock
和unlock
操作,可以避免忘記寫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
的時候,不是立即去內存取變量值的,而是做了優化用了之前的值?總之不是很確定 -
最後決定不要在寫線程裏做數據生成了,生成的數據也最好不要被其他線程覆蓋,於是給寫者增加了私有數據區和數據生成線程,解決了上述問題。雖然"分組突發"現象依然存在,但是可以確保不同寫者寫入的數據是不同的了
-