孫鑫VC學習筆記:多線程編程

孫鑫VC學習筆記:多線程編程

孫鑫VC學習筆記:多線程編程

SkySeraph Dec 11st 2010  HQU

Email:[email protected]    QQ:452728574

Latest Modified Date:Dec.11st 2010 HQU

=================================================================================

程序&進程&線程

=================================================================================

  • 程序 & 進程

程序

計算機指令的集合,它以文件的形式存儲在磁盤上

進程

通常被定義爲一個正在運行的程序的實例,是一個程序在其自身的地址空間中的一次執行活動

區別:進程是資源申請、調度和獨立運行的單位,因此,它使用系統中的運行資源;而程序不能申請系統資源,不能被系統調度,也不能作爲獨立的運行的單位, 因此,他不佔用系統的運行資源。

  • 進程由兩個部分組成:

 1、操作系統用來管理進程的內核對象。內核對象是操作系統內部分配的一個內存塊,內核對象也是系統用來存放關於進程的統計信息的地方。

 2、地址空間。它包含所有可執行模塊或DLL模塊的代碼和數據。他還包含動態內存分配的空間。如線程堆棧和堆分配空間。

內核對象:是操作系統內部分配的一個內存塊,它是一種只能被內核訪問的數據結構, 其成員負責維護該對象的各種信息,應用程序無法找到並直接改變它們的內容,只能通過Windows提供的函數對內核對象進行操作。

  • 進程

進程是不活潑的。進程從來不執行任何東西,它只是線程的容器。

若要使進程完成某項操作,它必須擁有一個在它的環境中運行的線程,此線程負責執行包含在進程的地址空間中的代碼。

單個進程可能包含若干個線程,這些線程都“同時”執行進程地址空間中的代碼。

每個進程至少擁有一個線程,來執行進程的地址空間中的代碼。

當創建一個進程時,操作系統會自動創建這個進程的一個線程,稱爲主線程。此後,該線程可以創建其他的線程

  • 線程

線程有兩個部分組成:

 1。線程的內核對象,操作系統用它來對線程實施管理,內核對象也是系統用來存放線程統計信息的地方。

 2。線程堆棧,它用於維護線程在執行代碼時需要的所有參數和局部變量。

當創建線程時,系統創建一個線程內核對象。

該線程內核對象不是線程本身,而是操作系統用來管理線程的較小的數據結構。

可以將線程內核對象視爲由關於線程的統計信息組成的一個小型數據結構。

線程總是在某個進程環境中創建。

系統從進程的地址空間中分配內存,供線程的堆棧使用。

新線程運行的進程環境與創建線程的環境相同。

因此,新線程可以訪問進程的內核對象的所有句柄、進程中的所有內存和在這個相同的進程中的所有其他線程的堆棧。這使得單個進程中的多個線程確實能夠非常容易的互相通信。

線程只有一個內核對象和一個堆棧,保留的記錄很少,因此所需要的內存也很少。

因爲線程需要的開銷比進程少,因此在編程中經常採用多線程來解決編程問題,而儘量避免創建新的進程。

  • 線程運行

對於單個CPU

操作系統爲每一個運行線程安排一定的CPU時間——時間片。

系統通過一種循環的方式爲線程提供時間片,線程在自己的時間內運行,因時間片相當短,因此,給用戶的感覺,就好像線程是同時進行的一樣。

如果計算機擁有多個CPU,線程就能真正意義上運行了

  • 注意

 我們可以用多進程代替多線程,但是這樣不是明智的,因爲

 1.每新建一個進程,系統要爲之分配4GB的虛擬內存,浪費資源;而多線程共享同一個地址空間,佔用資源較少

 2.在進程之間發生切換時,要交換整個地址空間;而線程之間的切換隻是執行環境的改變, 效率較高。 

=================================================================================

線程的創建

=================================================================================

  • 實例着手

=================================================================================

  • 實例1

