线程同步一般有三种实现方法:互斥对象(CreateMutex),事件对象(CreateEvent),关键代码段(CriticalSection),下面分别进行介绍
一、互斥对象(CreateMutex)
创建互斥
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTESlpMutexAttributes, // 指向安全属性的指针,NULL表示默认安全性
BOOLbInitialOwner, // 初始化互斥对象的所有者,TRUE表示创建这个互斥对象的线程获得该对象所有权,否则该线程将不获得
LPCTSTRlpName // 指向互斥对象名的指针,NULL表示匿名对象,如果存在命名则下次调用则GetLastError返回ERROR_ALREADY_EXISTS
//返回值:返回创建互斥对象的句柄
);
互斥等待
DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds);
调用该函数后,函数会一直等待,只有在以下两种情况才会返回
1.指定对象变成有信号状态
2.指定的等待时间间隔已过
3.使内部计数器+1
释放互斥
BOOL ReleaseMutex(HANDLE hMutex);//释放指定对象的所有权,使内部计数器-1
#include <Windows.h>
#include <iostream>
using namespace std;
typedef struct _STRUCT_DATA_
{
int id; //用于标识出票id
int tickets;
}_DATA,*_pDATA;
HANDLE g_hMutex;
DWORD WINAPI Fun1Proc(LPVOID lpParam);
DWORD WINAPI Fun2Proc(LPVOID lpParam);
void main()
{
HANDLE hThread1;
HANDLE hThread2;
_DATA stru_data;
stru_data.id = 0;
stru_data.tickets = 100;
g_hMutex = CreateMutex(NULL,FALSE,L"Ticket");
if (g_hMutex)
{
if (ERROR_ALREADY_EXISTS == GetLastError())
{
cout<<"the instance is exist!"<<endl;
return;
}
}
hThread1 = CreateThread(NULL,0,Fun1Proc,&stru_data,0,NULL);
hThread2 = CreateThread(NULL,0,Fun2Proc,&stru_data,0,NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
Sleep(4000);
}
DWORD WINAPI Fun1Proc(LPVOID lpParam)
{
_pDATA data = (_pDATA)lpParam;
while(TRUE)
{
WaitForSingleObject(g_hMutex,INFINITE);
if (data->tickets>0)
{
Sleep(1);
cout<<"id: "<<data->id++<<endl;
cout<<"thread1 sell<< ticket: "<<data->tickets--<<endl;
}
else
break;
ReleaseMutex(g_hMutex);
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParam)
{
_pDATA data = (_pDATA)lpParam;
while(TRUE)
{
WaitForSingleObject(g_hMutex,INFINITE);
if (data->tickets>0)
{
Sleep(1);
cout<<"id: "<<data->id++<<endl;
cout<<"thread2 sell ticket: "<<data->tickets--<<endl;
}
else
break;
ReleaseMutex(g_hMutex);
}
return 0;
}
二、事件对象(CreateEvent)
HANDLECreateEvent(
LPSECURITY_ATTRIBUTESlpEventAttributes,// 安全属性
BOOLbManualReset,// 复位方式,TRUE表示人工重置(需要调用ResetEvent将事件置为无信号状态),FALSE表示自动重置(线程得到所有权后立即置为无信号状态)
BOOLbInitialState,// 初始状态,TRUE为有信号状态,FALSE为无信号状态
LPCTSTRlpName // 对象名称
);
BOOL SetEvent(HANDLE hEvent);//把指定事件对象设置为有信号状态
BOOL ResetEvent(HANDLE hEvent);//把指定事件对象设置为无信号状态
同样,我们以上面示例为基础,采用事件对象进行线程同步
#include <Windows.h>
#include <iostream>
using namespace std;
typedef struct _STRUCT_DATA_
{
int id; //用于标识出票id
int tickets;
}_DATA,*_pDATA;
HANDLE g_hEvent;
DWORD WINAPI Fun1Proc(LPVOID lpParam);
DWORD WINAPI Fun2Proc(LPVOID lpParam);
void main()
{
HANDLE hThread1;
HANDLE hThread2;
_DATA stru_data;
stru_data.id = 0;
stru_data.tickets = 100;
g_hEvent = CreateEvent(NULL,FALSE,FALSE,L"Ticket");
if (g_hEvent)
{
if (ERROR_ALREADY_EXISTS == GetLastError())
{
cout<<"the instance is exist!"<<endl;
return;
}
}
hThread1 = CreateThread(NULL,0,Fun1Proc,&stru_data,0,NULL);
hThread2 = CreateThread(NULL,0,Fun2Proc,&stru_data,0,NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
SetEvent(g_hEvent);
Sleep(4000);
CloseHandle(g_hEvent);
}
DWORD WINAPI Fun1Proc(LPVOID lpParam)
{
_pDATA data = (_pDATA)lpParam;
while(TRUE)
{
WaitForSingleObject(g_hEvent,INFINITE);
if (data->tickets>0)
{
Sleep(1);
cout<<"id: "<<data->id++<<endl;
cout<<"thread1 sell<< ticket: "<<data->tickets--<<endl;
SetEvent(g_hEvent);
}
else
{
SetEvent(g_hEvent);
break;
}
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParam)
{
_pDATA data = (_pDATA)lpParam;
while(TRUE)
{
WaitForSingleObject(g_hEvent,INFINITE);
if (data->tickets>0)
{
Sleep(1);
cout<<"id: "<<data->id++<<endl;
cout<<"thread2 sell ticket: "<<data->tickets--<<endl;
SetEvent(g_hEvent);
}
else
{
SetEvent(g_hEvent);
break;
}
}
return 0;
}
使用事件对象实现线程间同步需要注意区分人工重置事件对象和自动重置事件对象。
当人工重置事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程。在一个线程得到该事件对象之后,操作系统并不会将该事件设置为无信号状态,需要显示调用ResetEvent函数将其设置为无信号状态,否则该对象会一直是有信号状态。
当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度,同时操作系统会将该事件对象设置无信号状态。这样,当对所保护的代码执行完成后,需要调用SetEvent函数将该事件对象设置为有信号状态。
三、关键代码段(CriticalSection)
关键代码段,也称为临界区,工作在用户方式下。它是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);//初始化临界区
//另外还有如下三个函数,分别如下:
EnterCriticalSection //进入临界区
LeaveCriticalSection //离开临界区
DeleteCriticalSection //删除临界区
同理,我们可以采用关键代码段(CriticalSection)来实现上述代码线程同步功能,如下所示:
#include <Windows.h>
#include <iostream>
using namespace std;
typedef struct _STRUCT_DATA_
{
int id; //用于标识出票id
int tickets;
}_DATA,*_pDATA;
CRITICAL_SECTION g_cs;
DWORD WINAPI Fun1Proc(LPVOID lpParam);
DWORD WINAPI Fun2Proc(LPVOID lpParam);
void main()
{
HANDLE hThread1;
HANDLE hThread2;
_DATA stru_data;
stru_data.id = 0;
stru_data.tickets = 100;
hThread1 = CreateThread(NULL,0,Fun1Proc,&stru_data,0,NULL);
hThread2 = CreateThread(NULL,0,Fun2Proc,&stru_data,0,NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
InitializeCriticalSection(&g_cs);
Sleep(4000);
LeaveCriticalSection(&g_cs);
}
DWORD WINAPI Fun1Proc(LPVOID lpParam)
{
_pDATA data = (_pDATA)lpParam;
while(TRUE)
{
EnterCriticalSection(&g_cs);
if (data->tickets>0)
{
Sleep(1);
cout<<"id: "<<data->id++<<endl;
cout<<"thread1 sell<< ticket: "<<data->tickets--<<endl;
LeaveCriticalSection(&g_cs);
}
else
{
LeaveCriticalSection(&g_cs);
break;
}
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParam)
{
_pDATA data = (_pDATA)lpParam;
while(TRUE)
{
EnterCriticalSection(&g_cs);
if (data->tickets>0)
{
Sleep(1);
cout<<"id: "<<data->id++<<endl;
cout<<"thread2 sell ticket: "<<data->tickets--<<endl;
LeaveCriticalSection(&g_cs);
}
else
{
LeaveCriticalSection(&g_cs);
break;
}
}
return 0;
}
三种方式之间的区别:
互斥对象和事件对象都属于内核对象,利用内核对象进行线程同步时,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。
关键代码段工作在用户方式下,同步速度较快,但在使用关键代码段时,很容易进入死锁状态,因为在等待进入关键代码段时无法确定超时值。
使用建议:
在MFC程序中,可以在类的构造函数中调用InitializeCriticalSection函数,在析构函数中调用DeleteCriticalSection函数,在所需保护的代码前面调用EnterCriticalSection函数,在访问完所需保护的资源后,调用LeaveCriticalSection函数。