多线程编程基础及简易聊天室

本文介绍了多线程编程的基础知识,包括进程、线程的概念及其组成部分,详细讲解了创建线程的CreateThread函数和互斥对象Mutex的使用。此外,文章还提到了基于MFC的简易聊天室程序的实现,通过加载套接字在CChatApp::InitInstance()和CChatDlg::OnInitDialog()函数中进行初始化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、基本概念

程序是计算机指令的集合,它以文件的形式存储在磁盘上。

进程:通常被定义为一个正在运行的程序的实例,是一个程序在其自身的地址空间中的一次执行活动。

线程:有时被称为轻量级进程,是程序执行流的最小单元。每个进程至少拥有一个线程。

进程是资源申请、调度和独立运行的单位,因此,它使用系统中的运行资源;而程序不能申请系统资源,不能被系统调度,也不能作为独立运行的单位,因此,它不占用系统的运行资源。进程从来不执行任何东西,它只是线程的容器,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程的地址空间中的代码。当创建一个进程时,操作系统会自动创建这个进程的第一个线程,称为主线程。此后,该线程可以创建其他的线程。每个进程有它自己的私有地址空间。进程A可能有一个存放在它的地址空间中的数据结构,地址是0x12345678,而进程B则有一个完全不同的数据结构存放在它的地址空间中,地址是0x12345678。当进程A中运行的线程访问地址为0x12345678的内存时,这些线程访问的是进程A的数据结构。当进程B中运行的线程访问地址为0x12345678的内存时,这些线程访问的是进程B的数据结构。进程A中运行的线程不能访问进程B的地址空间中的数据结构,反之亦然。

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

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

线程由两个部分组成:
1、线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。
2、线程堆栈,它用于维护线程在执行代码时需要的所有参数和局部变量。

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

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

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

二、相关函数

1、CreateThread 函数

函数原型:

HANDLE CreateThread ( 
	LPSECURITY_ATTRIBUTES lpThreadAttributes,
	DWORD dwStackSize,
	LPTHREAD_START_ROUTINE lpStartAddress,
	LPVOID lpParameter,
	DWORD dwCreationFlags,
	LPDWORD lpThreadId 
);

函数参数:

<1>lpThreadAttributes:当值为NULL时,让该线程使用默认的安全性。如果希望所有的子进程能够继承该线程对象的句柄,就必须设定一个SECURITY_ATTRIBUTES结构体,将它的bInheritHandle成员初始化为TRUE。

<2> dwStackSize:设置线程初始栈的大小,即线程可以将多少地址空间用于它自己的栈,以字节为单位。系统会把这个参数值四舍五入为最接近的页面大小。如果这个值为0,或者小于默认的提交大小,那么默认将使用与调用该函数的线程相同的栈空间大小。

<3>lpStartAddress:指向应用程序定义的线程入口函数,这个函数将由新线程执行,表明新线程的起始地址。该函数的名称可以自定义,格式如下:

DWORD  WINAPI  ThreadProc( LPVOID lpParameter );

<4>lpParameter:可以通过这个参数给创建的新线程传递参数。该参数提供了一种将初始化值传递给线程函数的手段。这个参数的值既可以是一个数值,也可以是一个指向其它信息的指针。

<5>dwCreationFlags:设置用于控制线程创建的附加标记。如果该值为CREATE_SUSPENDED,那么线程创建后处于暂停状态,直到程序调用了ResumeThread函数为止。如果值为0,那么线程在创建之后立即运行。

<6>lpThreadId:指向一个变量,用来接收线程ID。当创建一个线程时,系统会为该线程分配一个ID。

2、CreateMutex函数

函数原型:HANDLE  CreateMutex( LPSECURITY_ATTRIBUTES  lpMutexAttributes, BOOL  bInitialOwner,  LPCTSTR  lpName);

该函数可以创建或打开一个命名的或匿名的互斥对象,然后程序就可以利用该互斥对象完成线程间的同步。

函数参数:

<1>lpMutexAttributes:值为NULL时,让互斥对象使用默认的安全性。

<2>bInitialOwner:若值为真,则创建这个互斥对象的线程获得该对象的所有权;否则,该线程将不获得所创建的互斥对象的所有权。

<3>lpName:指定互斥对象的名称。如果值为NULL,则创建一个匿名的互斥对象。

PS:如果该函数调用成功,将返回所创建的互斥对象的句柄。如果创建的是命名的互斥对象,并且在CreateMutex函数调用之前,该命名的互斥对象存在,将返回已经存在的这个互斥对象的句柄,而这时调用GetLastError函数,将返回ERROR_ALREADY_EXISTS。当线程结束对互斥对象的访问后,应释放该对象的所有权,需调用ReleaseMutex