#include "windows.h"

#include "iostream"

using namespace std;

DWORD WINAPI Fun1Proc(LPVOID lpParameter);//聲明線程入口函數

void main()

{

//創建新線程

HANDLE hThread1;

hThread1 = CreateThread(

NULL,//使用缺省的安全性

0,//初始提交的棧的大小

Fun1Proc,//線程入口函數

NULL,//傳遞爲線程的參數

0,//附加標記  0表示線程創建後立即運行

NULL);//線程ID

//關閉線程,但不會終止新建的線程

CloseHandle(hThread1);

cout<<"main thread is running"<<endl;

Sleep(1000);//暫停主線程

/*說明:如果不添加Sleep語句,主線程會在自己的時間片中運行完成後(該時間片在main函數,也就是主線程全部執行完畢後還有時間剩餘),選擇直接退出,主線程都退出了,依附於主線程的新線程也就不會有機會得到執行了,只有讓主線程暫停執行(採用sleep函數),即掛起,讓出執行的權利,操作系統會從等待的線程中選擇一個來運行,那麼新創建的線程得到機會執行*/

}

 

DWORD WINAPI Fun1Proc(LPVOID lpParameter)

{

cout<<"thread1 is running!"<<endl;

return 0;

}

結果:孫鑫給的是main thread is running 換行 thread1 is running!

  我的結果:在VC6.0下,一通亂碼;在VS2008下,沒出現換行

分析:估計原因出自我的本本是雙核的,而VC6.0的亂碼是因爲裝了插件緣故,不知是否是這樣?

=================================================================================

  • 實例2

添加全局變量

int index=0;

將main函數中輸出語句修改爲:

while(index++<50)

cout<<"main thread is running"<<endl;

將線程中輸出語句修改爲:

while(index++<50)

cout<<"thread1 is running!"<<endl;

將main函數中sleep語句省去

  • 說明:

主線程和副線程在交替運行,也就是主線程在它的時間片運行結束後,副線程得到執行的權利,在它自己所對應的時間片中運行,此時主線程其實還沒有運行結束,它將等待着副線程運行結束後繼續執行

=================================================================================

  • 步驟說明                          【思路】:線程創建

=================================================================================

  • 一、創建一個線程

創建線程使用CreateThread:The CreateThread function creates a thread to execute within the address space of the calling process.

HANDLE CreateThread(

LPSECURITY_ATTRIBUTES lpThreadAttributes,   //結構體指針

DWORD dwStackSize,  //指定初始提交棧的大小 

LPTHREAD_START_ROUTINE lpStartAddress, //由線程執行,表示線程的起始地址,指定線程入口函數,

LPVOID lpParameter,  //指定一個單獨的值傳遞給線程

DWORD dwCreationFlags, //指定控件線程創建的附加標記

LPDWORD lpThreadId );   //指向一個用來接收線程的標識符變量

參數1

指向SECURITY_ATTRIBUTES結構體的指針。這裏可以設置爲NULL,使用默認的安全性

參數2

指定初始提交的棧的大小,即線程可以將多少地址空間用於自己的棧,以字節爲單位。系統會將這個值四捨五入爲最近的頁面

如果該值是0或者小於缺省提交大小,則使用和調用線程一樣的大小。

 頁 面

系統管理內存時使用的內存單位,不同的CPU其頁面大小也是不同的。

X86 使用的頁面大小是4KB。當保留地址空間的一個區域時,系統要確保該區域的大小是 系統的頁面大小的倍數  

參數3

指向LPTHREAD_START_ROUTINE(應用程序定義的函數類型)的指針。這個函數將被線程執行,表示了線程的起始地址,指定線程入口函數,該入口函數的參數類型以及返回類型要與ThreadProc()函數聲明的類型要保持一致。

參數4

指定傳遞給線程的單獨的參數的值。

參數5

指定控制線程創建的附加標記。

