Windows程序设计__孙鑫C++Lesson15《多线程与聊天室程序创建》

Windows程序设计__孙鑫C++Lesson15《多线程与聊天室程序创建》

本节要点:                                                                                                                                                                         
1.程序、进程、线程的概念
2.多线程的并发执行
3.多线程模拟售票系统-互斥对象的引入
4.互斥对象的所有权
5.利用互斥信号量 只允许程序的一个实例运行
6.多线程聊天室的创建
//***********************************************************************
1.程序、进程、线程的概念                                                                                                                                             

这些概念的理论比较丰富,若要深入学习可参考孙仲秀《操作系统教程》,在这里仅将课程内容中涉及的知识点列在下面。

(1)程序和进程的概念

程序是计算机指令的集合,它以文件的形式存储在磁盘上。
进程:通常被定义为一个正在运行的程序的实例,是一个程序在其自身的地址空间中的一次执行活动。
进程是资源申请、调度和独立运行的单位,因此,它使用系统中的运行资源;而程序不能申请系统资源,不能被系统调度,也不能作为独立运行的单位,因此,它不占用系统的运行资源。
(2)进程的组成部分

进程由两个部分组成:
 1、操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方。
 2、地址空间。它包含所有可执行模块或DLL模块的代码和数据。它还包含动态内存分配的空间。如线程堆栈和堆分配空间。

(3)进程的特点

进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程的地址空间中的代码。
单个进程可能包含若干个线程,这些线程都“同时” 执行进程地址空间中的代码。
每个进程至少拥有一个线程,来执行进程的地址空间中的代码。当创建一个进程时,操作系统会自动创建这个进程的第一个线程,称为主线程。此后,该线程可以创建其他的线程。
(4)线程概念和特点

线程由两个部分组成:
 1、线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。
 2、线程堆栈,它用于维护线程在执行代码时需要的所有参数和局部变量。
当创建线程时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。可以将线程内核对象视为由关于线程的统计信息组成的一个小型数据结构。
线程总是在某个进程环境中创建。系统从进程的地址空间中分配内存,供线程的堆栈使用。新线程运行的进程环境与创建线程的环境相同。因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存和在这个相同的进程中的所有其他线程的堆栈。这使得单个进程中的多个线程确实能够非常容易地互相通信。
线程只有一个内核对象和一个堆栈,保留的记录很少,因此所需要的内存也很少。
因为线程需要的开销比进程少,因此在编程中经常采用多线程来解决编程问题,而尽量避免创建新的进程。

