C++多线程编程

4 篇文章 0 订阅

1. 多线程的意义

程序是计算机指令的集合,它以文件的形式存储在磁盘上。进程通常被定义为一个正在运行的程序的实例,系统赋予每个进程独立的地址空间。进程从不执行任何东西,负责执行代码的是线程,创建进程时,操作系统会自动为该进程创建一个主线程。

多线程程序相对于多进程程序,主要有两个优点:

(1)共享一个虚拟地址空间,占用资源较少

(2)线程间切换只是执行环境的切换,执行效率较高

2. C++中多线程的实现

C++中创建线程利用函数CreateThread实现,线程的任务函数作为其中一个参数传入。线程的任务函数具有固定的格式,可以有一个指针参数,如果需要传递的参数比较复杂,可以通过封装成某个结构体取地址传入。

#include <Windows.h>
#include <iostream>

using namespace std;

DWORD WINAPI ThreadProc(LPVOID lpParameter);
int main()
{
	// 构造线程参数
	char lpParameter[] = "Hello MultiThread";
	// 创建线程
	HANDLE hThread = CreateThread(NULL, 0, ThreadProc, (LPVOID)lpParameter, NULL, NULL);
	// 去掉主线程对线程的引用
	CloseHandle(hThread);

	cout << "Main is running!" << endl;
	return 0;
}

// 线程入口函数
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	cout << "ThreadProc is running!" << endl;
	return 0;
}

3. 线程同步的方法

多个线程间需要共享一些资源(比如内存、文件或数据库等)时,为避免发生访问冲突,应该在各个线程间进行一个同步处理,让各个线程“排队”操作共享资源。

(1)互斥对象

互斥对象属于内核对象,包含一个使用数量,一个线程ID和一个计数器。其中,线程ID用于标识系统中哪个线程当前拥有互斥对象的,计数器用于指明该线程拥有互斥对象的次数。互斥对象的创建,通过调用函数CreateMutex实现。

下面通过火车站售票的程序说明互斥对象的使用方法:

#include <Windows.h>
#include <iostream>

using namespace std;

// 线程入口函数
DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
// 共享资源
int tickets = 100;
// 互斥对象
HANDLE hMutex;

int main()
{
	// 创建线程
	HANDLE hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, NULL, NULL);
	HANDLE hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, NULL, NULL);

	// 去掉主线程对线程的引用
	CloseHandle(hThread1);
	CloseHandle(hThread2);

	// 创建互斥对象
	hMutex = CreateMutex(NULL, FALSE, NULL);

	// 主线程睡眠4秒
	Sleep(4000);

	// 关闭互斥对象句柄
	CloseHandle(hMutex);

	return 0;
}

// 线程1入口函数
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
	while (TRUE)
	{
		// 请求互斥对象所有权
		WaitForSingleObject(hMutex, INFINITE);

		if (tickets > 0)
		{
			cout << "Thread-1 sell ticket ------ " << tickets-- << endl;
		}
		else
		{
			break;
		}

		// 释放互斥对象的所有权
		ReleaseMutex(hMutex);
	}

	return 0;
}

// 线程2入口函数
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
	while (TRUE)
	{
		// 请求互斥对象所有权
		WaitForSingleObject(hMutex, INFINITE);

		if (tickets > 0)
		{
			cout << "Thread-2 sell ticket ------ " << tickets-- << endl;
		}
		else
		{
			break;
		}

		// 释放互斥对象的所有权
		ReleaseMutex(hMutex);
	}

	return 0;
}

(2)事件对象

互斥对象也属于内核对象,包含一个使用数量,一个人工重置标识和一个当前状态标识。其中,人工重置标识用于标识事件对象是否为人工重置事件对象,当前状态标识用于事件对象是否处于可以被请求的空闲状态。人重置事件对象和自动重置事件对象的区别是:当人工重置事件对象处于空闲状态时,所有请求事件对象的线程都可以获得事件对象的使用权,因为线程请求到事件对象以后并不会改变其空闲状态;当自动重置事件对象处于空闲状态时,请求事件对象的线程中只能有一个线程获得事件对象的使用权,因为线程请求到事件对象以后会将其置为非空闲状态。

互斥对象的创建,通过调用函数CreateEvent实现。

下面通过火车站售票的程序说明事件对象的使用方法:

#include <Windows.h>
#include <iostream>

using namespace std;

// 线程入口函数
DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
// 共享资源
int tickets = 100;
// 事件对象
HANDLE hEvent;