如果CREATE_SUSPENDED標記被指定,線程創建後處於暫停 狀態不會運行,直到調用了ResumeThread函數。    

如果該值是0,線程在創建之後立即運行。

參數6

[out]指向一個變量用來接收線程的標識符。創建一個線程時,系統會爲線程分配一個ID號。

 Windows NT/2000:如果這個參數是NULL,線程的標識符不會返回。

 Windows 95/98  :這個參數不能是NULL 

如果線程創建成功,此函數返回線程的句柄。

=================================================================================

  • 二、編寫線程函數

可參考ThreadProc: DWORD WINAPI ThreadProc(LPVOID lpParameter);

=================================================================================

  • 三、關閉線程句柄

在主線程中創建完一個新線程之後,一般會調用CloseHandle()方法來關閉新創建的線程的句柄。

 BOOL CloseHandle(HANDLE hObject);

注意:關閉句柄並沒有終止新創建的線程,新建的線程繼續在運行。

至於爲什麼要關閉線程句柄,主要有兩個原因:

 1.在本主線程中,這個句柄已經沒什麼用了。

 2.當關閉線程句柄時和創建的線程執行完畢之後,系統會遞減新線程的內核對象使用計數,當使用計數爲0時,系統就會釋放線程內核對象;

  如果在主線程中沒有關閉這個句柄,那麼始終會保留這個引用,這樣線程的內核對象的使用計數即使在創建的線程執行完畢之後也不會降爲0,

  因此線程的內核對象無法釋放,直到進程終止時系統纔會清理這些殘留的對象。

所以應該在不再使用線程的句柄的時候將其關閉掉,讓線程的線程內核對象的引用計數減1。

=================================================================================

  • 四、暫停線程的執行

當線程暫停執行的時候,也就是表示它放棄了執行的權力。

操作系統會從等待運行的線程隊列中選擇一個線程來運行。新創建的線程就可以得到運行的機會。

可以使用函數Sleep:

 void Sleep(

  DWORD dwMilliseconds //sleep time 以毫秒爲單位

 );

暫停當前線程指定時間間隔的執行。

=================================================================================

互斥

=================================================================================

  • 實例着手

=================================================================================

  • 實例1(車票銷售)

#include "iostream"

using namespace std;

#include "windows.h"

 

DWORD WINAPI ThreadProc1(LPVOID lpParameter);

DWORD WINAPI ThreadProc2(LPVOID lpParameter);

 

int ticket=50;

 

void main()

{

      HANDLE handle1=CreateThread(NULL,0,ThreadProc1,NULL,0,NULL);

      HANDLE handle2=CreateThread(NULL,0,ThreadProc2,NULL,0,NULL);

      CloseHandle(handle1);

      CloseHandle(handle2);

  /*說明:爲了使得主線程在退出之前保證副進程的執行完成,有些實現方法是採用恆真的空循環,單此種方法主線程會佔用cpu的運行時間,如果採用Sleep,則主線程完全不佔用cpu的任何運行時間*/

  Sleep(4000);

  //getchar();//VS2008

}

 

DWORD WINAPI ThreadProc1(LPVOID lpParameter){

      //說明:在線程的時間片內持續運行

      while(TRUE)

      {

           if(ticket>0)

   cout<<"thread1 sale the ticket id is:"<<ticket--<<endl;

           else

                 break;

      }

      return 0;

}

DWORD WINAPI ThreadProc2(LPVOID lpParameter)

{

      while(TRUE)

      {

           if(ticket>0)

            cout<<"thread2 sale the ticket id is:"<<ticket--<<endl;

           else

                 break;

      }

      return 0;

}

  • 結果:

<1> 孫鑫給的結果是兩個線程輪流執行操作,輸出結果如下,他的機子是單核的

thread1 sale the ticket id is:50

thread2 sale the ticket id is:49

。。。

