15.3.2多线程程序容易出现的问题

//如果线程2执行完上述操作之后,正好轮到线程1开始执行,而这时线程1上次是执行到if语句块之后才暂停的,而这时tickets变量的值已经为0了,也就是线程1卖了号码为0的票。
//为了避免上述情况的发生,要求在多个线程之间进行一个同步处理,保证一个线程访问共享资源时候,其他线程不能访问该资源。

15.3.3利用互斥对象实现线程同步

//互斥对象属于内核对象,他能确保线程对单个资源的互斥访问权;互斥对象包含一个使用数量,一个线程ID与一个计数器,其中ID用于标识系统中的那个线程当前拥有互斥对象,计数器表示该线程拥有互斥对象的次数。
//为了创建互斥对象,可以调用CreateMutex。

HANDLE CreateMutex(
  LPSECURITY_ATTRIBUTES lpMutexAttributes,
                       //若为NULL,使用默认的安全性
  BOOL bInitialOwner,  // 真表示创建这个互斥对象的线程获得该对象所有权;假该线程将不获得所创建的互斥对象所有权。
  LPCTSTR lpName   // 指定互斥对象名称,NULL创建一个匿名互斥对象
);

//线程对共享资源访问结束后,应该释放该对象所有权,这时需要调用ReleaseMutex,将释放指定对象的所有权。(P操作)

BOOL ReleaseMutex(
  HANDLE hMutex   // handle to mutex object
);

//线程必须主动请求共享对象的使用权才有可能获得该所有权,可以通过调用WaitForSingleObject函数来实现。(V操作)

DWORD WaitForSingleObject(
  HANDLE hHandle,        // handle to object to wait for
  DWORD dwMilliseconds   // 等待时间间隔,以毫秒为单位。0表示立即返回,INFINITE表示永远等待。
);

//调用WaitForSingleObject后,该函数会一直等待,只有以下两种情况才会返回:指定对象变成有信号状态;指定等待时间间隔已过。

#include<windows.h>
#include<iostream.h>
DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
int index=0;
int tickets=100;
HANDLE hMutexs=100;
void main()
{
    HANDLE hThread1;
    HANDLE hThread2;
    //创建互斥对象
    hMutex=CreateMutex(NULL,FALSE,NULL);
    //创建线程
    hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
    hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
    CloseHandle(hThread1);
    CloseHandle(hThread2);
    Sleep(4000);//让主线程睡四秒,系统给线程1和线程2分配时间片运行
}
//线程1的入口函数
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
    while(TRUE)
    {
        WaitForSingleObject(hMutex,INFINITE);
        if(tickets>0)
        {
            Sleep(1);
            cout<<"thread1 sell ticket:"<<tickets--<<endl;
        }
        else
        {
            break;
        }
        ReleaseMutex(hMutex);
    }
    return 0;
}
//线程2的入口函数
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
    while(TRUE)
    {
        WaitForSingleObject(hMutex,INFINITE);
        if(tickets>0)
        {
            Sleep(1);
            cout<<"thread2 sell ticket:"<<tickets--<<endl;
        }
        else
        {
            break;
        }
        ReleaseMutex(hMutex);
    }
    return 0;
}

//创建互斥对象时候,第二个参数是FALSE,表示当前没有线程拥有该互斥对象,于是操作系统就将互斥对象设置为有信号状态。当第一个线程调用WaitForSingleObject,有信号就会执行以下语句,同时将有信号改为无信号。接着调用 ReleaseMutex将无信号变成有信号。这个就保证了while程序是作为一个整体进行运行,不会出现程序运行到while程序的中间因为时间片到而停止的现象,自然不会打印出0。

//如果将上述程序修改为:
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
    WaitForSingleObject(hMutex,INFINITE);
    while(TRUE)
    {
        if(tickets>0)
        {
            Sleep(1);
            cout<<"thread1 sell ticket:"<<tickets--<<endl;
        }
        else
        {
            break;
        }
    }
    ReleaseMutex(hMutex);
    return 0;
}

