`
javababy1
  • 浏览: 1161870 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

孙鑫MFC笔记(15)--多线程和聊天室的创建

 
阅读更多

孙鑫MFC笔记(15)--多线程和聊天室的创建

代码1分析:

说明:对于单核cpu的电脑来说,线程都在自己的时间片中运行,单位时间内,系统只能运行一个线程,交替运行;对于多核cpu或多cpu的电脑来说,才是真正意义上的单位时间内运行多个线程

说明:如果采用向导创建的Win32 Console程序选择的是空项目时,则以下使用方式是正确的,如果选择是”hello world”的简单应用程序时,则在main函数中想要使用输入输出流,则必须使用:

#include <iostream>

using namespace std;

#include <windows.h> //调用API函数

#include <iostream.h>

说明:程序有一个主线程,入口函数main,在主线程上可以创建新线程,入口函数定义成如下形式:

DWORD WINAPI ThreadProc1(LPVOID lpParameter); //函数声明

void main()

{

HANDLE handle=CreateThread(NULL,0,ThreadProc1,NULL,0,NULL); //创建新线程

说明:调用CloseHandle函数并没有终止新创建的线程,只是表明在主线程中对新创建的线程的引用不是很感兴趣而已,所以在没有在做什么动作之前就关闭了,这个函数在主线程运行过程中必须调用,否则即使对应线程运行结束,对应句柄也是不会关闭的,知道主线程退出时,才会由系统集中清理。

CloseHandle(handle);

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

说明:如果不添加Sleep语句,主线程会在自己的时间片中运行完成后(该时间片在main函数,也就是主线程全部执行完毕后还有时间剩余),选择直接退出,主线程都退出了,依附于主线程的新线程也就不会有机会得到执行了,只有让主线程暂停执行(采用sleep函数),即挂起,让出执行的权利,操作系统会从等待的线程中选择一个来运行,那么新创建的线程得到机会执行

Sleep(10);

}

DWORD WINAPI ThreadProc1(LPVOID lpParameter)

{

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

return 0;

}

输出结果:

main thread is running

thread1 is running

代码2分析:

添加全局变量

int index=0;

将main函数中输出语句修改为:

while(index++<50)

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

将线程中输出语句修改为:

while(index++<50)

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

将main函数中sleep语句省去

则得到的结果是:

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

thread1 is running!

thread1 is running!

thread1 is running!

thread1 is running!

thread1 is running!

thread1 is running!

thread1 is running!

thread1 is running!

thread1 is running!

thread1 is running!

thread1 is running!

thread1 is running!

thread1 is running!

thread1 is running!

thread1 is running!

thread1 is running!

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

main thread is running

结果说明主线程和副线程在交替运行,也就是主线程在它的时间片运行结束后,副线程得到执行的权利,在它自己所对应的时间片中运行,此时主线程其实还没有运行结束,它将等待着副线程运行结束后继续执行

代码3分析(车票的销售):

#include <windows.h>

#include <iostream.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);

}

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;

}

输出结果:

thread1 sale the ticket id is:50

thread1 sale the ticket id is:49

thread1 sale the ticket id is:48

thread1 sale the ticket id is:47

thread1 sale the ticket id is:46

thread1 sale the ticket id is:45

thread1 sale the ticket id is:44

thread1 sale the ticket id is:43

thread1 sale the ticket id is:42

thread1 sale the ticket id is:41

thread1 sale the ticket id is:40

thread1 sale the ticket id is:39

thread1 sale the ticket id is:38

thread1 sale the ticket id is:37

thread1 sale the ticket id is:36

thread1 sale the ticket id is:35

thread1 sale the ticket id is:35

thread2 sale the ticket id is:34

thread2 sale the ticket id is:33

thread2 sale the ticket id is:32

thread2 sale the ticket id is:31

thread2 sale the ticket id is:30

thread2 sale the ticket id is:29

thread2 sale the ticket id is:28

thread2 sale the ticket id is:27

thread2 sale the ticket id is:26

thread2 sale the ticket id is:25

thread2 sale the ticket id is:24

thread2 sale the ticket id is:23

thread2 sale the ticket id is:22

thread2 sale the ticket id is:21

thread2 sale the ticket id is:20

thread2 sale the ticket id is:19

thread1 sale the ticket id is:18

thread1 sale the ticket id is:17

thread1 sale the ticket id is:16

thread1 sale the ticket id is:15

thread1 sale the ticket id is:14

thread1 sale the ticket id is:13

thread1 sale the ticket id is:12

thread1 sale the ticket id is:11

thread1 sale the ticket id is:10

thread1 sale the ticket id is:9

thread1 sale the ticket id is:8

thread1 sale the ticket id is:7

thread1 sale the ticket id is:6

thread1 sale the ticket id is:5

thread1 sale the ticket id is:4

thread1 sale the ticket id is:3

thread1 sale the ticket id is:2

thread1 sale the ticket id is:1

我们发现结果一切正常,但是有可能会遇到如下一种情况:

当ticket数量运行到1时,线程1正在运行,此时线程1运行到输出语句时,它的时间片已经结束,则线程1对ticket id的减减动作没有完成,此时线程2开始执行,发现数量是1,则执行减减动作,使得数量为0,返回,线程1继续执行,此时票的数量已经是0了,线程1继续执行输出语句,对票的数量执行减减,则数量变为-1,这是不允许的。这是由于抢占全局的资源所引起的。

解决这个问题的办法是实现线程间的“同步”,即一个线程在对一个全局的资源进行操作的过程中,是不允许其他线程对全局的资源进行访问,直到该线程对资源操作完毕后。

涉及概念:互斥对象

n 互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。

n 互斥对象包含一个使用数量,一个线程ID和一个计数器。

n ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。

涉及到三个函数:

1. CreateMutex:创建互斥对象,返回互斥对象的句柄,其中第二个参数如果是TRUE,表示调用该函数的线程拥有互斥对象的所有权,即互斥对象处于非空闲状态。如果是FALSE,则表示当前互斥对象处于空闲状态,其他线程可以占用。

2. WaitForSingleObject:等待互斥对象的使用权,如果第二个参数设置为INFINITE,则表示会持续等待下去,直到拥有所有权,才有权执行该函数下面的语句。一旦拥有了所有权,则会将互斥对象的的线程ID设置为当前使用的线程ID值。

3. ReleaseMutex:将互斥对象所有权进行释放,交还给系统。

可以将互斥对象想象成一把钥匙,CreateMutex创建了这把钥匙,WaitForSingleObject等待这把钥匙去访问一个公共的资源,比如一个房间,如果拥有了钥匙,则这个房间的所有权就属于这个进程了,别人是进不去这个房间的,直到进程将这个房间的钥匙归还掉,即ReleaseMutex。

具体实现代码:

#include <windows.h>

#include <iostream.h>

DWORD WINAPI ThreadProc1(LPVOID lpParameter);

DWORD WINAPI ThreadProc2(LPVOID lpParameter);

int ticket=100;

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

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

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相同的动作。

两段代码最主要的区别就是前者在销售所有票的过程中都独占着互斥对象资源,而后者是销售完一张票后就将互斥对象资源释放掉了。

几点思考:

1. 在main函数中将CreateMutex 的第二个参数修改为TRUE,则表示主线程在创建互斥对象的时候就拥有了所有权,执行代码,发现线程1和2都没有执行,只是因为主线程没有释放掉互斥对象。考虑在线程1和2的WaitForSingleObject前添加ReleaseMutex可否?也就是我在申请前将别人占用的互斥对象释放掉,这显然是不行的,不然就失去互斥的真正意义了,如果我自己想用,就将别人踢掉,就失去了规则了。那内部是怎么实现的呢?在CreateMutex或WaitForSingleObject申请到互斥对象的所有权后,会将互斥对象中的线程ID设置为调用线程的ID,ReleaseMutex时会比较当前的线程ID和互斥对象中的ID是否一致,如果不一致,则禁止释放。所以要想线程1和2能正常执行,则必须在CreateMutex后调用ReleaseMutex。

2. 在main函数中CreateMutex的第二个参数修改为TRUE后,互斥对象的所有权由主线程所有,然后调用WaitForSingleObject,发现申请依然成功,这是因为拥有所有权的线程是自身线程,单互斥对象的内部发生了变化,它内部的计数器设置成了2,也就是如果希望线程1和2能执行,则需要两次调用ReleaseMutex。ReleaseMutex函数的意义就是将计数器递减。

3. 如果在线程中调用WaitForSingleObject后没有调用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(addrSock,(SOCKADDR*)&addrServer,sizeof(SOCKADDR));

if(SOCKET_ERROR==retVal)

{

closesocket(addrSock);

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方法也刚好传递一个参数,即当前窗口的句柄

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/roger_ge/archive/2008/09/08/2896576.aspx

其他参考:

http://mxmkeep.blog.163.com/blog/static/106492415200992414047520/
http://blog.chinaunix.net/u1/50617/showart.php?id=481327
http://blog.sina.com.cn/s/blog_57f53ed201000ceh.html
http://blog.csdn.net/roger_ge/archive/2008/09/08/2896576.aspx

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

HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
DWORD dwStackSize, // initial stack size 页面是系统在管理内存时使用的一个内存单位,不同的CPU其页面大小也是不同的,为0则和调用线程一样的大小
LPTHREAD_START_ROUTINE lpStartAddress, // thread function 表示线程的起始地址,对于主线程来说main函数为它入口的入口函数,对于我们创造的线程来说也需要一个入口函数,这个函数的地址就在第三个函数这里指定,名字是自己取的
LPVOID lpParameter, // thread argument
DWORD dwCreationFlags, // creation option 如果是0创建完成后立即运行,若不立即运行具见MSDN
LPDWORD lpThreadId // thread identifier
);

多线程:

#include <Windows.h>
#include <iostream>
using namespace std;

DWORD WINAPI Fun1Proc1( LPVOID lpParameter );
DWORD WINAPI Fun1Proc2( LPVOID lpParameter );

int tickets = 100;
HANDLE hMutex;

int main()
{
HANDLE hThread1,hThread2;
hThread1 = CreateThread(NULL,0,Fun1Proc1,NULL,0,NULL);//创建一个线程
hThread2 = CreateThread(NULL,0,Fun1Proc2,NULL,0,NULL);
CloseHandle(hThread1);//关闭线程句柄,但线程并没有被关闭
CloseHandle(hThread1);

/*
while (index++ < 1000)
{
cout<<"main thread is running"<<endl;
}
*/

//创建一个或者打开一个命名的或者没有命名的互斥对象
//hMutex = CreateMutex(NULL,FALSE,NULL);//第二个参数为false,当前就没有线程拥有这个互斥对象,操作系统就会将这个
//互斥对象设为已通知状态,也就是有信号状态,若为true则是主线程拥有了这个互斥对象

//第一次创建这个互斥对象的时候,主线程拥有了这个互斥对象,除了将这个互斥对象的线程ID设为主线程的线程ID
//同时将我们的互斥对象的计数器设为1,互斥对象为未通知状态,WaitForSingleObject又请求到,所以计数器2,
//我们调用ReleaseMutex(hMutex);实际上是计数器减一,2次计数器为0,操作系统将互斥对象的线程ID设为0,
//互斥对象设为已通知状态

//hMutex = CreateMutex(NULL,TRUE,NULL);
hMutex = CreateMutex(NULL,TRUE,"tickets");
if(hMutex)
{

if(ERROR_ALREADY_EXISTS==GetLastError())
{
cout<<"the application instance is exit!"<<endl;
return 0;
}
}

WaitForSingleObject(hMutex,INFINITE);//1.互斥对象ID和线程ID一样也可以调用 2.互斥对象设为已通知状态,也就是有信号状态
ReleaseMutex(hMutex);
ReleaseMutex(hMutex);

Sleep(4000);//指定暂停时间间隔,放弃运行,系统调用子线程

return 0;
}

DWORD WINAPI Fun1Proc1( LPVOID lpParameter )
{
while(TRUE)
{
WaitForSingleObject(hMutex,INFINITE);//其返回值是可以判断进程是异常终止还是正常获得1:14:32
if(tickets > 0)
{
Sleep(1);
cout<<"thread1 sell tickets:"<<tickets--<<endl;
}
else
{
break;
}
ReleaseMutex(hMutex);
}

return 0;
}

DWORD WINAPI Fun1Proc2( LPVOID lpParameter )
{
while(TRUE)
{
WaitForSingleObject(hMutex,INFINITE);
if(tickets > 0)
{
Sleep(1);
cout<<"thread2 sell tickets:"<<tickets--<<endl;
}
else
{
break;
}
ReleaseMutex(hMutex);
}

return 0;
}

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics