VB 版 (精华区)

发信人: bloom (├┝┞┟┠┢┣), 信区: VB
标  题: VB API教程(王国荣版)(八)(转载)
发信站: 哈工大紫丁香 (2000年09月07日18:43:11 星期四), 转信

【 以下文字转载自 cnTemp 讨论区 】
【 原文由 bloom 所发表 】
发信人: Love1976 (狄飞惊), 信区: VisualBasic       
发信站: BBS 水木清华站 (Thu Apr  6 04:22:14 2000)

发信人: coolknight (酷骑士~找工作中), 信区: VB
标  题: VB 與 Windows API 講座(八)
发信站: 武汉白云黄鹤站 (Tue Nov  9 20:13:11 1999), 站内信件

VB 與 Windows API 講座(八)
InterProcess Communication(行程通訊)(下)
 
http://www.kj.com.tw
王國榮
 
延續上一期的介紹,本期筆者預定介紹的主題有行程的同步執行(Process 
Synchroniza
tion)及多執行緒(MultiThread),這兩個主題雖然聽起來比較響亮,但筆者卻比較
常被
問到:「如何等待被啟動的程式結束執行?如何主動結束被啟動的程式?」,這兩
個問
題本期將有深入的探討,最後筆者還想針對上一期提出的資料傳遞,再介紹兩種實
用的
方法─WM_COPYDATA 訊息及 Pipe(資料緩衝管),本期的內容很多,二話不說,讓
我們開
始吧!
 
------------------------------------------------------------------------
----
----
程式的啟動與等待
------------------------------------------------------------------------
----
----
 
 
啟動其他程式最簡單的方法是呼叫 VB 的 Shell 敘述,例如:
 
Shell "notepad", vbNormalFocus
 
以這種方式啟動另一個程式之後,啟動者與被啟動者之間將各執行各的,彼此沒有
任何
關係。
 
啟動程式後,等待結束執行
------------------------------------------------------------------------
----
----
 
Shell 也可以當作函數使用,如果當函數使用,會傳回被啟動者的 Process ID,
利用
Process ID 我們可以取得 Process handle,接著再以 Process handle 為參數呼
叫 W
aitForSingleObject 函數,便可以達到「等待被啟動者結束執行」的目的,範例
程式如
下:(完整實例請參閱 deadwait.vbp 專案)
 
Dim pId As Long, pHnd As Long
 
pId = Shell("Notepad", vbNormalFocus) ' Shell 函數傳回 Process ID
pHnd = OpenProcess(SYNCHRONIZE, 0, pId) ' 取得 Process handle
If pHnd <> 0 Then
    Call WaitForSingleObject(pHnd, INFINITE) ' 無限等待,直到 Process 結

    Call WaitForSingleObject(pHnd, INFINITE) ' 無限等待,直到 Process 結

    Call CloseHandle(pHnd)
End If
 
以上程式呼叫了 OpenProcess、WaitForSingleObject、及 CloseHandle 三個 API
 函數
,其中 OpenProcess 可取得 Process handle,CloseHandle 是與 OpenProcess 
相對的
函數,作用是釋放 Process handle,這兩個函數的作用只是起頭與收尾,程式真
正的重
點在於 WaitForSingleObject 函數。
 
WaitForSingleObject 函數須傳入「物件 handle」及「等待時間」,在以上程式
中,筆
者分別傳入 pHnd(Process handle) 及 INFINITE(=&HFFFFFFFF,表示無限等待),
結果
將使得 WaitForSingleObject 函數一直等待 Process 的結束執行,如果 Process
 不結
束執行,則呼叫 WaitForSingleObject 函數的程式便不會向下執行。
 
圖-1 啟動程式後,等待結束
 
以上的等待方式很不好,因為在等待期間等待者(啟動者)完全不做任何事情(不接
受鍵盤
與滑鼠的輸入、不處理視窗的描繪工作、不接受任何訊息…),結果會讓使用者覺
得程式
好像死掉一樣,您可以執行筆者所提供的 deadwait.vbp,在按下「啟動 notepad
,並且
死等」鈕之後,試著拉曳被啟動的 notepad 程式,便可以瞭解等待者程式所出現
的現象

 
等待若干時間
等待若干時間
------------------------------------------------------------------------
----
----
 
無限等待讓筆者想到「七爺八爺」為了不見不散的承諾而淹死在橋下的故事,寫程
式應
該不必這麼死心眼,除了無限等待之外,我們可以指定等待的時間,方法是設定 
WaitF
orSingleObject 的參數二,等待時間的單位是「千分之一秒」,例如 
WaitForSingleO
bject( pHnd, 2000) 表示等待兩秒,如果被等待者在兩秒之內沒有結束執行,則
等待者
將不再等待,而直接執行後面的程式。
 
圖-2 將 WaitForSingleObject 的參數二設定為兩秒
 
其實如何設定等待時間是很難拿捏的,即使只是短暫的等待,使用者還是會覺得程
式執
行的不夠流暢。接著讓我們來研究解決之道。
 
利用迴圈判斷被啟動者是否已結束
------------------------------------------------------------------------
----
----
 
首先請比較無限等待的 WaitForSingleObject(pHnd, INFINITE) 與以下程式的差
別:
 
Do
    ret = WaitForSingleObject(pHnd, 0) ' 等待時間設定為 0
    ret = WaitForSingleObject(pHnd, 0) ' 等待時間設定為 0
Loop While ret = WAIT_TIMEOUT
 
以上程式有兩個重點:(1) WaitForSingleObject 的等待時間被設定為 0,這將使
得呼
叫 WaitForSingleObject 的程式立刻返回,繼續執行下一行程式 (2) 
WaitForSingleO
bject 的傳回值:雖然 WaitForSingleObject 不再死等被等待者的結束執行,但
它的傳
回值仍然可用來判斷被等待者是否已經結束,如果傳回值等於 
WAIT_TIMEOUT(=&H102),
表示被等待者還在執行,反之則否。
 
以上程式將等待時間設定為 0,並且根據 WaitForSingleObject 的傳回值來決定
是否脫
離迴圈,因此其結果與 WaitForSingleObject(pHnd, INFINITE) 的死等方式是相
同的。
再比較以下程式:(完整實例請參閱 loopwait.vbp 專案)
 
