多线程程序的编写,多线程应用中容易出现的问题。互斥对象的讲解,如何采用互斥对象来实现多线程的同步。如何利用命名互斥对象保证应用程序只有一个实例运行。应用多线程编写网络聊天室程序。 1.基本概念 进程是资源申请、调度和独立运行的单位,因此,它使用系统中的运行资源;而程序不能申请系统资源,不能被系统调度,也不能作为独立运行的单位,因此,它不占用系统的运行资源。 进程由两个部分组成: 1、操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方。 2、地址空间。它包含所有可执行模块或DLL模块的代码和数据。它还包含动态内存分配的空间。 如线程堆栈和堆分配空间。 进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程的地址空间中的代码。 单个进程可能包含若干个线程,这些线程都“同时” 执行进程地址空间中的代码。 每个进程至少拥有一个线程,来执行进程的地址空间中的代码。当创建一个进程时,操作系统会自动创建这个进程的第一个线程,称为主线程。此后,该线程可以创建其他的线程。 进程地址空间 系统赋予每个进程独立的虚拟地址空间。对于32位进程来说,这个地址空间是4GB。 每个进程有它自己的私有地址空间。进程A可能有一个存放在它的地址空间中的数据结构,地址是0x12345678,而进程B则有一个完全不同的数据结构存放在它的地址空间中,地址是0x12345678。当进程A中运行的线程访问地址为0x12345678的内存时,这些线程访问的是进程A的数据结构。当进程B中运行的线程访问地址为0x12345678的内存时,这些线程访问的是进程B的数据结构。进程A中运行的线程不能访问进程B的地址空间中的数据结构,反之亦然。 4GB是虚拟的地址空间,只是内存地址的一个范围。在你能成功地访问数据而不会出现非法访问之前,必须赋予物理存储器,或者将物理存储器映射到各个部分的地址空间。&&&? 4GB虚拟地址空间中,2GB是内核方式分区,供内核代码、设备驱动程序、设备I/O高速缓冲、非页面内存池的分配和进程页面表等使用,而用户方式分区使用的地址空间约为2GB,这个分区是进程的私有地址空间所在的地方。一个进程不能读取、写入、或者以任何方式访问驻留在该分区中的另一个进程的数据。对于所有应用程序来说,该分区是维护进程的大部分数据的地方。 线程 &&&& 线程由两个部分组成: 1、线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。 2、线程堆栈,它用于维护线程在执行代码时需要的所有参数和局部变量。 当创建线程时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。可以将线程内核对象视为由关于线程的统计信息组成的一个小型数据结构。 线程总是在某个进程环境中创建。系统从进程的地址空间中分配内存,供线程的堆栈使用。新线程运行的进程环境与创建线程的环境相同。因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中的所有其他线程的堆栈。这使得单个进程中的多个线程确实能够非常容易地互相通信。 线程只有一个内核对象和一个堆栈,保留的记录很少,因此所需要的内存也很少。 因为线程需要的开销比进程少,因此在编程中经常采用多线程来解决编程问题,而尽量避免创建新的进程。 线程运行 &&& 操作系统为每一个运行线程安排一定的CPU时间 —— 时间片。系统通过一种循环的方式为线程提供时间片,线程在自己的时间内运行,因时间片相当短,因此,给用户的感觉,就好像线程是同时运行的一样。 如果计算机拥有多个CPU,线程就能真正意义上同时运行了。 2.新建一个win32 console application工程MultiThread 添加windows.h头文件使用API函数,iostream头文件。 The CreateThread function creates a thread to execute within the address space of the calling process. HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to security attributes DWORD dwStackSize, // initial thread stack size——线程堆栈的大小 LPTHREAD_START_ROUTINE lpStartAddress, // pointer to thread function LPVOID lpParameter, // argument for new thread DWORD dwCreationFlags, // creation flags //CREATE_SUSPENDED直到调用ResumeThread开始run;0则马上开始 LPDWORD lpThreadId // pointer to receive thread ID ); //返回新线程的句柄 DWORD WINAPI ThreadProc( LPVOID lpParameter // thread data ); //线程函数 编写如下: DWORD WINAPI Fun1Proc( LPVOID lpParameter // thread data ); void main() //主线程 { HANDLE hThread1; hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL); CloseHandle(hThread1); //关闭线程,说明main中对此线程无兴趣。 cout<<"main thread!"<<endl; } DWORD WINAPI Fun1Proc( LPVOID lpParameter // thread data ) { cout<<"thread1 is running!"<<endl; return 0; } 但这样只出现了main thread!,因为main是主线程,执行完了就退出了,这样进程也退出了(即新线程还没执行就退出了)。所以这里不要让主线程退出,这里让主线程等待,则系统把时间片分给等待中的线程执行。 The Sleep function suspends the execution of the current thread for a specified interval. //暂停 VOID Sleep( DWORD dwMilliseconds // sleep time in milliseconds毫秒 ); 这里可在main函数最后加一个Sleep(100); 注释掉sleep,在main函数前加个int index=0,在cout前加个while(index++<1000)……。这样,在mainthread和thread1交替执行。(貌似孙鑫那个是先thread1后是thread2) 出售火车票代码: main函数中多加一个线程2,然后Sleep(4000); main前即一个int tickets=100; 两个线程函数中分别改为: while(TRUE) { if(tickets>0) cout<<"thread1 sells tickets:"<<tickets--<<endl; else break; } //thread2改为thread2 sells 结果交替输出thread1和thread2 sells,但孙鑫那个是先thread1一段时间,再thread2 sells。 特殊情况:线程从哪暂停就从哪儿继续。若线程1暂停在if语句之后,然后线程2执行,使tickets为0了,而线程1又继续执行,则输出tickets为0了,就出错了。 产生错误的原因是两个线程使用同一个资源,我们需要使线程同步来避免错误。使大家看到这个错误:在两个线程的if语句里加一个Sleep(1);//我用sleep 3ms才出现了。 线程同步:创建互斥对象。 The CreateMutex function creates a named or unnamed mutex object. HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, // pointer to security attributes BOOL bInitialOwner, // flag for initial ownership //true的话,调用此对象的线程对它有ownership LPCTSTR lpName // pointer to mutex-object name ); 互斥对象 互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。 互斥对象包含一个使用数量,一个线程ID和一个计数器。 ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。 在main前加一个HANDLE hMutex;在main中CloseHandle之后创建一个互斥对象: hMutex=CreateMutex(NULL,FALSE,NULL); The WaitForSingleObject function returns when one of the following occurs: The specified object is in the signaled state. //对象有信号状态 The time-out interval elapses. //超时的时间间隔流逝了 DWORD WaitForSingleObject( HANDLE hHandle, // handle to object to wait for DWORD dwMilliseconds // time-out interval in milliseconds ); If dwMilliseconds is INFINITE, the function's time-out interval never elapses 在线程函数前面添加: WaitForSingleObject(hMutex,INFINITE); //开始的时候,hMutex有信号,这时上面给此互斥对象 这个线程的ID,并使之成为未通知状态;若上面函数遇到hMutex无信号,则此函数处于等待状态。 释放互斥对象: The ReleaseMutex function releases ownership of the specified mutex object. BOOL ReleaseMutex( HANDLE hMutex // handle to mutex object ); 在线程函数后面:加个ReleaseMutex(hMutex); //这样互斥对象的ID就为0了 形象比喻:WaitSingleObject--看钥匙(互斥)对象在别人手里没,没有就自己拿,不然就等别人用完了去拿。ReleaseMutex——钥匙用完了。 互斥对象中有计数器,第一次创建的时候(若设为TRUE则对象计数器加1),当此对象再用WaitForSingleObject的时候,计数器再加1变为了2。用ReleaseMutex时计数器减1。这时互斥对象仍然是未通知状态。 这时将两个线程的代码注释掉,然后都添加: WaitForSingleObject(hMutex,INFINITE); cout<<"thread1 is running"<<endl; //线程2是thread2 这样运行结果是两个线程都运行了。 因为一个线程运行完后,若互斥对象没有释放,则系统会将它释放。 3.保证程序一个实例运行。 CreateMutex的返回值: If the function succeeds, the return value is a handle to the mutex object. If the named mutex object existed before the function call, the function returns a handle to the existing object and GetLastError returns ERROR_ALREADY_EXISTS. Otherwise, the caller created the mutex. 先CreateMutex(互斥对象需要命名),然后用GetLastError看是否返回ERROR_ALREADY_EXISTS. hMutex=CreateMutex(NULL,TRUE,"mutex"); if(ERROR_ALREADY_EXISTS==GetLastError()) { cout<<"only one instance can run!"<<endl; return; } 4.新建一个MFC的exe程序Chat,基本对话框的,删除默认的一些东西。 添加一个组框,标题为“接收数据”。在其中放置一个编辑框IDC_EDIT_RECV。 再添加一个组框,标题为“发送数据”。在其中摆放一个IP地址控件,可以让我按十进制输入IP地址。再添加一个编辑框,IDC_EDIT_SEND. 在下面添加一个发送按钮。 BOOL AfxSocketInit( WSADATA* lpwsaData = NULL ); //它会加载套接字库(1.0版本的) If lpwsaData is not equal to NULL, then the address of the WSADATA structure is filled by the call to ::WSAStartup. This function also ensures that ::WSACleanup is called for you before the application terminates. Call this function in your CWinApp::InitInstance override to initialize Windows Sockets 在InitInstance前部添加: if(!AfxSocketInit()) { AfxMessageBox("加载套接字库失败!"); return FALSE; } 但AfxSocketInit没有申明,要包含AfxSock.h头文件。在StdAfx.h后面添加它。 在CChatDlg中添加一个成员函数BOOL InitSocket public;成员变量 SOCKET m_socket private. 在InitSocket函数中: m_socket=socket(AF_INET,SOCK_DGRAM,0); //构造一个套接字 socket的返回值: If no error occurs, socket returns a descriptor referencing the new socket. Otherwise, a value of INVALID_SOCKET is returned, and a specific error code can be retrieved by calling WSAGetLastError. if(INVALID_SOCKET==m_socket) { MessageBox("创建套接字失败!"); return FALSE; } 再构造addrSock: SOCKADDR_IN addrSock; addrSock.sin_addr.S_un.S_addr=htonl(INADDR_ANY); addrSock.sin_family=AF_INET; addrSock.sin_port=htons(6000); bind(m_socket,(SOCKADDR*)&addrSock,sizeof(SOCKADDR_IN)); bind的返回值: If no error occurs, bind returns zero. Otherwise, it returns SOCKET_ERROR, and a specific error code can be retrieved by calling WSAGetLastError. int retVal; retVal=bind(m_socket,(SOCKADDR*)&addrSock,sizeof(SOCKADDR_IN)); if(SOCKET_ERROR==retVal) { closesocket(m_socket); MessageBox("绑定套接字失败!"); return FALSE; } return TRUE; 在OnInitDialog函数后部调用InitSocket(); 然后创建一个线程来接收数据。但CreateaThread只能传递一个参数,所以在Dlg构造函数之前构造一个结构体: struct RECVPARAM { SOCKET sock; HWND hwnd; }; 然后在OnInitDialog函数后部,添加: RECVPARAM *pRecvParam=new RECVPARAM; pRecvParam->sock=m_socket; pRecvParam->hwnd=m_hWnd; HANDLE hThread=CreateThread(NULL,0,RecvProc,(LPVOID)pRecvParam,0,NULL); CloseHandle(hThread); 然后编写线程函数RecvProc,可以直接复制添加到OnInitDialog之前,或者添加成员函数: DWORD WINAPI ThreadProc( //名字改为RecvProc LPVOID lpParameter // thread data );//public 但这时调用此线程函数会有问题,因为这个函数是类的成员函数,调用此函数的时候要加载类对象(但运行时 不知道此对象……),所以应定义为静态static函数,这样它只属于类本身,而不属于对象了。所以运行时代码可直接调用函数。 所以在头文件声明的时候要在前面加个static(这样就相当于全局函数了)。 DWORD WINAPI CChatDlg::RecvProc(LPVOID lpParameter) { SOCKET sock=((RECVPARAM*)lpParameter)->sock; HWND hwnd=((RECVPARAM*)lpParameter)->hwnd; delete lpParameter; //视频讲述时,遗忘了释放内存的操作。sunxin SOCKADDR_IN addrFrom; int len=sizeof(SOCKADDR_IN); char recvBuf[200]; char tempBuf[200]; int retVal; while(TRUE) { retVal=recvfrom(sock,recvBuf,len,0,(SOCKADDR*)&addrFrom,&len); if(SOCKET_ERROR==retVal) break; sprintf(tempBuf,"%s说:%s",inet_ntoa(addrFrom.sin_addr),recvBuf); ::PostMessage(hwnd,WM_RECVDATA,0,tempBuf); } return 0; } 在CChatDlg构造函数中定义消息: #define WM_RECVDATA WM_USER+1 然后在消息映射中:声明: afx_msg void OnRecvData(WPARAM wParam,LPARAM lParam); //chatdlg头文件里 在Dialog的END_MESSAGE_MAP前: ON_MESSSAGE(WM_RECVDATA,OnRecvData) //不要冒号 然后编写消息响应函数: void CChatDlg::OnRecvData(WPARAM wParam,LPARAM lParam) { CString str=(char *)lParam; CString strTemp; GetDlgItemText(IDC_EDIT_RECV,strTemp); str+="\r\n"; str+=strTemp; SetDlgItemText(IDC_EDIT_RECV,str); } 再添加“发送”消息响应函数: CIPAddressCtrl类对应IP地址控件: int GetAddress( BYTE& nField0, BYTE& nField1, BYTE& nField2, BYTE& nField3 ); int GetAddress( DWORD& dwAddress ); 代码如下: void CChatDlg::OnButtonSend() { // TODO: Add your control notification handler code here DWORD dwIP; ((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP); SOCKADDR_IN addrTo; addrTo.sin_addr.S_un.S_addr=htonl(dwIP); addrTo.sin_family=AF_INET; addrTo.sin_port=htons(6000); CString strSend; GetDlgItemText(IDC_EDIT_SEND,strSend); sendto(m_socket,strSend,strSend.GetLength()+1,0, (SOCKADDR*)&addrTo,sizeof(SOCKADDR_IN)); SetDlgItemText(IDC_EDIT_SEND,""); } 但这时编辑框还不会换行,要将控件属性的multiline多行 勾上。也可将发送按钮的 默认按钮 选项勾上,这样按回车就可直接发送了,也可将之visible设为false,因此起来。但按回车依然有效。
(孙鑫 十五)多线程
最新推荐文章于 2015-12-16 22:01:06 发布