<2> 我在兩個線程的if語句中加入Sleep(1000);,這樣能清楚的看到雙核下線程的運行狀況

thread1 sale the ticket id is:thread2 sale the ticket id is:5049  【雙核下】【問題:同時運行,但是輸出都是最後一起輸出】

。。。

=================================================================================

  • 說明

=================================================================================

  • 問題:上述例1,有可能會遇到如下一種情況:【單核】

       當ticket數量運行到1時,線程1正在運行,此時線程1運行到輸出語句時,它的時間片已經結束,則線程1對ticket id的減減動作沒有完成,此時線程2開始執行,發現數量是1,則執行減減動作,使得數量爲0,返回,線程1繼續執行,此時票的數量已經是0了,線程1繼續執行輸出語句,對票的數量執行減減,則數量變爲-1,這是不允許的。這是由於搶佔全局的資源所引起的。

       解決這個問題的辦法是實現線程間的“同步”,即一個線程在對一個全局的資源進行操作的過程中,是不允許其他線程對全局的資源進行訪問,直到該線程對資源操作完畢後。

  • 涉及概念:互斥對象
  • ① 互斥對象(mutex)

屬於內核對象,它能夠確保線程擁有對單個資源的互斥訪問權。

互斥對象包含一個使用數量,一個線程ID和一個計數器。

ID用於標識系統中的哪個線程當前擁有互斥對象,計數器用於指明該線程擁有互斥對象的次數。

  • ② 涉及到三個函數:

[1].CreateMutex:創建互斥對象,返回互斥對象的句柄

 HANDLE CreateMutex(

 LPSECURITY_ATTRIBUTES lpMutexAttributes,//

 BOOL bInitialOwner,  // flag for initial ownership,

 LPCTSTR lpName     // pointer to mutex-object name

 );

參數1

指向SECURITY_ATTRIBUTES結構體的指針。可以傳遞NULL,讓其使用默認的安全性。

參數2

指示互斥對象的初始擁有者。 如果該值是真,調用者創建互斥對象,調用的線程獲得互斥對象的所有權。 否則,調用線程捕獲的互斥對象的所有權。(就是說,如果該參數爲真,則調用該函數的線程擁有互斥對象的所有權。否則,不擁有所有權,當前互斥對象處於空閒狀態,其他線程可以佔用)

參數3

互斥對象名稱。傳遞NULL創建的就是沒有名字的互斥對象,即匿名的互斥對象。

返回

創建成功之後 ,返回一個互斥對象句柄。如果一個命名的互斥對象在本函數調用之前已經存在,則返回已經存在的對象句柄。然後可以調用GetLastError檢查其返回值是否爲ERROR_ALREADY_EXISTS,TRUE則表示命名互斥對象已經存在,否則表示互斥對象是新創建的。

 

 當前沒有線程擁有互斥對象,操作系統會將互斥對象設置爲已通知狀態(有信號狀態)

[2].WaitForSingleObject:等待互斥對象的使用權,如果第二個參數設置爲INFINITE,則表示會持續等待下去,直到擁有所有權,纔有權執行該函數下面的語句。一旦擁有了所有權,則會將互斥對象的的線程ID設置爲當前使用的線程ID值。

[3].ReleaseMutex:將互斥對象所有權進行釋放,交還給系統。

  • ③  理解:可以將互斥對象想象成一把鑰匙,CreateMutex創建了這把鑰匙,WaitForSingleObject等待這把鑰匙去訪問一個公共的資源,比如一個房間,如果擁有了鑰匙,則這個房間的所有權就屬於這個進程了,別人是進不去這個房間的,直到進程將這個房間的鑰匙歸還掉,即ReleaseMutex。
  • ④ 調用的形式                 【思路】:互斥條件實現線程同步

 //在主線程中

 ...

 HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);

 ...

 //其他線程中

 ...

 WaitForSingleObject(hMutex, INFINITE);

 //受保護的代碼

 ...

 ReleaseMutex(hMutex);

  • 解決方法:增加互斥條件,實現線程之間的同步  代碼如下例2

