本文首先討論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);
}