Do
     ret = WaitForSingleObject(pHnd, 0) ' 等待時間設定為 0
    DoEvents
Loop While ret = WAIT_TIMEOUT
 
以上程式不過是在迴圈裡面多加了 DoEvents 敘述,結果將使得讓程式能夠繼續接
收 W
indows 所傳送過來的事件,讓原本像死掉的程式再度活潑起來。
 
以上的解決方案確實不錯,不過筆者還是要說明一下它的缺點,首先是堆疊的使用
,當
程式程序進入某一程序時,都會使用到若干堆疊,而在正常狀況下,當程式離開程
序時
程式程序進入某一程序時,都會使用到若干堆疊,而在正常狀況下,當程式離開程
序時
(執行了 End Sub),堆疊就會被歸還,這使得堆疊在程式的運作過程中不會逐漸地
減少
,但以上程式卻會一直佔用著堆疊直到脫離迴圈為止,這將會增加「堆疊空間不足
」的
機率,筆者建議您在執行 loopwait.vbp 之後,多按幾次「啟動 notepad」鈕,然
後中
斷程式,並且選取 VB 功能表的「檢視/呼叫堆疊」,就可以瞭解堆疊使用的情形

 
除了堆疊問題之外,請再做個實驗,在被啟動的 notepad 程式還沒結束以前,就
按下
loopwait 表單的關閉鈕,結果 loopwait 的表單被關閉了,但程式卻還停留在執
行狀態
,沒有真正地結束,為什麼會這樣呢?這是因為 loopwait 程式還在等待 notepad
 程式
的緣故,所以不算真的結束執行,不過這個問題比較容易解決,只要在 
Form_Unload 事
件程序中加入 End 敘述即可。
 
------------------------------------------------------------------------
----
----
主動結束被啟動的程式
------------------------------------------------------------------------
----
----
 
如果說被啟動者一直不結束,啟動者能不能主動將它結束?答案是肯定的,方法很
簡單
,只要呼叫 TerminateProcess( pHnd, 0 ) 即可,完整實例請參考 Terminate.
vbp,不
過使用 TerminateProcess 來結束程式太「暴力」了,主要的缺點是被結束的程式
根本
就不會被詢問是否要存檔,這可能會造成資料的遺漏。
 
 
WM_CLOSE 訊息
------------------------------------------------------------------------
----
----
 
對於標準的 Windows 視窗來說,在收到 WM_CLOSE 訊息時,都會進行資料的儲存
,然後
自我結束,假設我們想結束視窗標題為 "未命名 - 記事本" 的程式,則可能撰寫
的程式
如下:(完整實例請參閱 close.vbp 專案)(註:若是 NT,則記事本剛被啟動時的
視窗標
題是 "未命名- 記事本",減號左邊少一個空白字元)
 
Dim hWnd As Long
hWnd = FindWindow(0&, ByVal "未命名 - 記事本")
Call SetForegroundWindow(hWnd)
Call PostMessage(hWnd, WM_CLOSE, 0, 0&)
 
首先是呼叫 FindWindow 取得 hWnd,然後再呼叫 SetForegroundWindow 將視窗調
到最
前端,最後則呼叫 PostMessage 送出 WM_CLOSE 訊息,PostMessage 的用法與我
們上一
期所介紹的 SendMessage 完全相同,所不同的是 SendMessage 會等待接收訊息的
視窗
處理完訊息之後才返回,而 PostMessage 則是將訊息送出後立刻返回,以上程式
筆者採
用 PostMessage 的原因是,WM_CLOSE 訊息的處理可能會花一點時間,送出訊息的
程式
沒有必要等待接收訊息的視窗處理完此一訊息才繼續向下執行。
 
也許有人會問,VB 程式又沒有處理 WM_CLOSE 訊息,此一方案對 VB 程式有效嗎
?答案
也許有人會問,VB 程式又沒有處理 WM_CLOSE 訊息,此一方案對 VB 程式有效嗎
?答案
是肯定的,VB 內建的視窗程序(參見第48期)會在收到 WM_CLOSE 訊息之後,將此
一訊息
轉換成 Form_Unload 事件,因此 VB 程式只要和平常一樣處理 Form_Unload 事件
,就
具備處理 WM_CLOSE 訊息的能力。
 
其實以上程式還是有個問題:由於視窗的標題是可能變動的(以記事本為例,尚未
存檔前
,標題是 "未命名 - 記事本",存檔後標題會變成 "檔案名 - 記事本"),而以上
程式所
呼叫的 FindWindow 卻是根據視窗的標題來尋找 hWnd,解決這個問題有點棘手,
筆者所
採用的方法是捨棄 FindWindow,而改用 EnumWindows 來尋找 hWnd。
 
EnumWindows 函數
------------------------------------------------------------------------
----
----
 
EnumWindows 的用途是列舉螢幕最上層的視窗,呼叫的方法如下:
 
Call EnumWindows( AddressOf EnumWindowsProc, lParam)
 
呼叫之後,Windows 會逐一列舉所有最上層視窗的 hWnd,再逐一傳遞給 
EnumWindowsP
roc Callback 函數(參數一,有關 Callback 函數意義請參閱第48期),所以我們
必須再
撰寫 EnumWindowsProc 函數方可讀取 Windows 所傳遞過來的 hWnd。
EnumWindowsProc
 函數的規格如下:
 
 
Function EnumWindowsProc(ByVal hWnd As Long, ByVal lParam As Long) As 
Boolea
n
 
除了參數一 hWnd 之外,參數二 lParam 將等於程式呼叫 EnumWindows 時所傳遞
進來的
 lParam。
 
圖-3 EnumWindows 範例程式
 
以下讓筆者舉個實例來說明 EnumWindows 及 EnumWindowsProc 的使用,首先請開
啟 E
numWin.vbp 專案,然後檢視 EnumWin.bas 之中的 EnumWindowsProc Callback 函
數,
如下:
 
Dim S As String
' 讀取 hWnd 的視窗標題
S = String(80, 0)
Call GetWindowText(hwnd, S, 80)
S = Left(S, InStr(S, Chr(0)) - 1)
' 若視窗標題不為空字串,即顯示在 ListBox 之中
If Len(S) > 0 Then Form1.List1.AddItem S
EnumWindowsProc = True ' 表示繼續列舉 hWnd
 
