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