=================================================================================

  • 實例2(車票銷售) 增加互斥條件    

#include "iostream"

using namespace std;

#include "windows.h"

 

DWORD WINAPI ThreadProc1(LPVOID lpParameter);

DWORD WINAPI ThreadProc2(LPVOID lpParameter);

 

int ticket=50;

HANDLE hMutex;

 

void main()

{

      HANDLE handle1=CreateThread(NULL,0,ThreadProc1,NULL,0,NULL);

      HANDLE handle2=CreateThread(NULL,0,ThreadProc2,NULL,0,NULL);

      CloseHandle(handle1);

      CloseHandle(handle2);

  //說明:爲了使得主線程在退出之前保證副進程的執行完成,有些實現方法是採用恆真的空循環,單此種方法主線程會佔用cpu的運行時間,如果採用Sleep,則主線程完全不佔用cpu的任何運行時間

  hMutex=CreateMutex(NULL,FALSE,NULL); //第二個參數爲FALSE,將互斥對象聲明爲空閒狀態

  Sleep(4000);

}

 

DWORD WINAPI ThreadProc1(LPVOID lpParameter){

      //說明:在線程的時間片內持續運行

      while(TRUE)

      {

  WaitForSingleObject(hMutex,INFINITE); //第二個參數爲INFINITE表示一直等待,直到擁有互斥對象

  if(ticket>0)

   {

   Sleep(1);

   cout<<"thread1 sale the ticket id is:"<<ticket--<<endl;

   }

           else

                 break;

  ReleaseMutex(hMutex); //使用完了,將互斥對象還給操作系統

      }

      return 0;

}

 

DWORD WINAPI ThreadProc2(LPVOID lpParameter)

{

      while(TRUE)

      {

  WaitForSingleObject(hMutex,INFINITE); //第二個參數爲INFINITE表示一直等待,直到擁有互斥對象         

  if(ticket>0)

   {

   Sleep(1);

   cout<<"thread2 sale the ticket id is:"<<ticket--<<endl;

   }

           else

                 break;

  ReleaseMutex(hMutex); //使用完了,將互斥對象還給操作系統

      }

      return 0;

}

=================================================================================

  • 思考

=================================================================================

  • ① 如果將WaitForSingleObject和ReleaseMutex放置在while循環的外部,發現程序的輸出結果是隻有線程1在銷售票,線程2沒有銷售一張票,

這是因爲線程1在得到互斥對象的所有權後,進入到了循環,而釋放互斥權的調用必須等到循環執行結束,即使線程1在時間片完成後,將執行權交給了線程2,單線程2發現互斥對象的所有權還是被佔用着,所以沒有做任何動作,線程1繼續執行,直到將票銷售完,退出循環,釋放互斥對象的所有權,此時線程2在得到所有權後發現票已經銷售一空,也就退出了。

在上述代碼中,爲什麼輸出結果正確呢,也就是線程1和2交替售票,這是因爲線程1得到互斥對象的控制權後執行單張票的銷售動作,動作完成就立即釋放了控制權。在線程1的執行時間片完成後,線程2就開始執行了,線程2執行和線程1相同的動作。