//运行程序一直由线程1在运行,因为在while里面没有ReleaseMutex函数对信号量重置为有信号状态。

//如果将上述程序修改为:
void main()
{
    HANDLE hThread1;
    HANDLE hThread2;
    //创建互斥对象
    hMutex=CreateMutex(NULL,TRUE,NULL);
    //创建线程
    hThread1=CreateThread(NULL,0,FunProc1,NULL,0,NULL);
    hThread2=CreateThread(NULL,0,FunProc2,NULL,0,NULL);
    CloseHandle(hThread1);
    CloseHandle(hThread2);
    ReleaseMutex(hMutex);
    Sleep(4000);//让主线程睡四秒,系统给线程1和线程2分配时间片运行
}

//因为第二个参数为TRUE,所以主线程拥有互斥对象,因此如果想让线程1和线程2获得信号量,需要调用ReleaseMutex释放信号量。如果在创建互斥对象后调用了WaitForSingleObject,那么需要两次调用ReleaseMutex释放信号量。

15.4保证应用程序只有一个实例运行

在调用CreateMutex函数创建一个命名的互斥对象后,可以接着调用GetLastError函数,如果该函数返回的是ERROR_ALREADY_EXISTS,就表明先前已经创建了这个命名的互斥对象,如果不是,说明这个互斥对象是新建的。

void main()
{
    HANDLE hThread1;
    HANDLE hThread2;
    //创建互斥对象
    hMutex=CreateMutex(NULL,TRUE,NULL);
    if(hMutex)
    {
        if(ERROR_ALREADY_EXISTS==GetLastError())
        {
            cout<<"only one instance can run!"<<endl;
            return;
        }
    }
    //创建线程
    hThread1=CreateThread(NULL,0,FunProc1,NULL,0,NULL);
    hThread2=CreateThread(NULL,0,FunProc2,NULL,0,NULL);
    CloseHandle(hThread1);
    CloseHandle(hThread2);
    WaitForSingleObject(hMutex,INFINITE);
    ReleaseMutex(hMutex);
    ReleaseMutex(hMutex);
    Sleep(4000);
}

//这就是利用命名的互斥对象实现的同时只能有应用程序的一个实例在运行这个功能的方法。

15.5网络聊天程序的实现

//利用多线程技术编写一个图形界面的网络聊天程序,新建一个基于对话框的工程,工程名为chat。将对话框所有资源控件全部删除,添加控件如图
在这里插入图片描述

15.5.1加载套接字库

//在MFC中加载套接字库并进行版本协商函数:AfxSocketInit。
BOOL AfxSocketInit( WSADATA* lpwsaData = NULL );
//该函数内部调用了WSAStartup和WSASCleanup函数,该函数加载的是1.1版本的套接字库。且利用该函数加载套接字库时不用链接ws2_32.lib文件。如果该函数调用成就返回非零值,否则返回0。
//本例在CChatApp类的InitInstance函数开始位置调用AfxSocketInit函数:

if(!AfxSocketInit())
{
 AfxMessageBox("加载套接字库失败!");
 return false;
}

//因为程序中调用了AfxSocketInit函数,需要在stdafx.h中包含Afxsock.h头文件。

15.5.2创建并初始化套接字

//为CChatDlg类添加一个SOCKET类型的成员变量:m_socket,即套接字描述符,将其访问权限设置为private。然后为CChatDlg类添加一个BOOL类型的成员函数:InitSocket,public类型,用来初始化该类套接字成员变量:

