關於哲學家就餐問題的思考與理解以及利用UML來看待該問題
哲學家就餐問題(dinning philosophers problem):
最開始由荷蘭科學家迪傑斯特拉提出的問題 即五臺計算機同時試圖訪問五臺共享的磁碟驅動器。
這是一個多執行緒同步問題,用來解釋死鎖和資源耗盡。
問題描述:
有五個哲學家每天都只做兩件事情,吃飯(eating)和思考(thinking)。
專心的人做任何事情都會十分專注,哲學家作為一名wiser,自然也不例外,吃飯的時候便不會思考,思考的時候便不會吃飯。
餐桌上面有一份義大利麵,悲劇的是啥呢,五個人每個人只有一個叉子,但是哲學家吃麵喜歡用兩個叉子來吃飯,沒辦法咯。
而且他們只思考從不交談(廢話,笛卡爾什麼時侯的人,柏拉圖是什麼時候的人,這個面和叉子已經跨越了時空 哈哈哈。 )
這就很涼涼,很有可能會發生死鎖問題,如果五個大佬同時都喊餓啊餓啊,五個人都拿起了叉子,結果悲劇的發現沒有一個人能吃到兩個叉子的飯,吃飯不用兩個叉子又怎麼能叫吃飯呢?不吃了!老子等兩個叉子出現!大佬氣無叉而等待(執行緒氣無資源而計算機死鎖)
等時間久了誰都會腰痠背痛啊,而且有損哲學家的風範,有的人選擇等一會便放棄,過一會再拿叉子。(聰明的想法,避免了死鎖情況的發生,但可能會資源耗盡)
而且!還是會潛藏一種很坑的情況! 真的很坑!如果五個人選擇同時放下叉子,幾分鐘之後又同時拿起了筷子。。。(活鎖現象,真是有夠作的。)
死鎖(deadlock),指當兩個以上的運算單元,雙方都在等待對方停止執行,以獲取系統資源,但是沒有一方提前退出時的狀況。
死鎖發生的四個條件:
1、互斥條件:執行緒對資源的訪問是排他性的,如果一個執行緒對佔用了某資源,那麼其他執行緒必須處於等待狀態,直到資源被釋放。
2、請求和保持條件:執行緒T1至少已經保持了一個資源R1佔用,但又提出對另一個資源R2請求,而此時,資源R2被其他執行緒T2佔用,於是該執行緒T1也必須等待,但又對自己保持的資源R1不釋放。
3、不剝奪條件:執行緒已獲得的資源,在未使用完之前,不能被其他執行緒剝奪,只能在使用完以後由自己釋放。
4、環路等待條件:在死鎖發生時,必然存在一個“程序-資源環形鏈”,即:{p0,p1,p2,...pn},程序p0(或執行緒)等待p1佔用的資源,p1等待p2佔用的資源,pn等待p0佔用的資源。(最直觀的理解是,p0等待p1佔用的資源,而p1而在等待p0佔用的資源,於是兩個程序就相互等待)
產生死鎖的原因主要是:
(1) 因為系統資源不足。
(2) 程序執行推進的順序不合適。
(3) 資源分配不當等。
如果系統資源充足,程序的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則就會因爭奪有限的資源而陷入死鎖。其次,程序執行推進順序與速度不同,也可能產生死鎖。
活鎖(livelock),活鎖與死鎖類似,都是需要獲取對方的資源而等待導致停滯不前,唯一的區別是,察覺到停滯不前時會先釋放自己的資源,過一段時間再進行獲取資源的嘗試,但如果雙方的操作同步了的話,仍然會導致停滯不前的情況。
言歸正傳
費的聰明人腦殼疼(哎呀媽呀,我腦殼er疼)的三種解法:
其一:服務生解法
大人物們就只管吃頓飯和思考宇宙奧祕吧,我為你們請了一個服務生來專門看著你們,只要餓了就告訴我,我來為你們分配資源, 假設哲學家依次編號為0至4。如果0和2在吃東西,則有四隻餐叉在使用中。1坐在0和2之間,所以兩隻餐叉都無法使用,而3和4之間有一隻空餘的餐叉。假設這時3或者4想要吃東西。如果他拿起了第五隻餐叉,就有可能發生死鎖。不過如果他徵求服務生同意,服務生會讓他進入等待佇列。這樣,餓肚子的哲學家就能在有兩把叉子空出來時,吃到久等的麵條,從而避免了死鎖。
問題的關鍵只有兩個中心人物,一個是哲學家,一個是服務生。只要把問題的原因找到這個問題就解決的了。和數學差不太多,根據已知條件推斷潛在條件,然後解決問題。
哲學家會做些什麼事情呢?
1,思考thinking(哲學家一個叉子都不想要,吃飽了思考人生呢~)
2,飢餓hungry(哲學家想要筷子ci飯,執行緒想要進入臨界區獲取資源)
3,吃飯eating(得到兩個叉子,獲得資源)
哲學家如何才能吃到好吃的義大利麵呢?答案只有一個——附近的兩個人沒有和他搶飯吃。
也就是需 philosopher [ i ] ==EATING
且滿足 philosopher(i+4)%5!=eating &&philosopher(i+1)%5!=eating;才能達到需求
具體的虛擬碼如下
process PHILOSOPHER[i]
while true do
{ THINK;
PICKUP(CHOPSTICK[i], CHOPSTICK[i+1 mod 5]);
EATING;
PUTDOWN(CHOPSTICK[i], CHOPSTICK[i+1 mod 5])
}
下面是我寫的關於服務生的一個類,可以作為參考(僅作參考啊!大概就是這個意思)
#include<iostream>
typedef int condition;
const int hungry = 0;
const int eating = -1;
const int thinking = 1;
class sever
{
int philosopher[5];
int self[5];
//哲學家準備拿叉子
void Pickup(int i)
{
// 哲學家餓了
philosopher[i] = hungry;
// 只有哲學家檢測能取得周圍的筷子才能吃飯
test(i);
// 如果不能通過檢測,由服務生將其置入等待佇列
if (philosopher[i] != eating)
self[i].wait;
}
//哲學家準備放下筷子
void Putdown(int i)
{
// 從新迴歸思考狀態
philosopher[i] = thinking;
}
//測試能否吃到飯
void test(int i)
{
if (philosopher[(i + 1) % 5] != eating&& philosopher[(i + 4) % 5] != eating
&& philosopher[i] == hungry) {
philosopher[i] = eating;
self[i].signal();
}
}
//初始化,全部置為思考狀態
void init()
{
for (int i = 0; i <=4; i++) {
philosopher[i] = thinking;
}
}
};
其二:資源分級解法法
這個世界上本來是人人平等的,但是有了等級制度,一切都變了模樣。
(一切居然都變得有序了 ('''_''') WTF~)
為叉子們整出一個等級制度,並且所有資源的拿取都依照規則執行,並按相反順序進行釋放,而且保證不會有兩個無關資源同時被同一項工作所需要。叉子按照定下的規則分成等級為1至5,每個大佬總是先拿起左右兩邊編號較低的餐叉,再拿編號較高的。用完餐叉後,他總是先放下編號較高的餐叉,再放下編號較低的。在這種情況下,當四位哲學家同時拿起他們手邊編號較低的餐叉時,只有編號最高的餐叉留在桌上,從而第五位哲學家就不能使用任何一隻餐叉了。而且,只有一位哲學家能使用最高編號的餐叉,所以他能使用兩隻餐叉用餐。當他吃完後,他會先放下編號最高的餐叉,再放下編號較低的餐叉,從而讓另一位哲學家拿起後邊的這隻開始吃東西。
又是一道虛擬碼(心虛,畢竟還不是太會多windows執行緒程式設計。。。)
class Philosopher {
void philo_class_test(fork_one, fork_two) {
if (fork_one < fork_two) {
sem_firstFork = fork_one;
Sem_secondFork = fork_two;
}
else {
sem_firstFork = fork_two;
sem_secondFork = fork_one;
}
}
};
int main() {
P(fork_one);
P(fork_two);
eating();
V(fork_one);
V(fork_two);
}
其三:
Chandy/Misra解法(髒筷子法)
對每一對競爭一個資源的哲學家,新拿一個餐叉,給編號較低的哲學家。每隻餐叉都是“乾淨的”或者“髒的”。最初,所有的餐叉都是髒的。當一位哲學家要使用資源(也就是要吃東西)時,他必須從與他競爭的鄰居那裡得到。對每隻他當前沒有的餐叉,他都發送一個請求。當擁有餐叉的哲學家收到請求時,如果餐叉是乾淨的,那麼他繼續留著,否則就擦乾淨並交出餐叉。當某個哲學家吃東西后,他的餐叉就變髒了。如果另一個哲學家之前請求過其中的餐叉,那他就擦乾淨並交出餐叉。如此迴圈往復~
思考:
其實到了現在的這個時候,我覺得哲學家就餐問題問題雖然解法有三種,三十區別都並不是太大,比如最後的這個髒筷子解法,怎麼看都很像是訊號量來實現,髒筷子的髒等價於設初始值為0,乾淨的筷子無非就是變成1,沒有太大區別。
VS內部<program.h>
C++11的寫法程式碼如下:
//-----------------------------------------------------------------------------
// File: Program.h
//
// Desc: Dining Philosophers sample.
//
// Demonstrates how to use Monitor object (lock) to protect critical section.
//
// Scenario is such that there are 5 philosophers and 5 tools on each side of a philosopher.
// Each philosopher can only take one tool on the left and one on the right.
// One of the philosophers must wait for a tool to become available because whoever grabs
// a tool will hold it until he eats and puts the tool back on the table.
//
// Application of the pattern could be transferring money from account A to account B.
// Important here is to pass locking objects always in the same (increasing) order.
// If the order is mixed you would encounter random deadlocks at runtime.
//
//-----------------------------------------------------------------------------
#include <algorithm>
#include <chrono>
#include <iostream>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
using std::cout;
using std::endl;
using std::adopt_lock;
using std::for_each;
using std::lock;
using std::mem_fn;
using std::move;
using std::to_string;
using std::exception;
using std::lock_guard;
using std::mutex;
using std::string;
using std::thread;
using std::unique_ptr;
using std::vector;
class Chopstick
{
public:
Chopstick() {};
mutex m;
};
int main()
{
auto eat = [](Chopstick* leftChopstick, Chopstick* rightChopstick,
int philosopherNumber, int leftChopstickNumber, int rightChopstickNumber)
{
if (leftChopstick == rightChopstick)
throw exception("Left and right chopsticks should not be the same!");
lock(leftChopstick->m, rightChopstick->m); // ensures there are no deadlocks
lock_guard<mutex> a(leftChopstick->m, adopt_lock);
string sl = " Philosopher " + to_string(philosopherNumber) +
" picked " + to_string(leftChopstickNumber) + " chopstick.\n";
cout << sl.c_str();
lock_guard<mutex> b(rightChopstick->m, adopt_lock);
string sr = " Philosopher " + to_string(philosopherNumber) +
" picked " + to_string(rightChopstickNumber) + " chopstick.\n";
cout << sr.c_str();
string pe = "Philosopher " + to_string(philosopherNumber) + " eats.\n";
cout << pe;
//std::chrono::milliseconds timeout(500);
//std::this_thread::sleep_for(timeout);
};
static const int numPhilosophers = 6;
// 5 utencils on the left and right of each philosopher. Use them to acquire locks.
vector< unique_ptr<Chopstick> > chopsticks(numPhilosophers);
for (int i = 0; i < numPhilosophers; ++i)
{
auto c1 = unique_ptr<Chopstick>(new Chopstick());
chopsticks[i] = move(c1);
}
// This is where we create philosophers, each of 5 tasks represents one philosopher.
vector<thread> tasks(numPhilosophers);
tasks[0] = thread(eat,
chopsticks[0].get(), // left chopstick: #1
chopsticks[numPhilosophers - 1].get(), // right chopstick: #5
0 + 1, // philosopher number
1,
numPhilosophers
);
for (int i = 1; i < numPhilosophers; ++i)
{
tasks[i] = (thread(eat,
chopsticks[i - 1].get(), // left chopstick
chopsticks[i].get(), // right chopstick
i + 1, // philosopher number
i,
i + 1
)
);
}
// May eat!
for_each(tasks.begin(), tasks.end(), mem_fn(&thread::join));
system("pause");
return 0;
}
/*
Philosopher 1 picked 1 chopstick.
Philosopher 3 picked 2 chopstick.
Philosopher 1 picked 5 chopstick.
Philosopher 3 picked 3 chopstick.
Philosopher 1 eats.
Philosopher 3 eats.
Philosopher 5 picked 4 chopstick.
Philosopher 2 picked 1 chopstick.
Philosopher 2 picked 2 chopstick.
Philosopher 5 picked 5 chopstick.
Philosopher 2 eats.
Philosopher 5 eats.
Philosopher 4 picked 3 chopstick.
Philosopher 4 picked 4 chopstick.
Philosopher 4 eats.
*/
最後的最後
關於用uml的方式來思考哲學家就餐問題。
nml主要有三大種類的模型 類模型(class model),狀態模型(state model),互動模型(interaction model)
類模型中 有類圖,描述了系統內部物件及其關係的靜態結構。
狀態模型中 有狀態圖,描述物件隨著時間發生變化的哪些方面。
互動模型有 用例圖(use case diagram ):關注系統的功能,強調系統為使用者做了什麼事情。
順序圖(sequence diagram)顯示互動的物件以及發生互動的時間順序。
活動圖(activity diagram):描述重要的處理步驟。
一共是五大種圖。
其實這個問題我一直都很納悶要如何去用建模的方式來去解決這個問題,雖然是學長留下的任務,但是我還是有些疑問,哲學家就餐問題並不是一個耦合度很高的問題,建模的思想是為了讓事物之間的複雜關係變得簡單化,清晰化,以一種面向物件的角度來思考問題。可是針對哲學家就餐問題,我並沒有感覺到有什麼物件來讓我面。針對不同的解法,比如第一種解法 我需要去針對誰去進行建模?針對這個問題?哲學家就餐問題?用誰去進行用例?三種解法只有服務生解法可以去以面向物件的思想去考慮哲學家就餐問題,哲學家作為一個類,服務生作為一個類,兩個類可以去進行互動,類圖可以畫出來,哲學家作為使用者,“使用者問題作為”一個待處理系統這個系統為哲學家這個“使用者”做了幫助就餐的事情,於是一個用例圖,把這個問題解決了。。。
具體的圖以及分析放到下篇文章裡面。