兩段代碼最主要的區別就是前者在銷售所有票的過程中都獨佔着互斥對象資源,而後者是銷售完一張票後就將互斥對象資源釋放掉了。

  • ② 在main函數中將CreateMutex 的第二個參數修改爲TRUE,則表示主線程在創建互斥對象的時候就擁有了所有權,執行代碼,發現線程1和2都沒有執行,只是因爲主線程沒有釋放掉互斥對象。考慮在線程1和2的WaitForSingleObject前添加ReleaseMutex可否?也就是我在申請前將別人佔用的互斥對象釋放掉,這顯然是不行的,不然就失去互斥的真正意義了,如果我自己想用,就將別人踢掉,就失去了規則了。那內部是怎麼實現的呢?在CreateMutex或WaitForSingleObject申請到互斥對象的所有權後,會將互斥對象中的線程ID設置爲調用線程的ID,ReleaseMutex時會比較當前的線程ID和互斥對象中的ID是否一致,如果不一致,則禁止釋放。所以要想線程1和2能正常執行,則必須在CreateMutex後調用ReleaseMutex。
  • ③ 在main函數中CreateMutex的第二個參數修改爲TRUE後,互斥對象的所有權由主線程所有,然後調用WaitForSingleObject,發現申請依然成功,這是因爲擁有所有權的線程是自身線程,單互斥對象的內部發生了變化,它內部的計數器設置成了2,也就是如果希望線程1和2能執行,則需要兩次調用ReleaseMutex。ReleaseMutex函數的意義就是將計數器遞減。

 進一步:互斥對象包含一個計數器,用來記錄互斥對象請求的次數, 所以在同一線程中請求了多少次就要釋放多少次;

 如 hMutex=CreateMutex(NULL,TRUE,NULL);  //當第二個參數設置爲TRUE時,互斥對象計數器設爲1

 WaitForSingleObject(hMutex,INFINITE);  //因爲請求的互斥對象線程ID與擁有互斥對象線程ID相同,可以再次請求成功,計數器加1

 ReleaseMutex(hMutex);  //第一次釋放,計數器減1,但仍有信號

 ReleaseMutex(hMutex);  //再一次釋放,計數器爲零

  • ④ 如果在線程中調用WaitForSingleObject後沒有調用ReleaseMutex,則該線程執行終止後(不是單次時間片完成後),操作系統會自動釋放掉互斥對象

即 如果操作系統發現線程已經正常終止,會自動把線程申請的互斥對象ID設爲0,同時也把計數器清零,其他對象可以申請互斥對象。

    可以根據WaitForSingleObject的返回值判斷該線程是如何得到互斥對象擁有權的;如果返回值是WAIT_OBJECT_0,表示由於互斥對象處於有信號狀態才獲得所有權的;如果返回值是WAIT_ABANDONED,則表示先前擁有互斥對象的線程異常終止 或者終止之前沒有調用 ReleaseMutex釋放對象,此時就要警惕了訪問資源有破壞資源的危險

  • ⑤ 讓程序單位時間內只能運行一個實例,也就是如果實例存在,則不打開新的實例:

由CreateMutex在MSDN中的介紹可以知道,只需要將該函數的第三個參數,Mutex對象的名字不設置成NULL,即給它取個名字,然後代碼修改如下:

hMutex=CreateMutex(NULL,FALSE,”instance”); //Mutex的名稱可以任意的取

      if(hMutex)

      {

           if(ERROR_ALREADY_EXISTS==GetLastError())

           {

                 cout<<"the application instance is exit!"<<endl;

                 return;

           }

      }

 

=================================================================================

實例:創建多線程聊天程序  

=================================================================================

  • 1.創建一個基於對話框的MFC程序,界面如下:

 

 

 

 

  • 2.添加套接字庫頭文件:

調用MFC的內置函數:AfxSocketInit,該函數其實也是調用Win32中的WSAStartup,並且是調用1.1的套接字庫版本,該函數能確保程序終止前調用WSACleanup的調用,該函數的放置位置最好在CWinApp中的InitInstance中,注意包含頭文件Afxsock.h,在StdAfx.h這個頭文件中進行包含。

StdAfx.h頭文件是一個預編譯頭文件,在該文件中包含了MFC程序運行的一些必要的頭文件,如afxwin.h這樣的MFC核心頭文件等。它是第一個被程序加載的文件。

  • 3.加載套接字庫:

在CWinApp中的InitInstance添加如下代碼:

if(FALSE==AfxSocketInit())

