本文首先討論16位Windows下不具備的線程的概念,然後着重講述在32位Windows 95環境下多線程的編程技術,最後給出利用該技術的一個實例,即基於Windows95下TCP/IP的可視電話的實現。
一、問題的提出
作者最近在開發基於Internet網上的可視電話過程中,碰到了這樣一個問題。在基於Internet網上的可視電話系統中,同時要進行語音採集、語音編解碼、圖象採集、圖象編解碼、語音和圖象 碼流的傳輸, 所有的這些事情,都要並行處理。特別是語音信號,如果進行圖象編解碼時間過長,語音信號得不到服務,通話就有間斷,如果圖象或語音處理時間過長,而不能及時的傳輸碼流數據,通信同樣也會中斷。這樣就要求我們實現一種並行編程,在只有一個CPU的機器上,也就是要將該CPU時間按照一定的優先準則分配給各個事件,定期處理某一事件而不會在某一事件處理過長,在32位Windows95或WindowsNT下,我們可以用多線程的編程技術來實現這種並行編程。實際上這種並行編程在很多場合下都是必須的。例如,在FileManager拷貝文件時,它顯示一個對話框, 列出源文件和目標文件的名稱,並在對話框中包含了一個Cancel按鈕。如果在文件拷貝過程中,點中Cancel按鈕,就會終止拷貝。
在16位Windows中,實現這類功能需要在FileCopy循環內部週期性地調用PeekMessage函數。如果正在讀一個很大的數據塊,則只有當這個塊讀完以後才能響應這個按鈕動作,如果從軟盤讀文件,則要花費好幾秒的時間,由於機器反應太遲鈍,你會頻繁地點中這個按鈕,以爲系統不知道你想終止這個操作。如果把FileCopy指令放入另外一個線程,你就不需要在代碼中放一大堆PeekMessage函數,處理 用戶界面的線程將與它分開操作,這樣,點中Cancel按鈕後會立即得到響應。同樣的道理,在應用程序中創建一個單獨線程來處理所有打印任務也是很有用的,這樣,用戶可以在打印處理時繼續使用應用程序。
二、線程的概念
爲了瞭解線程的概念,我們必須先討論一下進程的概念。
一個進程通常定義爲程序的一個實例。在Win32中, 進程佔據4GB的地址空間。與它們在MS-DOS和16位Windows操作系統中不同, Win32進程是沒有活力的。這就是說,一個Win32進程並不執行什麼指令,它只是佔據着4GB的地址空間,此空間中有應用程序EXE文件的 代碼和數據。EXE需要的任意DLL也將它們的代碼和數據裝入到進程的地址空間。除了地址空間,進程還佔有某些資源,比如文件、動態內存分配和線程。當進程終止時,在它生命期中創建的各種資源將被清除。
但是進程是沒有活力的,它只是一個靜態的概念。爲了讓進程完成一些工作,進程必須至少佔有一個線程,所以線程是描述進程內的執行,正是線程負責執行包含在進程的地址空間中的代碼。實際上,單個進程可以包含幾個線程, 它們可以同時執行進程的地址空間中的代碼。爲了做到這一點,每個線程有自己的一組CPU寄存器和堆棧。
每個進程至少有一個線程在執行其地址空間中的代碼,如果沒有線程執行進程 地址空間中的代碼, 進程也就沒有繼續存在的理由,系統將自動清除進程及其地址空間。爲了運行所有這些線程,操作系統爲每個獨立線程安排一些CPU 時間,操作系統以輪轉方式向線程提供時間片,這就給人一種假象,好象這些線程都在同時運行。創建一個Win32進程時,它的第一個線程稱爲主線程,它 由系統自動生成,然後可由這個主線程生成額外的線程,這些線程,又可生成更多的線程。
三、線程的編程技術
1、編寫線程函數
所有線程必須從一個指定的函 數開始執行,該函數稱爲線程函數,它必須具有下列原型:
DWORDWINAPIYourThreadFunc(LPVOIDlpvThreadParm);
該函數輸入一個LPVOID型的參數,可以是一個DWORD型的整數,也可以是一個指向一個緩衝區的指針, 返回一個DWORD型的值。象WinMain函數一樣,這個函數並不由操作系統調用, 操作系統調用包含在KERNEL32.DLL中的非C運行時的一個內部函數,如StartOfThread,然後由StartOfThread函數建立起一個異常處理框架後,調用我們的函數。
2、創建一個線程
一個進程的主線程是由操作系統自動生成,如果你要讓一個主線程創建額外的線程,你可以調用來CreateThread完成。
HANDLECreateThread(LPSECURITY_ATTRIBUTES lpsa,DWORDcbstack,LPTHREAD_START_ROUTINElpStartAddr,
LPVOID lpvThreadParm,DWORDfdwCreate,LPDWORDlpIDThread);
其中lpsa參數爲一個指向SECURITY_ATTRIBUTES結構的指針。如果想讓對象爲缺省安全屬性的話,可以傳一個NULL,如果想讓任一個子進程都可繼承一個該線程對象句柄,必須指定一個SECURITY_ATTRIBUTES結構,其中bInheritHandle成員初始化爲TRUE。參數cbstack表示線程爲自己所用堆棧分配的地址空間大小,0表示採用系統缺省值。
參數lpStartAddr用來表示新線程開始執行時代碼所在函數的地址,即爲線程函數。lpvThreadParm爲傳入線程函數的參數,fdwCreate參數指定控制線程創建的附加標誌,可以取兩種值。如果該參數爲0,線程就會立即開始執行,如果該參數爲CREATE_SUSPENDED,則系統產生線程後,初始化CPU,登記CONTEXT結構的成員,準備好執行該線程函數中的第一條指令,但並不馬上執行,而是掛起該線程。最後一個參數lpIDThread 是一個DWORD類型地址,返回賦給該新線程的ID值。
3、終止線程
如果某線程調用了ExitThread 函數,就可以終止自己。
VOIDExitThread(UINTfuExitCode );
這個函數爲調用該函數的線程設置了退出碼fuExitCode後, 就終止該線程。調用TerminateThread函數亦可終止線程。
BOOLTerminateThread(HANDLE hThread,DWORDdwExitCode);
該函數用來結束由hThread參數指定的線程, 並把dwExitCode設成該線程的退出碼。當某個線程不在響應時,我們可以用其他線程調用該函數來終止這個不響應的線程。
4、設定線程的相對優先級
當一個線程被首次創建時,它的優先級等同於它所屬進程的優先級。在單個進程內可以通過調用SetThreadPriority函數改變線程的相對優先級。一個線程的優先級是相對於其所屬的進程的優先級而言的。
BOOLSetThreadPriority(HANDLE hThread,intnPriority);
其中參數hThread是指向待修改 優先級線程的句柄,nPriority可以是以下的值:
THREAD_PRIORITY_LOWEST,
THREAD_PRIORITY_BELOW_NORMAL,
THREAD_PRIORITY_NORMAL,
THREAD_PRIORITY_ABOVE_NORMAL,
THREAD_PRIORITY_HIGHEST
5、掛起及恢復線程
先前我提到過可以創建掛起狀態的線程(通過傳遞CREATE_SUSPENDED標誌給函數CreateThread來實現)。當你這樣做時,系統創建指定線程的核心對象,創建線程的棧,在CONTEXT結構中初始化線程CPU註冊成員。然而,線程對象被分配了一個初始掛起計數值1,這表明了系統將不再分配CPU去執行線程。要開始執行一個線程,另一個線程必須調用ResumeThread並傳遞給它調用CreateThread時返回的線程句柄。
DWORD ResumeThread(HANDLEhThread);
一個線程可以被掛起多次。如果一個線程被掛起3次, 則該線程在它被分配CPU之前必須被恢復3次。除了在創建線程時使用CREATE_SUSPENDED標誌,你還可以用SuspendThread函數掛起線程。
DWORDSuspendThread(HANDLE hThread);
四、多線程編程技術的應用
我在前面說過,爲了實現基於TCP/IP下的可視電話,就必須“並行”地執行語音採集、語音編解碼、圖象採集、圖象編解碼以及碼流數據的接收與發送。語音與圖象的採集由硬件採集卡進行,我們的程序只需初始化該硬件採集卡,然後實時讀取採集數據即可,但語音和圖象數據的編解碼以及碼流數據的傳輸都必須由程序去協調執行,決不能在某一件事件上處理過長,必須讓CPU輪流的爲各個事件服務,Windows95下的線程正是滿足這種要求的編程技術。
下面我給出了利用Windows95 環境下的多線程編程技術實現的基於TCP/IP的可視電話的部分源碼,其中包括主窗口過程函數,以及主叫端與被叫端的TCP/IP接收線程函數和語音編解碼的線程函數。由於圖象編解碼的實時性比語音處理與傳輸模塊的實時性的 要求要低些,所以我以語音編解碼爲事件去查詢圖象數據,然後進行圖象編解碼,而沒有爲圖象編解碼去單獨實現一個線程。
在主窗口初始化時, 我用CREATE_SUSPENDED標誌創建了兩個線程hThreadG7231和hThreadTCPRev。一個用於語音編解碼,它的線程函數爲G723Proc, 該線程不斷查詢本地有無編好碼的語音和圖象的碼流,如有,則進行H.223打包,然後通過TCP的端口發送給對方。另外一個線程用於TCP/IP的接收,它的線程函數爲AcceptThreadProcRev,該線程不斷偵 測TCP/IP端口有無對方傳來的碼流,如有,就接收碼流,進行H.223解碼後送入相應的緩衝區。該緩衝區的內容,由語音編解碼線程G723Proc查詢,並送入相應的解碼器。由於使用了多線程的編程技術,使得操作系統定時去服務語 音編解碼模塊和傳輸模塊,從而保證了通信的不中斷。
五、程序源碼:
//基於TCP/IP可視電話主窗口的窗口過程
LONG APIENTRY MainWndProc(HWND hWnd,UINT message,UINT wParam, LONG lParam)
{
static HANDLE hThreadG7231,hThreadTCPListen,hThreadTCPRev;
DWORDThreadIDG7231,ThreadIDTCPListen,ThreadIDTCPRev;
static THREADPACK tp;
static THREADPACK tp1;
unsigned char Buf[80];
CAPSTATUS capStatus;
switch (message)
{
case WM_CREATE:
Init_Wsock(hWnd); //初始化一些數據結構
Init_BS(2,&bs);
vd_tx_pdu.V_S = 0;vd_tx_pdu.N_S = 0;
vd_rx_pdu.V_R = 0;vd_tx_sdu.bytes = 0;
if( dnldProg ( hWnd, "h324g723.exe") )
{
//裝入語音編解碼的DSP核心
MessageBox(hWnd,"Load G.723.1 Kernel Error","Error",MB_OK);
PostQuitMessage(0); }
else
MessageBox(hWnd,"Load G.723.1 Kernel OK!","Indication",MB_OK);
//創建語音編解碼的線程
parag7231.hWnd = hWnd;
hThreadG7231=CreateThread (NULL, 0,(LPTHREAD_START_ROUTINE)G723Proc,
(G7231DATA *)?g7231,
CREATE_SUSPENDED,(LPDWORD)&ThreadIDG7231);
if (!hThreadG7231)
{
wsprintf(Buf, "Error in creating G7231 thread: %d",GetLastError());
MessageBox (hWnd, Buf, "WM_CREATE", MB_OK);}
//創建TCP/IP接收線程
tp1.hWnd = hWnd;
hThreadTCPRev = CreateThread (NULL, 0,(LPTHREAD_START_ROUTINE)AcceptThreadProcRev,
(G7231DATA *)&tp1,CREATE_SUSPENDED,
(LPDWORD)&ThreadIDTCPRev);
if (!hThreadTCPRev)
{
wsprintf(Buf, "Error in creating TCP Receive thread: %d",GetLastError());
MessageBox (hWnd, Buf, "WM_CREATE", MB_OK);}
//開始偵聽網絡
SendMessage(hWnd,WM_COMMAND,IDM_LISTEN,NULL);
break;
case WM_VIDEO_ENCODE: //圖象編碼
if(needencode)EncodeFunction(hWnd);
needencode = SendVideoToBuff(&vd_tx_sdu, buff);
frameMode=TRUE;
capPreview(capWnd,FALSE);
capOverlay(capWnd,FALSE);
capGrabFrameNoStop(capWnd);
break;
case WM_VIDEO_DECODE: //圖象解碼
Video_Decod_begin = 1;
play_movie();
Video_Decod_begin = 0;
break;
case WM_COMMAND:
switch(LOWORD(wParam))
{
case IDM_CONNECT: //響應對方的呼叫,接通可視電話
WskConnect( hWnd );
ResumeThread(hThreadTCPRev); //運行TCP/IP接收線程
ResumeThread(hThreadG7231); //運行語音編解碼線程
BeginG7231Codec(); //初始化圖象採集卡,並開始採集圖象
frameMode = FALSE;
capWnd = capCreateCaptureWindow((LPSTR)"Capture Window",
WS_CHILD WS_VISIBLE,
100, 100, 176,144 ,
(HWND) hWnd, (int) 0);
capSetCallbackOnError(capWnd, (FARPROC)ErrorCallbackProc) ;
capSetCallbackOnStatus(capWnd, (FARPROC)StatusCallbackProc) ;
capSetCallbackOnFrame(capWnd, (FARPROC)FrameCallbackProc) ;
capDriverConnect(capWnd, 0);
CenterCaptureWindow(hWnd, capWnd);
capDlgVideoSource(capWnd);
capDlgVideoFormat(capWnd);
capDlgVideoCompression(capWnd);
capGetStatus(capWnd,&capStatus,sizeof(CAPSTATUS));
StartNewVideoChannel(hWnd, capWnd) ;
image = image_one;
frameMode = TRUE;
capPreview(capWnd,FALSE);
capOverlay(capWnd,FALSE);
capGrabFrameNoStop(capWnd);
break;
case IDM_LISTEN: //撥對方號碼,呼叫對方
sock = socket( AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
MessageBox(hWnd, "socket() failed", "Error", MB_OK);
closesocket(sock);
break;}
if (!FillAddr(hWnd, &local_sin, FALSE )) //獲取TCP/IP地址和端口號
break;
EnableMenuItem(GetMenu( hWnd ), IDM_LISTEN, MF_GRAYED);
SetWindowText( hWnd, "Waiting for connection..");
bind ( sock , (struct sockaddr FAR *)&local_sin,sizeof(local_sin);
if (listen( sock, MAX_PENDING_CONNECTS ) <0)
{
sprintf(szBuff, "%d is the error",
WSAGetLastError()); MessageBox(hWnd, szBuff, "listen(sock) failed",
MB_OK);
break;}
tp.hWnd="hWnd; //開始本地的TCP/IP接收線程"
_beginthread(AcceptThreadProc,0,&tp);
ResumeThread(hThreadG7231); // 開始本地語音編解碼的線程
break;
case IDM_DISCONNECT: //掛斷可視電話
CloseG7231Codec();
SuspendThread(hThreadG7231);
SuspendThread(hThreadTCPRev);
WSACleanup();
Init_Video_Decod_Again();
capSetCallbackOnError(capWnd, NULL);
capSetCallbackOnStatus(capWnd, NULL);
InvalidateRect(hWnd,NULL,1); capSetCallbackOnFrame(capWnd, NULL);
capSetCallbackOnVideoStream(capWnd, NULL);
capDriverDisconnect(capWnd);
Init_Wsock(hWnd);
MessageBox(hWnd, "Now closing the Video telephone","",MB_OK);
SetDisConnectMenus(hWnd);
SendMessage(hWnd, WM_COMMAND,IDM_LISTEN,NULL);
break;
case IDM_EXIT:
CloseG7231Codec();
SendMessage(hWnd, WM_CLOSE, 0, 0l);
break; default:
return (DefWindowProc(hWnd, message, wParam, lParam));
}
break;
case WM_CLOSE:
if (IDOK !="MessageBox(" hWnd, "OK to close window?", gszAppName,
MB_ICONQUESTION MB_OKCANCEL ))break ;
case WM_DESTROY:
WSACleanup();
CloseG7231Codec();
TerminateThread(hThreadG7231,0);
TerminateThread(hThreadTCPRev,0);
capSetCallbackOnError(capWnd, NULL);
capSetCallbackOnStatus(capWnd, NULL);
capSetCallbackOnFrame(capWnd, NULL);
capSetCallbackOnVideoStream(capWnd, NULL);
capDriverDisconnect(capWnd);
FreeAll();
PostQuitMessage(0);
break;
default: /* Passes it on if unproccessed */
return (DefWindowProc(hWnd, message, wParam, lParam));
}
return (0);
}
//主叫方TCP/IP接收線程
DWORD WINAPI AcceptThreadProc( PTHREADPACK ptp )
{
SOCKADDR_IN acc_sin; /* Accept socket address internet style */
int acc_sin_len; /* Accept socket address length */
int status;
acc_sin_len="sizeof(acc_sin);"
//調用阻塞函數accept,一直到遠端響應爲止
sock="accept(" sock,(struct sockaddr FAR *) &acc_sin,(int FAR *) &acc_sin_len );
if (sock < 0)
{
sprintf(szBuff, "%d is the error", WSAGetLastError());
MessageBox(ptp->hWnd, szBuff, "accept(sock) failed", MB_OK);
return (1);
}
SetConnectMenus( ptp->hWnd ); //遠端提機,可視電話接通
BeginG7231Codec();
while (1)
{
beg1:
status = recv((SOCKET)sock, r_mux_buf,MY_MSG_LENGTH, NO_FLAGS_SET );
if (status == SOCKET_ERROR) {
status = WSAGetLastError();
if( status == 10054 ){
MessageBox(ptp->hWnd,"對方掛斷電話","Indication", MB_OK);
SendMessage( ptp->hWnd, WM_COMMAND,IDM_DISCONNECT,NULL);
_endthread();
return (1);
}
goto beg1;
}
if (status) {
r_mux_buf[ status ] = '/0';
if ( r_mux_buf_filled == 1 )
r_mux_buf_overwrite = 1;
else
r_mux_buf_filled = 1;
r_mux_buf_length = status;
}
else
{
MessageBox( hWnd, "Connection broken", "Error", MB_OK);
SendMessage( ptp->hWnd, WM_COMMAND,IDM_DISCONNECT,NULL);
_endthread();
return (2);
}
demux(); //線路碼流H.223解碼
}
return (0);
}
//被叫方TCP/IP接收線程
DWORD WINAPI AcceptThreadProcRev( PTHREADPACK ptp )
{
int status;
while (1)
{
beg2:
status = recv((SOCKET)sock, r_mux_buf,MY_MSG_LENGTH, NO_FLAGS_SET );
if (status == SOCKET_ERROR)
{
status =WSAGetLastError();
if( status == 10054 )
{
MessageBox(ptp->hWnd,"對方掛斷電話","Indication", MB_OK);
SendMessage( ptp->hWnd, WM_COMMAND,IDM_DISCONNECT,NULL);
return (1);
}
goto beg2;
}
if (status)
{
r_mux_buf[ status ] = '/0';
if( r_mux_buf_filled == 1 )
r_mux_buf_overwrite = 1;
else
r_mux_buf_filled = 1;
r_mux_buf_length = status;
}
else
{
MessageBox( hWnd, "Connection broken", "Error", MB_OK);
SendMessage( ptp->hWnd, WM_COMMAND,IDM_DISCONNECT,NULL);
return (2);
}
demux();
} /* while (forever) */
return (0);
}
//語音編解碼線程
DWORD WINAPI G723Proc(G7231DATA *data)
{
int i,len;
Audio_tx_pduad_tx_pdu;
unsigned char mux[MAX_MUX_PDU_SIZE];
do
{
len = 0;
//檢測本地有無語音,圖象碼流要傳輸
i = DetectAudioVideoData();
switch(i)
{
case AUDIO_ONLY: //只有語音碼流
AL2_CRC_coder(&ad_tx_sdu,&ad_tx_pdu);
//H.223打包
len = AL2_To_MUX(&ad_tx_pdu, mux);
break;
case VIDEO_ONLY: //只有圖象碼流
SDU_To_PDU(&vd_tx_sdu,&vd_tx_pdu);
tx_AL3_I_PDU(&vd_tx_pdu ,&bs , 1); //H.223打包
len = AL3_To_MUX(&vd_tx_pdu,mux);
break;
case AUDIO_VIDEO: //語音和圖象碼流
AL2_CRC_coder(&ad_tx_sdu,&ad_tx_pdu);
SDU_To_PDU(&vd_tx_sdu,&vd_tx_pdu);
tx_AL3_I_PDU(&vd_tx_pdu ,&bs , 1);
//H.223打包
len = AL2_AL3_To_MUX(&ad_tx_pdu,&vd_tx_pdu,mux);
break;
case NO_AUDIO_VIDEO: //此刻無碼流要傳輸
break;
}
//TCP/IP發送碼流
if(len != 0)
send((SOCKET)sock,mux,len,0);
//是否接收到待解碼的碼流,有就調用解碼器
PutVideoStreamToDecod();
}
while(1);
return (0);
}