這段程式很簡單,首先是根據 hWnd 呼叫 GetWindowText 讀取視窗的標題,然後
顯示在
這段程式很簡單,首先是根據 hWnd 呼叫 GetWindowText 讀取視窗的標題,然後
顯示在

 Form1 的 ListBox 之中。
 
撰寫 EnumWindowsProc Callback 函數時,最重要的事情是函數傳回值的設定,上
面的
例子將函數的傳回值設定為 True(敘述為「EnumWindowsProc = True」),作用是
請 Wi
ndows 繼續列舉 hWnd,直到沒有視窗可列舉為止,如果將函數的傳回值設定為 
False(
敘述為「EnumWindowsProc = False」),則作用是請 Windows 停止列舉 hWnd。此
外請
注意 EnumWindowsProc Callback 函數一定要放在 .bas 的一般模組中。
 
有了以上的 EnumWindowsProc Callback 函數,接著程式若呼叫 EnumWindows,則
 Win
dows 便會開始列舉螢幕最上層的視窗,並且將 hWnd 及 lParam 傳遞給 
EnumWindowsP
roc,進而達到顯示所有最上層視窗標題的目的。
 
讀取被啟動者的 hWnd
------------------------------------------------------------------------
----
----
 
雖然 EnumWindows 可列舉所有最上層的視窗,但我們只要找到被啟動者的 hWnd,
然後
再送出 WM_CLOSE 訊息,即可達到目的,為了讓 EnumWindowsProc Callback 函數
找到
被啟動者的 hWnd,我們可以在呼叫 EnumWindows 時傳入被啟動者的 Process ID
,如下

 
' pID 等於 Shell( "notepad", vbNormalFocus) 的傳回值
' pID 等於 Shell( "notepad", vbNormalFocus) 的傳回值
Call EnumWindows(AddressOf EnumWindowsProc, pID)
 
而在 EnumWindowsProc Callback 函數中,則是呼叫 
GetWindowThreadProcessId 讀取
 hWnd 的 Process ID,然後再與 EnumWindows 所傳入的 Process ID 做比較,以
確定
所找到的是否為被啟動者的視窗,程式如下:
 
Function EnumWindowsProc(ByVal hwnd As Long, ByVal lParam As Long) As 
Boolea
n
    Dim pID As Long
 
    Call GetWindowThreadProcessId(hwnd, pID) ' 取得 hWnd 所屬的 pID
    If pID = lParam Then ' 判斷 pID 是否與 EnumWindows 所傳入的參數相同

        ' 如果成立, 表示找到 Process 的視窗 hWnd
    End If
End Function
 
除了以上程式之外,還要注意一個問題,假設每一個 Process 只開啟一個視窗,
那麼以
上程式應該只會找到一個 hWnd,但實際上不然,因為視窗含有子視窗是很正常的
現象,
為了過濾子視窗,筆者呼叫了 GetParent 函數,GetParent 會傳回父視窗 hWnd,
若無
父視窗,則傳回 0,修改後的程式如下:
 
...
...
Call GetWindowThreadProcessId(hwnd, pID) ' 取得 hWnd 所屬的 pID
If pID = lParam Then ' 判斷 pID 是否與 EnumWindows 所傳入的參數相同
    If GetParent(hWnd) Then ' 除了判斷 pID 是否相同之外,還要過濾子視窗

        ' 如果成立, 表示找到 Process 非子視窗的視窗 hWnd
    End If
End If
 
如此一來便可以找到正確的 hWnd,完整實例請參閱 pHndClose.vbp 專案。這個方
法雖
然比較麻煩一點,但是比 FindWindow 來得精準。
 
------------------------------------------------------------------------
----
----
再探資料傳遞
------------------------------------------------------------------------
----
----
 
上一期筆者曾經藉助剪貼簿及共用記憶體,並且搭配 SendMessage 函數來達成資
料傳遞
的工作,本期筆者想再介紹兩種資料傳遞的方法。
 
WM_COPYDATA 訊息
------------------------------------------------------------------------
----
----
----
 

由於不同程式的定址空間是各自獨立的,因此藉著傳遞位址而把資料傳遞給另一個
程式
的方案並不適用於 Win32(原因請參閱上一期),不過 WM_COPYDATA 訊息倒是個例
外,以
下就讓筆者來說明如何利用 WM_COPYDATA 訊息來傳遞資料。
 
首先我們必須把資料長度及位址填寫在 COPYDATASTRUCT 資料結構中,如下:
 
Dim cs As COPYDATASTRUCT
cs.cbData = 資料的長度
cs.lpData = 資料的位址
cs.dwData = 長整數資料 ' 可自訂
 
而傳遞時所撰寫的敘述則如下:(參數四即為 COPYDATASTRUCT 資料結構)
 
Call SendMessage(接收者的hWnd, WM_COPYDATA, 傳送者的hWnd, cs)
 
此一傳遞資料的方案十分簡單,但是對 VB 程式來說,卻有一個大困難,那就是 
VB 不
提供「資料的位址」,以致我們無法把資料的位址指定給 cs.lpData,為了解決這
個問
題,筆者用 C 語言寫了一個 DataAddress 函數,用途是取得 VB 資料的位址,這
個程
式的原始程式碼放在 DataAddr.c 之中,並且已經編譯成 .DLL 檔案(DataAddr.
dll)。

 
 
使用 DataAddress 函數之前,請先將 DataAddr.dll 複製到 Windows 的所在目錄
,然
後在 VB 程式中撰寫以下宣告式:
 
Declare Function DataAddress Lib "DataAddr.dll" (data As Any) As Long
 
至於用法則如下:
 
Dim X As Integer, bArr(128) As Byte
Addr1 = DataAddress(X) ' 一般性的資料, 以資料名稱為參數, 傳回資料的位址

Addr2 = DataAddress(bArr(0)) ' 陣列資料, 傳回陣列第 0 個元素的位址
Addr3 = DataAddress(bArr(8)) ' 陣列資料, 傳回陣列第 8 個元素的位址
 
但請注意此一函數無法取得字串的位址,原因是字串傳遞給 DLL 時會先經過 VB 
的轉換
(請參閱第51期「Unicode 與字串」段落中的說明),雖然如此,我們還是可以先把
字串
轉換成 Byte 陣列,然後再取得 Byte 陣列的位址。

 
圖-4 WM_COPYDATA 範例程式
 