{   

 AfxMessageBox("套接字庫加載失敗!");

  return FALSE;

}

  • 4. 創建並初始化套接字,將自己假想成服務器端,進行套接字和地址結構的綁定,等待別人發送消息過來。

在CDialog中  添加私有成員變量:SOCKET m_socket

添加成員函數:

BOOL CChatDlg::InitSocket()

{

      m_socket=socket(AF_INET,SOCK_DGRAM,0); //UDP連接方式

      if(INVALID_SOCKET==m_socket)

      {

           MessageBox("套接字創建失敗!");

           return FALSE;

      }

      SOCKADDR_IN addrServer; //將自己假想成server

      addrServer.sin_addr.S_un.S_addr=htonl(INADDR_ANY);

      addrServer.sin_family=AF_INET;

      addrServer.sin_port=htons(1234);

      int retVal;

      retVal=bind(m_socket,(SOCKADDR*)&addrServer,sizeof(SOCKADDR));

      if(SOCKET_ERROR==retVal)

      {

           closesocket(m_socket);

           MessageBox("套接字綁定失敗!");

           return FALSE;

      }

      return TRUE;

}

  • 5.在CChatDlg類的外部添加結構體:

struct RECVPARAM

{

說明:因爲套接字本身只涉及傳輸的協議類型,是UDP還是TCP,而和是服務器端還是客戶端沒有必然的關係,所以本程序即涉及到服務器端又涉及到客戶端,採用同一個套接字是被允許的。

            SOCKET sock; //保存最初創建的套接字

            HWND hWnd; //保存對話框的窗口句柄

};

  • 6.在對話框的初始化代碼中完成線程的創建:

在CChatDlg::OnInitDialog函數中添加下面的代碼:

if(!InitSocket()) //服務器端的創建

           return FALSE;

      RECVPARAM *pRecvParam=new RECVPARAM;

      pRecvParam->hWnd=m_hWnd;

      pRecvParam->sock=m_socket;

  • 說明:

1.接收部分應該一直處於響應狀態,如果和發送部分放在同一段代碼中,勢必會阻塞掉髮送功能的實現,所以考慮將接收放在單獨的線程中,使它在一個while循環中,始終處於響應狀態

2.因爲需要傳遞兩個參數進去,一個是recvfrom需要用的套接字,另一個是當收到數據後需要將數據顯示在窗口中的對應文本框控件上,所以需要傳遞當前窗口的句柄,但CreateThread方法只能傳遞一個參數,即第四個參數,這時候就想到了採用結構體的方式傳遞。

HANDLE hThread=CreateThread(NULL,0,RecvProc,(LPVOID)pRecvParam,0,NULL);

CloseHandle(hThread);

  • 7.創建線程入口函數RecvProc:

可模仿ThreadProc的創建方式(在MSDN中有原型),但遇到一個問題,將該函數申明爲CChatDlg的成員函數嘛?答案不是的,因爲如果是成員函數的話,那它屬於某個具體的對象,那麼在調用它的時候勢必要讓程序創建一個對象,但該對象的構造函數有參數的話,系統就不知所措了,所以可以將函數創建爲全局函數,即不屬於類,但這失去了類的封裝性,最好的方法是將該方法聲明爲靜態方法,它不屬於任何一個對象。

在CChatDlg類的頭文件中添加:

static DWORD WINAPI RecvProc(LPVOID lpParameter);

在cpp文件中添加:

DWORD WINAPI CChatDlg::RecvProc(LPVOID lpParameter)