2.多线程的并发执行                                                                                                                                                         
这里以新建线程与主线程的交替执行为例.
(1)线程创建完毕后,要让他有机会执行,可以暂停主线程的执行,延缓主线程的退出。暂停主线程的运行,使用Sleep函数,其原型为:
VOID Sleep(
  DWORD dwMilliseconds   // 暂停时间 毫秒为单位
);
//***********************************************************************
#include <Windows.h>
#include <iostream.h>
#include <Winbase.h>
DWORD WINAPI Fun1(
  LPVOID lpParameter   // thread data
);
void main()
{
   HANDLE thread1;
   thread1=CreateThread(NULL,0,Fun1,NULL,0,0);
   CloseHandle(thread1);//并没有终止新创建的线程 系统会递减线程内核的实用计数
   cout<<"Main Thread is running!"<<endl;
   Sleep(500);//让新建线程有机会运行 否则执行结果为Main Thread is running!
}
DWORD WINAPI Fun1(
  LPVOID lpParameter   // thread data
)
{  
      cout<<"thread1 is running!"<<endl;
      return 0;
}
Sleep主线程则执行结果为: 
Main Thread is running!
thread1 is running!
//***********************************************************************
(2)主线程与新创建线程交替执行
//******************************************************
#include <Windows.h>
#include <iostream.h>
#include <Winbase.h>
int index=0;
DWORD WINAPI Fun1(
  LPVOID lpParameter   // thread data
);
void main()
{
   HANDLE thread1;
   thread1=CreateThread(NULL,0,Fun1,NULL,0,0);
   CloseHandle(thread1);//并没有终止新创建的线程 系统会递减线程内核的实用计数
   while(index++<1000)
    cout<<"Main Thread is running!"<<endl;
}
DWORD WINAPI Fun1(
  LPVOID lpParameter   // thread data
)
{  
      while(index++<1000)
        cout<<"thread1 is running!"<<endl;
      return 0;

//**********************************************************
执行结果为:
Main Thread is running!
thread1 is running!
Main Thread is running!
thread1 is running!
Main Thread is running!
thread1 is running!
...
//************************************************************
3.多线程模拟售票系统-互斥对象的引入                                                                                                                           
(1)实验代码
//**********************************************************
//SellTickets.cpp
#include <Windows.h>
#include <iostream.h>
#include <Winbase.h>
DWORD WINAPI Fun1(
  LPVOID lpParameter   // thread data
);
DWORD WINAPI Fun2(
  LPVOID lpParameter   // thread data
);
int index=0;
int tickets=100;
HANDLE hMutex;//互斥对象有一个线程ID和计数器
void main()
{
   HANDLE thread1,thread2;
   thread1=CreateThread(NULL,0,Fun1,NULL,0,0);
   thread2=CreateThread(NULL,0,Fun2,NULL,0,0);
   CloseHandle(thread1);//并没有终止新创建的线程 系统会递减线程内核的实用计数
   CloseHandle(thread2);
   Sleep(500);
}
DWORD WINAPI Fun1(
  LPVOID lpParameter 
)
{
 while(true)
 {  
  if(tickets>0)
  {
      cout<<"thread1 sell tickets:"<<tickets--<<endl;
  }
  else
     break;
 }
 return 0;
}
DWORD WINAPI Fun2(
  LPVOID lpParameter
)
{  
 while(true)
 {  
  if(tickets>0)
  {
     cout<<"thread2 sell tickets:"<<tickets--<<endl;
  }
  else
   break;
 }
    return 0;
}
//**********************************************************
运行结果如下图所示:

(2)上述代码存在的隐患

上述代码多次运行时产生可能错误。当tickets=1时,线程1暂停执行,线程2买票,然后tickets变为0;
线程1继续开始执行则线程1则输出卖出0的票。这是一个隐藏的错误,调试起来很不方便,十分困难。要尽量避免问题的出现。这种隐患在这里利用Sleep函数人为添加干扰机制,重现这个错

误,运行结果如下图所示:

(3)问题解决
上述的两个线程访问了同一个全局变量,避免出现这种问题需要引入同步机制,互斥的使用资源。
创建互斥对象,使用函数CreateMutex,其函数原型为HANDLE CreateMutex(
  LPSECURITY_ATTRIBUTES lpMutexAttributes,  // SD
  BOOL bInitialOwner,                       // initial owner
  LPCTSTR lpName                            // object name
);
等待互斥信号,使用函数WaitForSingleObject,其函数原型为
DWORD WaitForSingleObject(
  HANDLE hHandle,        // handle to object
  DWORD dwMilliseconds   // time-out interval
);
这个函数返回有两种情况,第一种等待的信号量变为有信号状态,第二种等待信号量超时。
等待时间设为INFINITE,则一直等待直到信号量变为有信号状态才返回。
释放互斥信号量的拥有权,使用函数ReleaseMutex
其函数原型为BOOL ReleaseMutex(
  HANDLE hMutex   // handle to mutex
);
引入互斥信号量的实验代码如下:
//**********************************************************
#include <Windows.h>
#include <iostream.h>
#include <Winbase.h>
DWORD WINAPI Fun1(
  LPVOID lpParameter   // thread data
);
DWORD WINAPI Fun2(
  LPVOID lpParameter   // thread data
);
int index=0;
int tickets=100;
HANDLE hMutex;//互斥对象有一个线程ID和计数器
void main()
{
   HANDLE thread1,thread2;
   thread1=CreateThread(NULL,0,Fun1,NULL,0,0);
   thread2=CreateThread(NULL,0,Fun2,NULL,0,0);
   CloseHandle(thread1);//并没有终止新创建的线程 系统会递减线程内核的实用计数
   CloseHandle(thread2);
   hMutex=CreateMutex(NULL,FALSE,NULL);
   Sleep(5000);
}
DWORD WINAPI Fun1(
  LPVOID lpParameter   // thread data
)
{  while(true)
 {  
  WaitForSingleObject(hMutex, INFINITE);
  if(tickets>0)
  {
   Sleep(10);//为了观察可能出现的错误 人为加入的干扰
   cout<<"thread1 sell tickets:"<<tickets--<<endl;//临界区互斥问题
  }
  else
   break;
  ReleaseMutex(hMutex);
 }
 return 0;
}
DWORD WINAPI Fun2(
  LPVOID lpParameter   // thread data
)
{  
 while(true)
 {  
  WaitForSingleObject(hMutex, INFINITE);
  if(tickets>0)
  {
   Sleep(10);//为了观察可能出现的错误 人为加入的干扰
   cout<<"thread2 sell tickets:"<<tickets--<<endl;
  }
  else
   break;
  ReleaseMutex(hMutex);
 }
        return 0;
}
//**********************************************************
运行结果如下图所示:


//**********************************************************
(4)注意信号的操作位置要正确,如果代码编写为:
DWORD WINAPI Fun1(
  LPVOID lpParameter   // thread data
)
{  
 WaitForSingleObject(hMutex, INFINITE);//等待信号量放在了循环外层 位置不当 Fun2类同
 while(true)
 {  
  if(tickets>0)
  {
   Sleep(10);
   cout<<"thread1 sell tickets:"<<tickets--<<endl;//临界区互斥问题
  }
  else
   break;
 }
 ReleaseMutex(hMutex);
 return 0;
}
//**********************************************************
则执行时只有一个线程运行了,运行结果可能为下图所示:

//**********************************************************
4.互斥对象的所有权
主线程拥有互斥对象时,在线程1和线程2中释放互斥对象时无法完成,互斥对象内部维护了一个线程ID。
遵守谁拥有互斥对象,谁释放互斥对象的原则。
(1)如果主函数编写如下:
//**********************************************************
void main()
{
   HANDLE thread1,thread2;
   thread1=CreateThread(NULL,0,Fun1,NULL,0,0);
   thread2=CreateThread(NULL,0,Fun2,NULL,0,0);
   CloseHandle(thread1);//并没有终止新创建的线程 系统会递减线程内核的实用计数
   CloseHandle(thread2);
   hMutex=CreateMutex(NULL,TRUE,NULL);//参数TRUE则主线程拥有互斥对象
   //ReleaseMutex(hMutex);
   Sleep(5000);
}
则线程1和线程2将得不到信号量,而不能执行。加上ReleaseMutex(hMutex);后线程1和2方能执行。
(2)如果主函数编写如下:
//**********************************************************
void main()
{
   HANDLE thread1,thread2;
   thread1=CreateThread(NULL,0,Fun1,NULL,0,0);
   thread2=CreateThread(NULL,0,Fun2,NULL,0,0);
   CloseHandle(thread1);//并没有终止新创建的线程 系统会递减线程内核的实用计数
   CloseHandle(thread2);
   //hMutex=CreateMutex(NULL,FALSE,NULL);
   hMutex=CreateMutex(NULL,TRUE,NULL);
   WaitForSingleObject(hMutex, INFINITE);//主线程已经拥有互斥对象 虽然互斥对象为未通知状                      
    //态但是它的拥有ID与主线程相同 仍然可以得到互斥对象
                            //此时互斥对象的引用计数变为2
    ReleaseMutex(hMutex);//此时互斥对象的引用计数仍然为1 为未通知状态
        ReleaseMutex(hMutex); //线程1 线程2 得不到执行机会 必须再次释放互斥对象
 Sleep(5000);
}
则ReleaseMutex(hMutex);应该执行两次。
//**********************************************************
(3)操作系统将已经结束的线程的互斥对象引用计数减1,释放互斥对象。WaitForSingleObject函数的返回值WAIT_ABANDONED表示
线程终止之前为释放互斥对象或者异常终止,
要根据返回值做相应的处理。当线程1申请互斥对象后,不释放互斥对象,
则线程2也可能执行,因为线程1结束时操作系统将释放互斥信号量。实验代码如下:
//**********************************************************
#include <Windows.h>
#include <iostream.h>
#include <Winbase.h>
DWORD WINAPI Fun1(
  LPVOID lpParameter   // thread data
);
DWORD WINAPI Fun2(
  LPVOID lpParameter   // thread data
);
int index=0;
int tickets=100;
HANDLE hMutex;//互斥对象有一个线程ID和计数器
void main()
{
   HANDLE thread1,thread2;
   thread1=CreateThread(NULL,0,Fun1,NULL,0,0);
   thread2=CreateThread(NULL,0,Fun2,NULL,0,0);
   CloseHandle(thread1);//并没有终止新创建的线程 系统会递减线程内核的实用计数
   CloseHandle(thread2);
   //hMutex=CreateMutex(NULL,FALSE,NULL);
   hMutex=CreateMutex(NULL,FALSE,NULL);
   Sleep(5000);
}
DWORD WINAPI Fun1(
  LPVOID lpParameter   // thread data
)
{  WaitForSingleObject(hMutex, INFINITE);//没有释放互斥对象
  cout<<"thread1 is running!"<<endl;
 return 0;
}
DWORD WINAPI Fun2(
  LPVOID lpParameter   // thread data
)
{
     WaitForSingleObject(hMutex, INFINITE);//没有释放互斥对象
 cout<<"thread2 is running!"<<endl;
 return 0;
}
//**********************************************************
运行结果为:
thread1 is running!
thread2 is running!
//***********************************************************************
5.利用互斥信号量 只允许程序的一个实例运行                                                                                                               
这里做了一个简单的测试,实验代码如下:
//***********************************************************************


#include <Windows.h>
#include <iostream.h>
#include <Winbase.h>
HANDLE hMutex;//互斥对象
void main()
{
   hMutex=CreateMutex(NULL,true,"Tickets");
   if(hMutex)  //判断已经存在一个实例在运行了
   {
    if(ERROR_ALREADY_EXISTS==GetLastError())
    {
     cout<<"only one instance can run!"<<endl;
        return;
    }
   }
  cout<<"running!"<<endl;
  Sleep(5000);
}//***********************************************************************
连续开启两个程序,运行效果如下图:

//***********************************************************************

6.多线程聊天室的创建                                                                                                                                                    
(1)加载套接字库
AfxSocketInit 函数包括了WSAStartup和WSACleanup 的调用,这个函数需要在重载CWinApp::InitInstance 函数中实现,
就是在你的应用程序InitInstance 函数中实现。这样不用链接套接字库文件。添加后的代码如下:
BOOL CChatApp::InitInstance()
{
 if(AfxSocketInit()==false)
 {
  AfxMessageBox("加载套接字失败!");
  return false;
 }
   ...
}
(2)线程函数的编写
参数的传递  当要传递多个参数时可以传递结构体的指针。
线程函数的编写 线程函数可以声明为全局函数,但是如果要求不能用全局函数时,
可以采用定义为类的静态成员函数:static DWORD WINAPI RecvProc(LPVOID lpParameter);注意声明为成员函数是不可行的。
(3)在对话框初始化时的,创建线程工作,代码如下:
BOOL CChatDlg::OnInitDialog()
{
  ...
  // TODO: Add extra initialization here
 InitSock();//初始化套接字 代码列在下面
 RECVPARAM *pRecvParam=new RECVPARAM;
    pRecvParam->socket=m_Socket;
 pRecvParam->hwnd =m_hWnd;//接收对话框的句柄 用于传回数据到对话框
 HANDLE handle=CreateThread(NULL,0,RecvProc,(LPVOID)pRecvParam,0,NULL);//创建单独的线程用来接收数据
 CloseHandle(handle);
 return TRUE;  // return TRUE  unless you set the focus to a control
}

(4)接收消息并显示到对话框上,使用自定义消息的方法。自定义消息创建过程如下:
#define WM_RECVDATA WM_USER+1  //step1 定义用户消息
afx_msg void OnRecvData(WPARAM wparam,LPARAM lparam);//step2 添加消息响应函数原型
ON_MESSAGE(WM_RECVDATA,OnRecvData)//step3消息映射
//获取数据消息响应函数 step4
void CChatDlg::OnRecvData(WPARAM wparam,LPARAM lparam)
{   
  CString str=(char *)lparam;
  CString tempstr;
     GetDlgItemText(IDC_EDIT_RECV,tempstr);
     str+="\r\n";
     str+=tempstr;
     SetDlgItemText(IDC_EDIT_RECV,str);//将获得数据显示在最前 先前数据显示到后面
}
(4)实验代码如下:
//***********************************************************************
//初始化套接字
BOOL CChatDlg::InitSock()
{
 m_Socket=socket(AF_INET,SOCK_DGRAM,0);//m_Socket为服务器套接字
    if(m_Socket==INVALID_SOCKET )
 {
  MessageBox("创建套接字失败!");
     return FALSE;
 }
    SOCKADDR_IN AddrSock;
    AddrSock.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
    AddrSock.sin_family=AF_INET;
 AddrSock.sin_port =htons(6000);
    int RetVal=bind(m_Socket,(SOCKADDR *)&AddrSock,sizeof(SOCKADDR));//绑定套接字
 if(SOCKET_ERROR==RetVal)
    {   closesocket(m_Socket);
  MessageBox("绑定套接字失败!");
     return FALSE;
 }
    return TRUE;

}
//接受数据的线程函数
DWORD WINAPI CChatDlg::RecvProc(LPVOID lpParameter )
{
   SOCKET Sock=((RECVPARAM *)lpParameter)->socket;
   HWND hWnd=((RECVPARAM *)lpParameter)->hwnd ;//取出两个参数值 注意转换类型
   SOCKADDR_IN AddrFrom;
   int len=sizeof(SOCKADDR);
   int RetVal;
   char RecvBuf[200];
   char TempBuf[100];
   while(true)
   {
    RetVal=recvfrom(Sock,RecvBuf,200,0,(SOCKADDR *)&AddrFrom,&len);
    if( SOCKET_ERROR ==RetVal)
    {
     AfxMessageBox("接收消息失败!");
     break;
    }
    sprintf(TempBuf,"Message from %s is :%s\n",inet_ntoa(AddrFrom.sin_addr),RecvBuf);
    ::PostMessage(hWnd,WM_RECVDATA,0,WPARAM(TempBuf));//发送消息注意这里用到了对话框的句柄  
    //将数据显示到对话框中 这里注意用到了一个传递数据地址来解决传递字符串的问题
   }
   return 0;
}
//获取数据并显示在对话框控件上的消息响应函数
void CChatDlg::OnRecvData(WPARAM wparam,LPARAM lparam)
{   
  CString str=(char *)lparam;
  CString tempstr;
     GetDlgItemText(IDC_EDIT_RECV,tempstr);
     str+="\r\n";
  str+=tempstr;
     SetDlgItemText(IDC_EDIT_RECV,str);//将获得数据显示在最前 先前数据显示到后面
}
//发送数据
void CChatDlg::OnBtnsend()
{
 // TODO: Add your control notification handler code here
 DWORD dwIp;
 ((CIPAddressCtrl *)GetDlgItem(IDC_IPSEND))->GetAddress(dwIp);
 SOCKADDR_IN AddrTo;
    AddrTo.sin_addr .S_un.S_addr =htonl(dwIp);//从IP地址控件上获取IP地址
    AddrTo.sin_family=AF_INET;
 AddrTo.sin_port=htons(6000);
    CString strSend;
 GetDlgItemText(IDC_EDIT_SEND,strSend);
 int RetVal;
 RetVal=sendto(m_Socket,strSend,strSend.GetLength()+1,0,(SOCKADDR *)&AddrTo,sizeof(SOCKADDR));//发送数据
 if(SOCKET_ERROR==RetVal)
  ShowLastError();
    SetDlgItemText(IDC_EDIT_SEND,"");
}
//显示错误的函数
void CChatDlg::ShowLastError()
{
 LPVOID lpMsgBuf;
 FormatMessage(
  FORMAT_MESSAGE_ALLOCATE_BUFFER |
  FORMAT_MESSAGE_FROM_SYSTEM |
  FORMAT_MESSAGE_IGNORE_INSERTS,
  NULL,
  GetLastError(),
  MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
  (LPTSTR) &lpMsgBuf,
  0,
  NULL
  );
 AfxMessageBox((LPCTSTR)lpMsgBuf, MB_OK | MB_ICONINFORMATION);
 LocalFree( lpMsgBuf );
}
//***********************************************************************
程序运行效果如下图:


//************************************************************************************************
本节小结:                                                                                                                                                                    
1.掌握多线程的运行机制,尤其是线程运行机会怎么分配
2.掌握线程的创建、线程函数编写尤其是线程函数参数的传递
3.掌握互斥信号量的创建、使用及其特点
4.通过多线程聊天室的编程,巩固UDP协议编程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值