接著讓筆者舉個利用 WM_COPYDATA 傳遞資料的實例,請同時載入執行筆者所提供
的 Da
taSend.vbp 及 DataRecv.vbp,然後按下 DataSend 程式的「CopyData」鈕,即可
將 T
extBox 之中的資料傳到 DataRecv 表單中。首先請看 DataSend 的程式:
 
 
Dim cs As COPYDATASTRUCT
Dim hWndRecv As Long, bArr() As Byte
bArr = StrConv(Text1.Text, vbFromUnicode) ' 將字串轉換成 Byte 陣列
cs.cbData = UBound(bArr) + 1 ' Byte 陣列的長度
cs.lpData = DataAddress(bArr(0)) ' Byte 陣列的位址
hWndRecv = FindWindow(0&, "DataRecv")
Call SendMessage(hWndRecv, WM_COPYDATA, Me.hwnd, cs)
 
這段程式很簡單,只要把 TextBox 之中的字串轉換成 Byte 陣列,再把 Byte 陣
列的長
度及位址指定給 cs.cbData 及 cs.lpData,接著即可呼叫 SendMessage 送出資料

 
再來看 DataRecv 之中的視窗程序:
 
Function WndProc(ByVal hwnd As Long, ByVal Msg As Long, ByVal wParam 
As Long
, ByVal lParam As Long) As Long
    If Msg = WM_COPYDATA Then
        Dim cs As COPYDATASTRUCT
        Call RtlMoveMemory(cs, ByVal lParam, Len(cs)) ' 取得 cs 資料結構

        ReDim bArr(0 To cs.cbData - 1) As Byte
        Call RtlMoveMemory(bArr(0), ByVal cs.lpData, cs.cbData)
        Form1.Text1.Text = StrConv(bArr, vbUnicode)
    Else
    Else
        WndProc = CallWindowProc(prevWndProc, hwnd, Msg, wParam, 
lParam)
    End If
End Function
 
以上程式有兩個關鍵處:(1) 視窗程序的 lParam 參數等於 cs 資料結構的位址,
所以
筆者使用「RtlMoveMemory(cs, ByVal lParam, Len(cs))」敘述將 lParam 的內容
複製
到 cs 資料結構中 (2) 取得 cs 資料結構之後,由於 cs.lpData 等於被傳送資料
的位
址,所以使用「RtlMoveMemory(bArr(0), ByVal cs.lpData, cs.cbData)」將資料
複製
到 bArr 陣列中。
 
最後筆者必須說明的是 WM_COPYDATA 訊息之所以能夠跨越程式傳遞資料,是因為
 Wind
ows 先把傳遞者的資料複製到共用記憶體,然後再把共用記憶體的位址傳遞給資料
接收
者,並不是直接將傳遞者的資料位址傳遞給資料接收者。
 
------------------------------------------------------------------------
----
----
Pipe(資料緩衝管)與資料傳遞
------------------------------------------------------------------------
----
----
 
連同上一期,筆者談了幾種資料傳遞的方法,它們都有一個共通點,那就是資料傳
遞者
以 SendMessage 來傳遞資料,但 SendMessage 最大的特性是資料傳遞者必須等待
資料
以 SendMessage 來傳遞資料,但 SendMessage 最大的特性是資料傳遞者必須等待
資料
接收者處理完資料之後才會返回,此一資料傳遞模式在大部分的情況下並沒有問題
,但
如果資料傳遞者本身的工作十分重視即時反應,而資料接受者偏偏又會花很多時間
來處
理所接收的資料,那麼在呼叫 SendMessage 期間,將會造成資料傳遞者失去即時
反應的
能力。
 
圖-5 利用 Pipe 來傳遞資料
 
此一問題可利用 Pipe(資料緩衝管) 來解決,對資料傳遞者來說,並不直接把資料
傳遞
給資料接收者,而是丟到 Pipe(資料緩衝管) 之中,由於 Pipe 一塊資料緩衝區,
因此
資料傳遞者不必等待資料接收者處理資料後才返回,而相對的,資料傳遞者也是從
 Pip
e 之中讀取資料。
 
使用 Pipe 要特別注意兩種情況的發生:
 
1. Pipe 滿檔時:此時若資料傳遞者企圖丟資料進入 Pipe,將會進入等待狀態,
直到有
資料被資料接收者讀走而空出可寫入資料的空間為止,因此 Pipe 的大小將影響資
料傳
遞者可能進入等待狀態的機率。
 
2. Pipe 之中完全沒有資料時:此時若資料接收者企圖讀取資料,也會進入等待狀
態,
直到又有資料被寫入 Pipe。
 
Pipe 的共用
Pipe 的共用
------------------------------------------------------------------------
----
----
 
Windows 所提供的 Pipe 並不是共用的資源,但如果兩個程式(Process)具有「父
子」關
係,且子 Process 又繼承了父 Process 的資源,則子 Process 可以使用父 
Process
所建立的 Pipe,因此想要利用 Pipe 來傳遞資料,首要之務是讓兩個 Process 產
生父
子及繼承的關係。
 
至於讓兩個 Process 產生父子及繼承的關係,其條件有二:(1) 子 Process 是由
父 P
rocess 所啟動的 (2) 父 Process 啟動子 Process 時,必須設定好繼承的參數。
為了
達到此一目的,我們必須呼叫 CreateProcess API 函數。
 
CreateProcess 函數的參數很多,也很囉唆,筆者暫時不深入介紹,本期筆者先利
用 C
reateProcess 撰寫了一個類似 VB Shell 的 ShellWithInherit 函數,此一函數
可建立
的具備繼承能力的子 Process,呼叫時只要傳入被啟動者(子 Process)的執行檔完
整路
徑名,若成功,將傳回子 Process ID,此一函數筆者收錄於 PipeWrite.bas 之中

 
Pipe 的建立
------------------------------------------------------------------------
----
----
 
