本節書摘來自異步社群出版社《c++多線程程式設計實戰》一書中的第2章,第2.5節,作者: 【黑山共和國】milos ljumovic(米洛斯 留莫維奇),更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。
程序之間的通信非常重要。雖然作業系統提供了程序間通信的機制,但是在介紹這些機制之前,我們先來考慮一些與之相關的問題。如果航空預定系統中有兩個程序在同時銷售本次航班的最後一張機票,怎麼辦?這裡要解決兩個問題。第1個問題是,一個座位不可能賣兩次。第2個問題是一個依賴性問題:如果程序a生成的某些資料是程序b需要讀取的(如,列印這些資料),那麼程序b在程序a準備好這些資料之前必須一直等待。程序和線程的不同在于,線程共享同一個位址空間,而程序擁有單獨的位址空間。是以,用線程解決第1個問題比較容易。至于第2個問題,線程也同樣能解決。是以,了解同步機制非常重要。
在讨論ipc之前,我們先來考慮一個簡單的例子:cd刻錄機。當一個程序要刻錄一些内容時,會在特定的刻錄緩沖區中設定檔案句柄(我們立刻要刻錄更多的檔案)。另一個負責刻錄的程序,檢查待刻錄的檔案是否存在,如果存在,該程序将刻錄檔案,然後從緩沖區中移除該檔案的句柄。假設刻錄緩沖區有足夠多的索引,分别編号為i0、i1、i2等,每個索引都能儲存若幹檔案句柄。再假設有兩個共享變量:p_next和p_free,前者指向下一個待刻錄的緩沖區索引,後者指向緩沖區中的下一個空閑索引。所有程序都要使用這兩個變量。在某一時刻,索引i0和i2為空(即檔案已經刻錄完畢),i3和i5已經加入緩沖。同時,程序5和程序6決定把檔案句柄加入隊列準備刻錄檔案。這一狀況如圖2.7所示。