函数实现。函数原型为:BOOL  ReleaseMutex( HANDLE  hMutex);调用成功返回非0值,否则返回0。

3、WaitForSingleObject函数

函数原型:DWORD  WaitForSingleObject( HANDLE  hHandle, DWORD  dwMilliseconds );

线程必须主动请求共享对象的使用权才有可能获得该所有权。

函数参数:

<1>hHandle:所请求的对象的句柄。一旦互斥对象处于有信号状态,则该函数就返回。如果该互斥对象处于无信号状态,则函数一直等待,这样就会暂停线程的执行。

<2>dwMilliseconds:指定等待的时间间隔,以毫秒为单位。如果指定的时间间隔已过,即所请求的对象仍处于无信号状态,函数也会返回。如果此参数设置为0,那么函数将测试该对象的状态并立即返回。如果将此参数设置为INFINITE,则该函数就会永远等待,直到等待的对象处于有信号状态才返回。

PS:函数返回值的说明:

返回值说明
WAIT_OBJECT_0所请求的对象是有信号状态
WAIT_TIMEOUT指定的时间间隔已过,并且所请求的对象是无信号状态
WAIT_ABANDONED先前拥有该互斥对象的线程结束前没有释放该对象,该对象的所有权将授予当前调用线程


三、聊天程序

使用MFC框架来写,应用程序名为Chat:

1、加载套接字

在CChatApp::InitInstance()函数中:

if(!AfxSocketInit())//是1.1的版本
{
	AfxMessageBox(_T("加载套接字库失败!"));
	return FALSE;
}
2、创建并初始化套接字

在CChatDlg::OnInitDialog()函数中:

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

其中InitSocket函数为:

BOOL CChatDlg::InitSocket()
{
	m_socket=socket(AF_INET,SOCK_DGRAM,0);
	if(INVALID_SOCKET==m_socket)
	{
		MessageBox(_T("套接字创建失败!"));
		return FALSE;
	}
	SOCKADDR_IN addrSock;
	addrSock.sin_family=AF_INET;
	addrSock.sin_port=htons(6000);
	addrSock.sin_addr.S_un.S_addr=htonl(INADDR_ANY);

	int retval;
	retval=bind(m_socket,(SOCKADDR*)&addrSock,sizeof(SOCKADDR));
	if(SOCKET_ERROR==retval)
	{
		closesocket(m_socket);
		MessageBox(_T("绑定失败!"));
		return FALSE;
	}
	return TRUE;

}

3、实现接收端功能和发送端

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

	wchar_t  recvBuf[200];
	wchar_t  tempBuf[300];
	int retval;
	while(TRUE)
	{
		retval=recvfrom(sock,(char *)recvBuf,200,0,(SOCKADDR*)&addrFrom,&len);
		if(SOCKET_ERROR==retval)
			break;
		char * ip = inet_ntoa(addrFrom.sin_addr);
		wchar_t * wIp = new wchar_t;
		memset(wIp,0,sizeof(wchar_t *));
		MultiByteToWideChar(CP_ACP,0,ip,-1,wIp,16);//unicode下编程要进行字符格式转换,否则显示乱码

		wsprintfW(tempBuf,L"%s说: %s",wIp,recvBuf);
		::PostMessage(hwnd,WM_RECVDATA,0,(LPARAM)tempBuf);
	}
	
	return 0;
}

LRESULT CChatDlg::OnRecvData(WPARAM wParam,LPARAM lParam)
{
	CString str=(wchar_t *)lParam;
	CString strTemp;
	GetDlgItemText(IDC_EDIT_RECV,strTemp);
	str+=L"\r\n";
	str+=strTemp;
	SetDlgItemTextW(IDC_EDIT_RECV,str);

	return TRUE;
}

void CChatDlg::OnBtnSend() 
{
	// TODO: Add your control notification handler code here
	DWORD dwIP;
	((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP);//获取IP控件上的IP地址

	SOCKADDR_IN addrTo;
	addrTo.sin_family=AF_INET;
	addrTo.sin_port=htons(6000);
	addrTo.sin_addr.S_un.S_addr=htonl(dwIP);

	CString strSend;
	GetDlgItemTextW(IDC_EDIT_SEND,strSend);

        //注意宽字符长度要翻倍
	sendto(m_socket,(char *)strSend.GetBuffer(),strSend.GetLength()*2+2,0,
		(SOCKADDR*)&addrTo,sizeof(SOCKADDR));
	strSend.ReleaseBuffer();  
	SetDlgItemTextW(IDC_EDIT_SEND,L"");
}



参考《VC深入详解》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值