除了建立具備繼承能力的子 Process 之外,父 Process 在建立 Pipe(呼叫 
CreatePip
除了建立具備繼承能力的子 Process 之外,父 Process 在建立 Pipe(呼叫 
CreatePip
e)時,也要將 Pipe 設定成可繼承的,建立的敘述如下:
 
Dim sa As SECURITY_ATTRIBUTES
sa.nLength = Len(sa) ' 設定 sa 資料結構的大小
sa.bInheritHandle = True ' 設定成可繼承的
Call CreatePipe(hPipeRead, hPipeWrite, sa, Pipe的大小) ' 建立 Pipe
 
在以上程式中,CreatePipe(建立 Pipe) 參數三 sa 的作用就是將 Pipe 設定可繼
承的
,除了參數三之外,參數四應該傳入 Pipe 的大小,若 CreatePipe 成功地建立 
Pipe,
參數一 hPipeRead 將傳回讀取 Pipe 所需之 handle 值,參數二則傳回寫入 Pipe
 所需
之 handle 值。
 
Pipe 的寫入與讀取
------------------------------------------------------------------------
----
----
 
寫資料進 Pipe 要呼叫 WriteFile 函數,呼叫的例子及各參數的意義如下:
 
Call WriteFile(hPipeWrite, length, 4, written, 0&) ' 呼叫例一, 一般資料

Call WriteFile(hPipeWrite, ByVal S, length, written, 0&) ' 呼叫例二, 字
串資料

 
 
◇ 參數一:傳入 hPipeWrite(來自 CreatePipe 的參數二)。
◇ 參數二:欲寫入 Pipe 的資料,由於此一參數採 As Any 的宣告方式,因此若
寫入的
是一般資料,直接填寫變數名稱即可,如呼叫例一,若寫入的是字串資料,則必須
在字
串之前加上 ByVal 保留字,如呼叫例二。
◇ 參數三:參數二的資料長度。
◇ 參數四:將傳回真正寫入 Pipe 的資料長度。
◇ 參數五:傳入 0& 即可。
 
讀取 Pipe 的資料則是呼叫 ReadFile,呼叫的例子及各參數的意義如下:
 
Call ReadFile(hPipeRead, length, 4, read, 0&)
S = String(length, Chr(0))
Call ReadFile(hPipeRead, ByVal S, length, read, 0&)
 
◇ 參數一:傳入 hPipeRead(來自 CreatePipe 的參數一)。
◇ 參數二:用來傳回從 Pipe 之中所讀取的資料,由於此一參數也是採 As Any 
的宣告
方式,所以寫法與 WriteFile 的參數二相同。
◇ 參數三:希望讀取的資料長度。
◇ 參數四:真正讀取的資料長度。
◇ 參數五:傳入 0& 即可。
 
請注意若 Pipe 之中沒有資料,而程式呼叫了 ReadFile,則程式會進入等待狀態
,如果
請注意若 Pipe 之中沒有資料,而程式呼叫了 ReadFile,則程式會進入等待狀態
,如果
進入等待狀態之後,又始終沒有其他程式寫資料進 Pipe,則等待的程式將形同死
當一般
,要避免此一問題,可先呼叫 PeekNamedPipe 函數以確定 Pipe 之中是否含有資
料,以
下是呼叫 PeekNamedPipe 函數的例子:(註:Peek 是「偷窺」的意思,所以此一
函數是
用來窺視 Pipe 之中的資料,但不會將資料讀走)
 
Dim length As Long, read As Long
PeekNamedPipe(hPipeRead, length, 4, read, 0&, 0&)
If read = 4 Then
    Call ReadFile(hPipeRead, length, 4, read, 0&)
    ...
End If
 
PeekNamedPipe 函數的前四個參數與 ReadFile 相同,後兩個參數補 0& 即可,它
的功
能與 ReadFile 相同,但是:(1) 不會把 Pipe 之中的資料讀走 (2) 如果 Pipe 
之中沒
有資料,則 PeekNamedPipe 仍會正常返回,不像 ReadFile 會進入等待狀態。
 
範例程式:PipeWrite 及 PipeRead
------------------------------------------------------------------------
----
----
 
最後來看筆者所撰寫的範例程式,其中 PipeWrite 程式會主動建立 Pipe,而按下
「啟
動 PipeRead」鈕會啟動 PipeRead 程式,並且將 PipeRead 程式建立成具有繼承
能力的
動 PipeRead」鈕會啟動 PipeRead 程式,並且將 PipeRead 程式建立成具有繼承
能力的
子 Process,按下「傳遞 PipeRead Handle」鈕會把 hPipeRead 傳遞給 PipeRead
 程式
,按下「讀取 test.txt 並且寫入 Pipe」則會開啟 test.txt 檔案,然後寫到 
Pipe 之
中。
 
圖-6 PipeWrite 程式
 
在 PipeWrite 程式中,比較特別的是寫資料進 Pipe 的部分,如下:
 
Line Input #1, S
S = S & vbCrLf
length = LenB(StrConv(S, vbFromUnicode))
Call WriteFile(hPipeWrite, length, 4, written, 0&) ' 寫入「字串長度」
Call WriteFile(hPipeWrite, ByVal S, length, written, 0&) ' 寫入「字串內
容」
 
其中「length = LenB(StrConv(S, vbFromUnicode))」的用途是取得字串的位元組
數(中
文字佔兩個位元組,英文字佔一個位元組),然後把這個長度寫入 Pipe,最後才寫
入「
字串內容」,筆者之所以要寫入「字串長度」的理由是,PipeRead 若不知道字串
長度,
而直接讀取字串內容,可能會讀取半個中文字(也就是只讀取中文字的第一個字元
),結
果將會產生亂碼,若可以先讀取字串長度,再讀取字串內容,將可避免此一問題。
由於
 PipeWrite 程式寫入了字串的長度及內容,所以 PipeRead 相對應的程式如下:

 
Call ReadFile(hPipeRead, length, 4, read, 0&) ' 讀取「字串長度」
Call ReadFile(hPipeRead, length, 4, read, 0&) ' 讀取「字串長度」
S = String(length, Chr(0)) ' 根據字串長度配置字串
Call ReadFile(hPipeRead, ByVal S, length, read, 0&) ' 讀取「字串內容」
 
除了讀取 Pipe 之中的資料外,PipeRead 主要的工作還有:(1) 撰寫視窗程序接
收 Pi
peWrite 程式所傳遞過來的 PipeRead handle 值(hPipeRead),(2) 為了模擬 
PipeRea
d 處理資料比較慢的效果,PipeRead 會利用 Timer 控制元件每 0.1 秒讀取 Pipe
 一次
,然後把資料顯示在 TextBox 之中。
 
圖-7 PipeRead 程式
 
------------------------------------------------------------------------
----
----
行程的同步執行(Process Synchronization)
------------------------------------------------------------------------
----
----
 
當兩(多)個程式使用到共用資源(記憶體)時,為了避免在使用資源的期間,被其他
程式
干擾或破壞,便有「同步執行」的需求,首先請回顧上一期所提出 x = x + 1 問
題,解
決這個問題最簡單的方法是利用呼叫 InterlockedIncrement(x)、
InterlockedDecreme
nt(x)、及 InterlockedExchange(x, v) 三個函數,這三個函數所執行的敘述分別
等於
:(API 宣告式請參閱 Interlock.bas)
 
 
InterlockedIncrement(x):x = x + 1
InterlockedDecrement(x):x = x - 1
InterlockedExchange(x, v):x = v
 
當我們呼叫以上函數時,Windows 保證在 x 被正確地更新以前,別的程式不會讀
寫 x。

 
x = x + n 又如何呢?
------------------------------------------------------------------------
----
----
 
假如我們希望執行 x = x + n 敘述,可以利用前面三個函數來達成目的嗎?寫成
以下敘
述如何?
 
Call InterlockedExchange(x, x + n)
 
答案是否定的,因為以上敘述其實會被分成:
 
temp = x + n
Call InterlockedExchange(x, temp)
 
而就在「temp = x + n」執行的期間,x 的值可能會遭到破壞,要執行「x = x 
+ n」敘
而就在「temp = x + n」執行的期間,x 的值可能會遭到破壞,要執行「x = x 
+ n」敘
述,其中一個方法是呼叫 InterlockedExchangeAdd API 函數,此一函數所執行的
動作
是:
 
InterlockedExchangeAdd(x, n):x = x + n
 
但比較可惜的是 Windows 95 並不支援此一函數(目前只有 NT 支援,將來 
Windows 98
 應該也會支援)。當然,除了 InterlockedExchangeAdd 函數之外,Mutex 也可以
用來
解決 x = x + n 的問題。
 
Mutex(互斥器)
------------------------------------------------------------------------
----
----
 
我的字典裡沒有Mutex 這個字,它是 Mutual Exclusion 的組合字,意思是「互相
排斥
」,由於 Mutex 沒有標準的翻譯,筆者姑且將它翻譯成互斥器,但本文還是以原
文稱之
。Mutex 的用途,簡單地說就是「我用的時候,你不能用;你用的時候,我不能用
」,
它的角色與前面介紹的 InterlockedXXXX 函數相似(Interlocked 的意思是「我鎖
定的
時候,你不能用;你鎖定的時候,我不能用」),只是 InterlockedXXXX 函數所提
供的
是變數鎖定的功能,而 Mutex 則是藉著互斥的關係而達到共用資源鎖定的功能。

 
筆者私底下稱 Mutex 為「獨享金牌」,舉例來說,程式 A 與程式 B 都會使用到
共用變
數 x,當程式 A 想要獨享 x 時(例如執行 x = x + 1 敘述之前),即「亮出金牌
」,接
數 x,當程式 A 想要獨享 x 時(例如執行 x = x + 1 敘述之前),即「亮出金牌
」,接
著即使程式 B 也想亮出金牌,但只有等待程式 A「收回金牌」才輪到程式 B 獨享
共用
變數 x,當然,程式 B 用完共用變數 x 時,也應該收回金牌,好讓程式 A 下次
又有機
會使用,要瞭解此一關係,請同時執行筆者所提供的 Mutex01.exe 及 Mutex02.
exe,並
且參考以下步驟進行測試:
 
1. 按下 Mutex01 的「Wait For "MutexX" 亮出金牌」鈕,接著表單上會顯示「現
在可
以放心獨享 Mutex01 與 Mutex02 共用的資源」。
2. 按下 Mutex02 的「Wait For "MutexX" 亮出金牌」,結果沒有反應,此時試著
拉曳
 Mutex02 的表單,結果也沒有反應,這是因為 Mutex02 進入等待 Mutex 的狀態

3. 按下 Mutex01 的「Release "MutexX" 收回金牌」,接著看到 Mutex02 恢復了
正常
,表示輪到了 Mutex02 使用金牌。
4. 按下 Mutex01 的「Wait For "MutexX" 亮出金牌」,結果輪到 Mutex01 進入
等待狀
態。
5. 按下 Mutex02 及 Mutex01 的「Release "MutexX" 收回金牌」讓一切恢復正常

 
圖-8 利用 Mutex01 及 Mutex02 程式來瞭解 Mutex 的特性
 
在 Mutex01 及 Mutex02 程式中,所謂「亮出金牌」指的是呼叫 
WaitForSingleObject
 函數(其實就是等著獨享金牌的意思),而「收回金牌」則是呼叫 ReleaseMutex。

 
呼叫 WaitForSingleObject 及 ReleaseMutex 之前,不管是 Mutex01 或 Mutex02
 都必
須先呼叫 CreateMutex 建立「相同名稱」的 Mutex,如此才不至於 Mutex01 與 
Mutex
須先呼叫 CreateMutex 建立「相同名稱」的 Mutex,如此才不至於 Mutex01 與 
Mutex
02 各使用不同的金牌,而無法達到「你用時我不用,我用時你不用」的效果。
CreateM
utex 呼叫的例子如下:
 
hMutex = CreateMutex(ByVal 0&, False, "MutexX")
 
其中參數三是用來指定金牌的名稱(本例是 "MutexX"),而傳回值等於 
hMutex(Mutex h
andle 值),有了 hMutex 值,接下來程式才能夠呼叫 WaitForSingleObject 及 
Relea
seMutex。
 
還有件有趣的事情,筆者在 Mutex01 及 Mutex02 之中都以 CreateMutex 建立 
Mutex,
而參數三也是傳入相同的名稱 "MutexX",這麼做並不會建立兩個 Mutex,因為第
二個程
式呼叫 CreateMutex 時,Windows 會傳回第一個程式所建立的 hMutex 值。
 
除了 Mutex 的建立(CreateMutex)、等待(WaitForSingleObject)、及釋放
(ReleaseMut
ex)之外,筆者在 Mutex01(02) 程式結束時也都會呼叫 CloseHandle(hMutex) 將
 Mute
x 歸還系統,CloseHandle 是與 CreateMutex 相對的函數,CreateMutex 會把 
Mutex
的被使用次數加一,而 CloseHandle 則會把 Mutex 的被使用次數減一,當被使用
次數
減到 0 時,Mutex 就會被破壞歸還系統。
 
根據 Mutex01 及 Mutex02 所做的實驗,我們可以歸納出以下幾點使用 Mutex 的
方法:

 
 
1. 如果程式會使用共用資源,則啟動時應呼叫 CreateMutex 建立此一共用資源的
 Mut
ex,如果共用的資源有多個,則建立不同名稱的多個 Mutex。
2. 開始使用共用資源以前,先呼叫 WaitForSingleObject 等待 Mutex。
3. 使用共用資源之後,立刻呼叫 ReleaseMutex,釋放 Mutex。
4. 程式結束時應呼叫 CloseHandle,如此才不至於出現 Mutex 留在系統中,而沒
有程
式在使用的浪費情事。
 
Semaphore
------------------------------------------------------------------------
----
----
 
Semaphore 應該怎麼翻譯呢?筆者真的不知道,也許可以叫它做「使用人數管制器
」,
假設某一 Semaphore 的最多使用人數被設定成 3,那麼此一 Semaphore 在任何時
間的
使用人數可能是 0、1、2、或 3,但絕對不可以超過 3。
 
Semaphore 所提供的 API 函數與 Mutex 相類似,包含 CreateSemaphore、
WaitForSin
gleObject、及 ReleaseSemaphore,在真實生活中,Semaphore 與餐廳的用餐很像
,假
設某一餐廳的可容納人數是 10 人,當我們要進入餐廳用餐時,服務生會先檢查是
否還
有空位,如果沒有空位,就必須「等待」(相當於 WaitForSingleObject)他人用餐
完畢
「空出座位」(相當於 ReleaseSemaphore),才可以進入餐廳。
 
想要瞭解 Semaphore 的用法,請先試用筆者所提供的 Sema.exe 程式(原始程式碼
收錄
想要瞭解 Semaphore 的用法,請先試用筆者所提供的 Sema.exe 程式(原始程式碼
收錄
於 Sema.vbp 專案),測試時請執行 Sema.exe 兩次,當 Sema.exe 第一次被啟動
時,程
式會呼叫 CreateSemaphore 建立名稱為 "SemaX" 的 Semaphore,並且將 
Semaphore 的
「座位」及「空位」設定為 5,其中所呼叫的敘述如下:
 
hSema = CreateSemaphore(ByVal 0&, 5, 5, "SemaX")
 
幾個參數的意義如下:
 
◇ 參數一:補上 ByVal 0& 即可。
◇ 參數二:「空位」數。
◇ 參數三:「座位」數。
◇ 參數四:Semaphore 名稱。
◇ 傳回值:Semphore handle。
 
當我們第二次啟動 Sema.exe 時,Sema.exe 雖然也會呼叫 CreateSemaphore 建立
名稱
為 "SemaX" 的 Semaphore,但由於此一名稱已經存在,所以 Semaphore 並不會重
新被
建立,而既有 Semaphore 的「座位」及「空位」數也不會改變。
 
試用 Sema 程式時,按下「WaitForSingleObject」鈕將會企圖從 Semaphore 之中
取得
一個座位,此時若有空位,則空位數減一程式立刻返回,若無空位,則進入等待狀
態,
您可以在同一個 Sema 程式表單上連續按下多次「WaitForSingleObject」直到程
式進入
等待狀態為止,此時進入等待狀態的程式會好像死掉一樣,但沒關係,請按下另一
個 S
等待狀態為止,此時進入等待狀態的程式會好像死掉一樣,但沒關係,請按下另一
個 S
ema 程式的「ReleaseSemaphore」空出一個座位(Windows 沒有規定 
ReleaseSemaphore
 一定要由呼叫 WaitForSingleObject 的程式來呼叫),而讓另一個進入等待狀態
的 Se
ma 程式再度活起來。在程式的撰寫上,ReleaseSemaphore 的呼叫例子如下:
 
Call ReleaseSemaphore(hSema, 1, count)
 
其中參數一傳入 Semaphore handle、參數二傳入欲空出的座位數,參數三則會傳
回呼叫
 ReleaseSemaphore 之前(請注意,不是之後)的空位剩餘數,若要計算呼叫 
ReleaseSe
maphore 之後的空位剩餘數,必須將參數二加到參數三。
 
除了 CreateSemaphore 及 ReleaseSemaphore 之外,Sema 程式也會呼叫 
WaitForSing
leObject 及 CloseHandle 函數,但它們的用法與前面的 Mutex01 程式相同,筆
者不再
重述。
 
Event(事件)
------------------------------------------------------------------------
----
----
 
Windows API 所提供的 Event(事件)功能,與 VB 的事件驅動觀念並不相同。在 
VB 程
式中我們會先寫好事件驅動程式,而當某一事件發生時,事件驅動程式便會被執行
,但
 API 的 Event 則比較像是交通號誌的「綠燈」,當程式執行到 
WaitForSingleObject
( hEvent, 等待時間) 時,系統會先檢查被等待的 hEvent 是否為亮燈狀態,若是
,則
( hEvent, 等待時間) 時,系統會先檢查被等待的 hEvent 是否為亮燈狀態,若是
,則
程式通行(也就是繼續執行),否則便進入等待狀態。
 
要改變 Event 的燈號狀態有幾種方法:
 
1. 呼叫 SetEvent( hEvent ) 將燈號打開,此時任何等待 hEvent 的程式一律通
行。
2. 呼叫 ResetEvent( hEvent ) 將燈號關閉,禁止通行。
3. 呼叫 PulseEvent( hEvent ) 將燈號打開後又立刻關閉,正在等待 hEvent 的
程式可
通行,但後來才呼叫 WaitForSingleObject 的程式則進入等待狀態。
 
就 Event 的建立方法,筆者習慣採用以下敘述:
 
hEvent = CreateEvent(ByVal 0&, True, False, "Event名稱")
 
各參數的意義如下:
 
◇ 參數一:補上 ByVal 0& 即可。
◇ 參數二:是否採用手動調整燈號。
◇ 參數三:燈號的起始狀態,False 表示熄燈。
◇ 參數四:Event 名稱。
◇ 傳回值:Event handle。
 
比較特別的是參數二,True表示我們自己會呼叫 SetEvent、ResetEvent、及 
PulseEve
比較特別的是參數二,True表示我們自己會呼叫 SetEvent、ResetEvent、及 
PulseEve
nt 來改變燈號的狀態,False則表示若有某一程式呼叫 WaitForSingleObject,
Event
即進入熄燈狀態,直到原程式再度呼叫 SetEvent,燈號才會再度打開。
 
Event 的用法比較簡單,筆者不再舉例說明,有關 Event 之 API 宣告式請參閱 
Event
.bas。
 
Waitable Timer(可等待的計時器)
------------------------------------------------------------------------
----
----
 
Waitable Timer 很像筆者所戴的功能錶,可以像鬧鐘一樣定時,也可以每隔若干
時間響
一次,聽起來好像還不賴,不過它是 NT4 新增的功能,Windows 95 並不提供,所
以筆
者暫時不介紹。其實 Waitable Timer 與 VB 的 Timer 控制元件在功能上很類似
,沒有
 Waitable Timer 可用,我們還是可以使用 Timer 控制元件。
 
------------------------------------------------------------------------
----
----
MultiThread vs. MultiProcess
------------------------------------------------------------------------
----
----
 
VB 不提供 MultiThread(多執行緒) 的功能,上一期筆者就這麼說了,但這樣的功
能有
VB 不提供 MultiThread(多執行緒) 的功能,上一期筆者就這麼說了,但這樣的功
能有
多重要呢?少了這樣的功能 VB 程式會少一塊肉嗎?
 
首先讓筆者來說明 Process 與 Thread 之間的關係,在一般觀念中,Process 代
表著一
個執行中的程式,但 Win32 則把執行中的程式分成 Process 與 Thread 兩部分,
Proc
ess 專司記憶體定址空間與程式資源的管理,Thread 才表示執行中的程式,就程
式的執
行而言,兩者缺一不可,若只有 Process,則程式徒有定址空間而不能執行,若只
有 T
hread 也不行,畢竟沒有一個程式的執行是不需要記憶體的。
 
就 Process 與 Thread 的關係來看,先有 Process,才有 Thread,想想,Thread
 執行
時所需之記憶體屬於 Process 所管轄,自然要先有 Process。有了 Process 之後
,接
著可以建立多個 Thread,也就是所謂的 MultiThread,但 VB 的情況有點不同,
程式執
行之後首先會建立 Process,然後再建立主 Thread,接著就不能再建立第二個 
Thread
 了,不像 C/C++ 可以再建立子 Thread,進而建構出 MultiThread 的程式。
 
偶而會有讀者問筆者:「VB 如何撰寫 MultiThread 的程式?」,筆者總是反問:
「為
什麼您需要 MultiThread 的程式?」(因為 VB 不能,所以轉移話題,這招好像不
錯!
),本期容筆者再多問一個問題:「無法撰寫 MultiThread 程式,那麼撰寫 
MulitProc
ess 程式,能不能滿足您的需求?」(因為 VB 不能,所以再把問題複雜化!)
 
圖-9 同一 Process 的多個 Thread 可共享 Process 的記憶體及資源
 
其實筆者無意為難讀者,首先讓筆者說明 MultiThread 的優點,請參考圖-9,由
於同一
其實筆者無意為難讀者,首先讓筆者說明 MultiThread 的優點,請參考圖-9,由
於同一
 Process 的不同 Thread 雖然擁有自己的堆疊(Stack),但定址空間則屬於一個 
Proce
ss,因此可以共用 Process 所擁有的資源及 Heap 記憶體(註:Stack 是用來存放
區域
變數,而 Heap 則是用來存放靜態變數及全域變數),此一功能的好處是,分工負
責不同
工作的 Thread 在共用記憶體及資源很方便的情況下,也很容易整合。如果是單一
 Thr
ead(請參考圖-10),由於不同 Process 的定址空間是各自獨立的,因此某一個 
Proces
s 所擁有的資源與 Heap 記憶體只能夠給該 Process 的 Thread 所使用,而無法
給另一
個 Process 的 Thread 所使用。
 
圖-10 不同 Process 如何共用記憶體及資源
 
雖然 VB 只能撰寫單一 Thread 的程式,但我們可以把程式規劃成 MultiProcess
,然後
利用上一期介紹的「檔案對映記憶體」讓不同 Process 得以共用記憶體,此外,
利用這
兩期所介紹的 IPC 功能則可以讓不同 Process 達到共用資源的目的。

--
" The Matrix is everywhere, it's all around us, here even in this room.
 You
 can see it out your window, or  on your television. You feel it when 
you
 go to work, or go to church or pay your taxes. It is the world that has
 been
 pulled over your eyes to blind you from the truth... Unfortunately, 
no one
can be told what the Matrix is. You have to see it for yourself."
                                                                   
Morphe

 Process 的不同 Thread 雖然擁有自己的堆疊(Stack),但定址空間則屬於一個 
Proce
--
" The Matrix is everywhere, it's all around us, here even in this room.
 You
 can see it out your window, or  on your television. You feel it when 
you
 go to work, or go to church or pay your taxes. It is the world that has
 been
 pulled over your eyes to blind you from the truth... Unfortunately, 
no one
can be told what the Matrix is. You have to see it for yourself."
                                                                   
Morphe

※ 来源:.武汉白云黄鹤站 bbs.whnet.edu.cn.[FROM: 202.114.3.124]

--
我并不是在等待奇迹,因为我知道没有奇迹的。
有的,也只是爱情、意志和勇气。
是这些东西的重叠后,而成为奇迹的。
所以,我从未曾想过放弃。

※ 修改:·Love1976 於 Apr  6 04:23:24 修改本文·[FROM: 202.112.140.138]

--
☆ 来源:.哈工大紫丁香 bbs.hit.edu.cn.[FROM: blo0m.bbs@smth.org]
--
※ 转载:.哈工大紫丁香 bbs.hit.edu.cn.[FROM: 202.118.247.254]
[百宝箱] [返回首页] [上级目录] [根目录] [返回顶部] [刷新] [返回]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:225.949毫秒