圖2.7
首先,程序5讀取<code>p_free</code>,把它的值i6儲存在自己的局部變量<code>f_slot</code>中。接着,發生了一個時鐘中斷,cpu認為程序5運作得太久了,決定轉而執行程序6。然後,程序6也讀取<code>p_free</code>,同樣也把i6儲存在自己的局部變量<code>f_slot</code>中。此時,兩個程序都認為下一個可用的索引是i6。程序6現在繼續運作,它把待拷貝檔案的句柄儲存在索引i6中,并更新<code>p_free</code>為i7。然後,系統讓程序6睡眠。現在,程序5從原來暫停的地方再次開始運作。它檢視自己的<code>f_slot</code>,發現可用的索引是i6,于是把自己待拷貝檔案的句柄寫到索引i6上,擦除了程序6剛寫入的檔案句柄。然後,程序5計算<code>f_slot+1</code>得i7,就把<code>p_free</code>設定為i7。現在,刻錄緩沖區内部保持一緻,是以刻錄程序并未出現任何錯誤。但是,程序6再也接收不到任何輸出。
程序6将被無限閑置,等待着再也不會有的輸出。像這樣兩個或更多實體讀取或寫入某共享資料的情況,最終的結果取決于程序的執行順序(即何時執行哪一個程序),這叫做競态條件(race condition)。
如何避免競态條件?大部分解決方案都涉及共享記憶體、共享檔案以及避免不同的程序同時讀寫共享資料。換句話說,我們需要互斥(mutual exclusion)或一種能提供獨占通路共享對象的機制(無論它是共享變量、共享檔案還是其他對象)。當程序6開始使用程序5剛用完的一個共享對象時,就會發生糟糕的事情。
程式中能被通路共享記憶體的部分叫做臨界區(critical section)。為了避免競态條件,必須確定一次隻能有一個程序進入臨界區。這種方法雖然可以避免競态條件,但是在執行并行程序時會影響效率,畢竟并行的目的是正确且高效地合作。要使用共享資料,必須處理好下面4個條件:
不允許同時有兩個程序在臨界區内;
不得對cpu的速度或數量進行假設;
在臨界區外運作的程序不得阻礙其他程序;
不得有任何程序處于永遠等待進入臨界區。
以上所述如圖2.8所示。過程a在t1時進入臨界區。稍後,程序b在t2嘗試進入其臨界區,但是失敗。因為另一個程序已經在臨界區中,同一時間内隻允許一個程序在臨界區内。在t3之前,程序b必須被臨時挂起。在程序a離開臨界區時,程序b便可立即進入。最終,程序b離開臨界區(t4時),又回到沒有程序進入臨界區的狀态。
圖2.8
下面是一個程序間通信的程式示例。我們建立的這個程式一開始就有兩個程序,它們要在一個普通視窗中完成繪制矩形的任務。從某種程度上看,這兩個程序需要互相通信,即當一個程序正在畫矩形時,另一個程序要等待。
準備就緒
确定安裝并運作了visual studio。
操作步驟
1. 建立一個新的預設c++控制台應用程式,命名為<code>ipcdemo</code>。
2. 右鍵單擊【解決方案資料總管】,并選擇【添加】-【建立項目】。選擇c++【win32控制台應用程式】,添加一個新的預設c++控制台應用程式,命名為<code>ipcworker</code>。
3. 在<code>ipcworker.cpp</code>檔案中輸入下面的代碼:
using namespace std;
typedef struct _tagcommunicationobject
{
hwnd hwndclient;
bool bexitloop;
long lsleeptimeout;
} communicationobject, *pcommunicationobject;
lresult callback wndproc(hwnd hdlg, uint umsg, wparam wparam, lparam lparam);
hwnd initializewnd();
pcommunicationobject pcommobject = null;
handle hmapping = null;
int _tmain(int argc, _tchar* argv[])
cout << "interprocess communication demo." << endl;
hwnd hwnd = initializewnd();
if (!hwnd)
{
cout << "cannot create window!" << endl << "error:t" <<
getlasterror() << endl;
return 1;
}
handle hmutex = createmutex(null, false, synchronizing_mutex_name);
if (!hmutex)
cout << "cannot create mutex!" << endl << "error:t" <<
hmapping = createfilemapping((handle)-1, null, page_readwrite, 0,
sizeof(communicationobject), communication_object_name);
if (!hmapping)
cout << "cannot create mapping object!" << endl << "error:t"
<< getlasterror() << endl;
pcommobject = (pcommunicationobject)mapviewoffile(hmapping,
file_map_write, 0, 0, 0);
if (pcommobject)
pcommobject->bexitloop = false;
pcommobject->hwndclient = hwnd;
pcommobject->lsleeptimeout = 250;
unmapviewoffile(pcommobject);
startupinfo startupinfored = { 0 };
process_information processinformationred = { 0 };
startupinfo startupinfoblue = { 0 };
process_information processinformationblue = { 0 };
bool bsuccess = createprocess(text("..\debug\ipcworker.exe"),
text("red"), null, null, false, 0, null, null, &startupinfored,
&processinformationred);
if (!bsuccess)
cout << "cannot create process red!" << endl << "error:t" <<
bsuccess = createprocess(text("..\debug\ipcworker.exe"),
text("blue"), null, null, false, 0, null, null, &startupinfoblue,
&processinformationblue);
cout << "cannot create process blue!" << endl << "error:t" <<
msg msg = { 0 };
while (getmessage(&msg, null, 0, 0))
translatemessage(&msg);
dispatchmessage(&msg);
unregisterclass(window_class_name, getmodulehandle(null));
closehandle(hmapping);
closehandle(hmutex);
cout << "end program." << endl;
return 0;
}
lresult callback wndproc(hwnd hwnd, uint umsg, wparam wparam, lparam lparam)
switch (umsg)
case wm_command:
{
switch (loword(wparam))
{
case button_close:
{
postmessage(hwnd, wm_close, 0, 0);
break;
}
}
break;
}
case wm_destroy:
pcommobject = (pcommunicationobject)mapviewoffile(hmapping,
file_map_write, 0, 0, 0);
if (pcommobject)
pcommobject->bexitloop = true;
unmapviewoffile(pcommobject);
postquitmessage(0);
default:
return defwindowproc(hwnd, umsg, wparam, lparam);
hwnd initializewnd()
wndclassex wndex;
wndex.cbsize = sizeof(wndclassex);
wndex.style = cs_hredraw | cs_vredraw;
wndex.lpfnwndproc = wndproc;
wndex.cbclsextra = 0;
wndex.cbwndextra = 0;
wndex.hinstance = getmodulehandle(null);
wndex.hbrbackground = (hbrush)(color_window + 1);
wndex.lpszmenuname = null;
wndex.lpszclassname = window_class_name;
wndex.hcursor = loadcursor(null, idc_arrow);
wndex.hicon = loadicon(wndex.hinstance, makeintresource(idi_application));
wndex.hiconsm = loadicon(wndex.hinstance, makeintresource(idi_application));
if (!registerclassex(&wndex))
return null;
hwnd hwnd = createwindow(wndex.lpszclassname,
text("interprocess communication demo"),
ws_overlappedwindow, 200, 200, 400, 300, null, null,
wndex.hinstance, null);
hwnd hbutton = createwindow(text("button"), text("close"),
ws_child | ws_visible | bs_pushbutton | ws_tabstop,
275, 225, 100, 25, hwnd, (hmenu)button_close, wndex.hinstance,
null);
hwnd hstatic = createwindow(text("static"), text(""), ws_child |
ws_visible, 10, 10, 365, 205, hwnd, null, wndex.hinstance, null);
showwindow(hwnd, sw_show);
updatewindow(hwnd);
return hstatic;
}<code>`</code>
示例分析
這次示範的示例有點難。我們需要兩個單獨的線程,是以在同一個解決方案中建立了兩個項目。
為了簡化這個示例,我們在主應用程式<code>ipcdemo</code>中建立了兩個程序<code>ipcdemo</code>将在應用程式視窗中繪制一個區域。如果沒有正确的通信和程序同步,就會發生多路通路共享資源的情況。考慮到作業系統會在程序間快速切換,而且大部分pc都有多核cpu,這很可能會導緻兩個程序同時畫一個區域,即多個程序同時通路未保護的區域。先來看<code>ipcworker</code>,這個名稱的意思是,需要程序為我們處理一些工作。
我們使用了一個映射對象(即,記憶體中為程序配置設定讀取或寫入的區域)。<code>ipcworker</code>或簡稱<code>worker</code>,要請求獲得一個已命名的互斥量。如果獲得互斥量,該程序就能處理并擷取一個指向記憶體區域(檔案映射)的指針,資訊将儲存在這個區域。必須獲得互斥量,才能進行獨占通路。程序在<code>waitforsingleobject</code>傳回後獲得互斥量。請看下面的語句:
pcommobject = ( pcommunicationobject )
mapviewoffile( hmapping, file_map_read, 0, 0, sizeof( communicationobject ) );<code>`</code>
調用<code>mapviewoffilewin32 api</code>獲得指向檔案映射對象的句柄(指針)。現在,程序可以從共享記憶體對象中讀取并獲得所需的資訊了。該程序要讀取<code>bexitloop</code>變量才能獲悉是否繼續執行。然後,該程序要讀取待繪制區域視窗的句柄(<code>hwnd</code>)。最後,還需要<code>lsleeptimeout</code>變量記錄程序睡眠多久。我們故意添加了sleep時間,因為程序間切換太快根本注意不到。
`
releasemutex( hmutex );`
調用<code>releasemutex win32 api</code>釋放互斥量的所有權,讓其他程序可以獲得互斥量,繼續執行其他任務。分析完<code>ipcworker</code>,我們來看<code>ipcdemo</code>項目。該項目定義了<code>_tagcommunicationobject</code>結構,用于整個檔案映射過程中對象之間的通信。
正是因為<code>ipcdemo</code>在運作<code>worker</code>程序之前就建立了檔案映射,是以從<code>worker</code>程序詢問檔案映射之前不用檢查檔案映射是否存在。<code>ipcdemo</code>建立并初始化應用程式視窗和待繪制區域後,建立了一個已命名的互斥量和檔案映射。然後,用不同的指令行參數(用以差別)建立不同的程序。
<code>wndproc</code>例程處理<code>wm_command和wm_destroy</code>消息。當我們需要通知應用程式安全地關閉時,<code>wm_command</code>觸發按鈕按下事件,而<code>wm_destroy</code>則釋放用過的檔案映射,并向主線程消息隊列寄送關閉消息:
postquitmessage( 0 );`
更多讨論
檔案映射要與常駐磁盤的檔案和常駐記憶體的檔案視圖一起運作。用記憶體的檔案視圖比用硬碟驅動的讀寫速度快。如果要用共享對象在程序之間處理一些簡單的事情,選用檔案映射是很好的程式設計習慣。如果把<code>createfilemapping api</code>的第1個參數設定為-1,磁盤中就不會有檔案存在:
bsuccess = createprocess( text( "..\debug\ipcworker.exe" ),
text( "red" ), null, null, false, 0, null, null,
&startupinfored, &processinformationred );<code>`</code>`
visual studio在調試模式中隻會從項目檔案夾開始啟動,不會從程式的<code>exe</code>檔案夾開始啟動。而且,visual studio預設把所有的win32項目都輸出到同一個檔案夾中。是以,在檔案路徑中,我們必須從項目檔案夾傳回上一級(檔案夾),然後找到debug檔案夾,整個項目的輸出(<code>exe</code>)就在這個檔案夾中。如果不想讓vs這樣啟動<code>exe</code>,就必須改變<code>createprocess</code>調用的路徑,或者添加通過指令行或其他類似方法通路檔案路徑的功能。