BOOL CChatDlg::InitSocket()
{
 //创建套接字库
 m_socket=socket(AF_INET,SOCK_DGRAM,0);
 if(INVALID_SOCKET==m_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;
 //绑定套接字
 retval=bind(m_socket,(SOCKADDR*)&addrSock,sizeof(SOCKADDR));
 if(SOCKET_ERROR==retval)
 {
  closesocket(m_socket);
  MessageBox("绑定失败!");
  return FALSE;
 }
 return TRUE;
}

//然后可以在对话框类的OnInitDialog函数倒数第二行调用这个函数,以便完成套接字的初始化工作。
InitSocket();

15.5.3实现接收端的功能

//因为接收端接收数据时,没有数据到来,recvfrom就会处于阻塞状态,导致程序暂停运行。所以将接收数据的操作放在一个单独的线程中完成,并给这个线程传递两个参数:一个是已创建的套接字,一个是对话框控件的句柄
//需要定义一个结构体,在该结构体中传递这两个参数,然后将这个结构体类型的指针变量传递给CreateThread函数的第四个参数。
//在对话框类的头文件中,该类的声明的外部定义一个RECVPARAM结构体:

struct RECVPARAM
{
 SOCKET sock;//已创建的套接字
 HWND hwnd;//对话框句柄
};

//在InitSocket函数中添加以下代码,完成数据接收线程的创建,并传递所需的参数:

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。

DWORD WINAPI CChatDlg::RecvProc()
{
 return 0;
}

/但是运行会报错,因为创建线程时会调用线程函数来启动线程,这里的线程函数是对话框类的成员函数,为了调用这个函数必须先产生一个对话框类的对象。这里的解决方法是将线程的函数声明为类的静态函数,在对话框头文件的中RecvProc声明前添加static。因为静态函数是属于类的,而不是属于某个对象的。

//现在在RecvProc函数中添加代码完成接收数据的功能:

DWORD WINAPI CChatDlg::RecvProc(LPVOID lpParameter)
{
 //获取主线程创建的套接字和窗口句柄
 SOCKET sock=((RECVPARAM*)lpParameter)->sock;
 HWND hwnd=((RECVPARAM*)lpParameter)->hwnd;
 delete lpParameter;
 SOCKADDR_IN addrFrom;
 int len=sizeof(SOCKADDR);
 char recvBuf[200];
 char tempBuf[300];
 int retval;
 while(TRUE)
 {
  //接收数据
  retval=recvfrom(sock,recvBuf,200,0,(SOCKADDR*)&addrFrom,&len);
  if(SOCKET_ERROR==retval)
   break;
  sprintf(tempBuf,"%s说:%s",inet_ntoa(addrFrom.sin_addr),recvBuf);
  ::PostMessage(hwnd,WM_RECVDATA,0,(LPARAM)tempBuf);
 }
 return 0;
}

//PostMessage发送一条自定义的消息:WM_RECVDATA,将显示的数据作为lparam参数传递。在对话框头文件中定义WM_RECVDATA这个消息:
#define WM_RECVDATA WM_USER+1
//然后再对话框类头文件中添加消息响应函数的声明:
//}}AFX_MSG
afx_msg void OnRectData(WPARAM wParam,LPARAM lParam);
DECLARE_MESSAGE_MAP()

//然后在对话框的源文件内添加WM_RECVDATA消息映射
//}}AFX_MSG_MAP
ON_NESAGE(WM_RECVDATA,OnRecvData)
END_MESSAGE_MAP()

//最后就是消息响应函数的实现

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);
}

15.5.4实现发送端功能

//当用户单击发送按钮后,将用户输入的数据发送给聊天的对方。双击对话框上面的发送按钮,就会自动生成一个按钮单击命令消息响应函数:OnBtnSend。

void CChatDlg::OnBtnSend() 
{
 // TODO: Add your control notification handler code here
 //获取对方IP
 DWORD dwIP;
 ((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP);
 SOCKADDR_IN addrTo;
 addrTo.sin_family=AF_INET;
 addrTo.sin_port=htons(6000);
 addrTo.sin_addr.S_un.S_addr=htonl(dwIP);
 CString strSend;
 //获得待发送数据
 GetDlgItemText(IDC_EDIT_SEND,strSend);
 //发送数据
 sendto(m_socket,strSend,strSend.GetLength()+1,0,(SOCKADDR*)&addrTo,sizeof(SOCKADDR));
 //清空发送编辑框中的内容
 SetDlgItemText(IDC_EDIT_SEND,"");
}

//运行结果如图。为了让编辑框控件接受换行符,必须设置该控件支持多行数据这一属性。如图。
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

身影王座

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值