int main()
{
	// 创建线程
	HANDLE hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, NULL, NULL);
	HANDLE hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, NULL, NULL);

	// 去掉主线程对线程的引用
	CloseHandle(hThread1);
	CloseHandle(hThread2);

	// 创建自动重置事件对象
	hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);

	// 主线程睡眠4秒
	Sleep(4000);

	// 关闭事件对象句柄
	CloseHandle(hEvent);

	return 0;
}

// 线程1入口函数
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
	while (TRUE)
	{
		// 请求事件对象所有权
		WaitForSingleObject(hEvent, INFINITE);

		if (tickets > 0)
		{
			cout << "Thread-1 sell ticket ------ " << tickets-- << endl;

			// 释放事件对象所有权
			SetEvent(hEvent);
		}
		else
		{
			// 释放事件对象所有权
			SetEvent(hEvent);

			break;
		}
	}

	return 0;
}

// 线程2入口函数
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
	while (TRUE)
	{
		// 请求事件对象所有权
		WaitForSingleObject(hEvent, INFINITE);

		if (tickets > 0)
		{
			cout << "Thread-2 sell ticket ------ " << tickets-- << endl;

			// 释放事件对象所有权
			SetEvent(hEvent);
		}
		else
		{
			// 释放事件对象所有权
			SetEvent(hEvent);

			break;
		}
	}

	return 0;
}


(3)关键代码段

关键代码段(又称临界区),指的是对一小段代码进行访问控制的结构,工作在用户模式下。使用前,应事先调用函数InitializeCriticalSection初始化临界区对象;使用后,应调用函数DeleteCriticalSection临界区对象所占用的资源;使用过程中,先利用函数EnterCriticalSection请求进入关键代码段并获取控制权,执行完关键代码段相应代码后再调用函数LeaveCriticalSection离开关键代码段并交出控制权。

下面通过火车站售票的程序说明事件对象的使用方法:

#include <Windows.h>
#include <iostream>

using namespace std;

// 线程入口函数
DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
// 共享资源
int tickets = 100;
// 临界区对象
CRITICAL_SECTION cs;

int main()
{
	// 创建线程
	HANDLE hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, NULL, NULL);
	HANDLE hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, NULL, NULL);

	// 去掉主线程对线程的引用
	CloseHandle(hThread1);
	CloseHandle(hThread2);

	// 初始化临界区对象
	InitializeCriticalSection(&cs);

	// 主线程睡眠4秒
	Sleep(4000);

	// 释放临界区对象
	DeleteCriticalSection(&cs);

	return 0;
}

// 线程1入口函数
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
	while (TRUE)
	{
		// 请求临界区对象所有权,以进入关键代码段
		EnterCriticalSection(&cs);

		if (tickets > 0)
		{
			// 释放临界区对象所有权,并离开关键代码段
			LeaveCriticalSection(&cs);

			cout << "Thread-1 sell ticket ------ " << tickets-- << endl;
		}
		else
		{
			// 释放临界区对象所有权,并离开关键代码段
			LeaveCriticalSection(&cs);

			break;
		}
	}

	return 0;
}

// 线程2入口函数
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
	while (TRUE)
	{
		// 请求临界区对象所有权,以进入关键代码段
		EnterCriticalSection(&cs);

		if (tickets > 0)
		{
			// 释放临界区对象所有权,并离开关键代码段
			LeaveCriticalSection(&cs);

			cout << "Thread-2 sell ticket ------ " << tickets-- << endl;
		}
		else
		{
			// 释放临界区对象所有权,并离开关键代码段
			LeaveCriticalSection(&cs);

			break;
		}
	}

	return 0;
}


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

利用命名互斥对象或命名事件对象,可以实现应用程序只有一个实例运行。原理是:创建内核对象时,如果具有该名称的内核对象已经存在,函数会返回已经创建的内核对象的句柄;这时,如果调用GetLastError函数,会返回ERROR_ALREADY_EXISTS。具体实现如下:

	// 利用互斥对象保证单实例
	hMutex = CreateMutex(NULL, TRUE, "NamedObject");
	if (hMutex)
	{
		if (ERROR_ALREADY_EXISTS == GetLastError())
		{
			cout << "Only one instance can run!" << endl;
			return;
		}
	}

	// 利用互斥对象保证单实例
	hEvent = CreateEvent(NULL, FALSE, TRUE, "NamedObject");
	if (hEvent)
	{
		if (ERROR_ALREADY_EXISTS == GetLastError())
		{
			cout << "Only one instance can run!" << endl;
			return;
		}
	}


5. 基于MFC的多线程聊天程序的编写


(1)加载套接字库

利用MFC中的AfxSocketInit函数可以初始化1.1版本的套接字库。如果需要用到其它版本的套接字库中的函数,就需要调用WSAStartup函数来初始化指定版本的套接字库。