{

      RECVPARAM* pRecvParam=(RECVPARAM*)lpParameter;

      HWND hWnd=pRecvParam->hWnd;

      SOCKET sock=pRecvParam->sock;

      char recvBuf[200];

      char resultBuf[200];

      SOCKADDR_IN addrFrom; //這個時候是假想成服務器端

      int len=sizeof(SOCKADDR_IN);

      while(TRUE) //處於持續響應狀態

      {

           int retVal=recvfrom(sock,recvBuf,200,0,(SOCKADDR*)&addrFrom,&len); //從客戶端接收數據,並將客戶端的地址結構體填充

           if(SOCKET_ERROR == retVal)

           {

                 AfxMessageBox("接收數據出錯"); //因爲本函數是靜態函數,所以只能調用全局的消息了

                 break;

           }

           else

           {

sprintf(resultBuf,"%s said:%s",inet_ntoa(addFrom.sin_addr),recvBuf);

//現在已經拿到客戶端送過來的消息了,但因爲自身是靜態函數,所以拿不到當前窗口對象中的控件的句柄,也就不能對其賦值了,唯一辦法就是用消息的形式將接收到的值拋出到窗口的消息隊列中,等待消息處理

 

                 ::PostMessage(hWnd,WM_RECVDATA,0,(LPARAM)resultBuf);                }

 

      }

 

      return 0;

 

}

 

  • 8.自定義消息:

定義自定義消息的宏:

#define WM_RECVDATA WM_USER+1

聲明消息響應函數:因爲有參數要傳遞,所以wParam和lParam都要寫,如果沒有參數需要傳遞,可以不寫

afx_msg void OnRecvData(WPARAM wParam,LPARAM lParam);

消息映射:

ON_MESSAGE(WM_RECVDATA,OnRecvData)

定義消息響應函數:

void CChatDlg::OnRecvData(WPARAM wParam,LPARAM lParam)

{

           //注意將文本框的屬性設置成多行

      CString recvData=(char*)lParam;

      CString temp; //文本框中現有的內容

      GetDlgItemText(IDC_EDIT_RECV,temp);

      temp+="\r\n";

      temp+=recvData;

      SetDlgItemText(IDC_EDIT_RECV,temp);

}

自此,消息的接收和顯示部分已經完成了

 

  • 9.消息的發送:

在發送按鈕點擊的響應函數中添加:

       DWORD dword;

      CIPAddressCtrl* pIPAddr=(CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1);

      pIPAddr->GetAddress(dword);

      //因爲對方有具體的IP地址值,我們假想對方是服務器端。在發送的時候程序就從服務器的角色轉變爲客戶端了

      SOCKADDR_IN addrServer;

      addrServer.sin_addr.S_un.S_addr=htonl(dword);

      addrServer.sin_family=AF_INET;

      addrServer.sin_port=htons(1234);

      CString strSend;

      GetDlgItemText(IDC_EDIT_SEND,strSend);

      sendto(m_socket,strSend,strlen(strSend)+1,0,(SOCKADDR*)&addrServer,sizeof(SOCKADDR));

              SetDlgItemText(IDC_EDIT_SEND,"");

  • 幾點思考:

1.本程序的核心在於將消息的發送的和接收發在了兩個不同的線程中,接收放在新創建的副進程中,因爲其要一直處於響應狀態,也就是需要一個while循環;發送放在主線程中。這樣消息的接收和發送就不存在先後順序了,且一直處於循環中的接收也不會影響到發送。

2.上述代碼中的新線程入口函數中可能沒有必要傳遞兩個參數進去,其中SOCKET參數可以在入口函數內部創建,反正SOCKET變量也就是聲明是TCP還是UDP,和發送或接收沒有必然的聯繫,如果這樣的話,就沒有必要聲明第五步中的結構體了,CreateThread方法也剛好傳遞一個參數,即當前窗口的句柄

 

Author:         SKySeraph

Email/GTalk: [email protected]    QQ:452728574

From:         http://www.cnblogs.com/skyseraph/

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,請尊重作者的勞動成果。


作者:skyseraph 
出處:http://www.cnblogs.com/skyseraph/ 
Email/GTalk: [email protected] QQ:452728574 
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。

發佈了11 篇原創文章 · 獲贊 18 · 訪問量 20萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章