// 初始化套接字库
BOOL MySocketInit(WORD wVersionRequested)
{
	WSADATA wsaData;
	if (0 != WSAStartup(wVersionRequested, &wsaData))
	{
		return FALSE;
	}

	// 检验是否为请求的版本号
	if (wsaData.wVersion != wVersionRequested)
	{
		WSACleanup();
		return FALSE;
	}

	return TRUE;
}


(2)创建并初始化套接字

Windows套接字在两种模式下执行IO操作,即阻塞模式和非阻塞模式。在阻塞模式下,在IO操作完成前,执行操作的Winsock函数会一直等待下去。在非阻塞模式下,Winsock函数会立即返回,在该函数执行的操作完成后,系统会采用某种方式把操作结果通知给调用线程。

在编写Windows平台下的网络通信应用程序时,既可以采用Unix标准的socket系列函数,以多线程的形式克服阻塞的影响,又可以采用Windows扩展的WSASocket系列函数,工作在基于异步消息的非阻塞模式。

// 创建并初始化套接字
BOOL InitSocket()
{
	// 创建异步套接字
	m_socket = WSASocket(AF_INET, SOCK_DGRAM, 0, NULL, 0, 0);
	if (INVALID_SOCKET == m_socket)
	{
		return FALSE;
	}

	// 绑定套接字至本地
	SOCKADDR_IN addrSock;
	addrSock.sin_family = AF_INET;
	addrSock.sin_addr.S_un.S_addr = htonl(ADDR_ANY);
	addrSock.sin_port = htons(6000);
	if (SOCKET_ERROR == bind(m_socket, (SOCKADDR *)&addrSock, sizeof(SOCKADDR)))
	{
		return FALSE;
	}

	// 注册网络读取事件
	if (SOCKET_ERROR == WSAAsyncSelect(m_socket, m_hWnd, UM_SOCK, FD_READ))
	{
		return FALSE;
	}

	return TRUE;
}


(3)实现接收端功能

// 响应UM_SOCK消息,接收并处理消息
LRESULT OnSock(WPARAM wParam, LPARAM lParam)
{
	switch (LOWORD(lParam))
	{
	case FD_READ:
		{
			// 接收数据
			WSABUF wsaBuf;
			wsaBuf.buf = new char[200];
			wsaBuf.len = 200;
			DWORD dwRead;
			DWORD dwFlag = 0;
			SOCKADDR_IN addrFrom;
			int len = sizeof(SOCKADDR); // 必须赋值,否则会导致接收失败
			if (SOCKET_ERROR == WSARecvFrom(m_socket, &wsaBuf, 1, &dwRead, &dwFlag,
				(SOCKADDR *)&addrFrom, &len, NULL, NULL))
			{
				delete wsaBuf.buf;
				return FALSE;
			}

			// 处理成功接收的数据
			CString str;
			str.Format("%s say: %s", inet_ntoa(addrFrom.sin_addr), wsaBuf.buf);
			str += "\r\n";
			CString strTemp;
			GetDlgItemText(IDC_EDIT_RECV, strTemp);
			str += strTemp;
			SetDlgItemText(IDC_EDIT_RECV, str);

			delete wsaBuf.buf;
		}
		break;
	default:
		break;
	}

	return TRUE;
}


(4)实现发送端功能

// 完成消息发送
void OnBtnSend()
{
	WSABUF wsaBuf;
	CString str;
	GetDlgItemText(IDC_EDIT_SEND, str);
	wsaBuf.buf = str.GetBuffer(str.GetLength());
	wsaBuf.len = str.GetLength() + 1;
	DWORD dwSend;
	DWORD dwIP;
	((CIPAddressCtrl *)GetDlgItem(IDC_IPADDRESS_TO))->GetAddress(dwIP);
	SOCKADDR_IN addrTo;
	addrTo.sin_family = AF_INET;
	addrTo.sin_addr.S_un.S_addr = htonl(dwIP);
	addrTo.sin_port = htons(6000);
	if (SOCKET_ERROR == WSASendTo(m_socket, &wsaBuf, 1, &dwSend, 0, 
		(SOCKADDR *)&addrTo, sizeof(SOCKADDR), NULL, NULL))
	{
		SetDlgItemText(IDC_EDIT_SEND, "");
		return;
	}
	SetDlgItemText(IDC_EDIT_SEND, "");
}


(5)释放套接字和套接字库占用的资源

// 释放套接字和套接字库相关资源
void ReleaseSocket()
{
	// 关闭套接字,释放其相关资源
	if (m_socket)
	{
		closesocket(m_socket);
	}

	// 终止套接字库的使用
	WSACleanup();
}




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值