四、内核同步对象—— 互斥(Mutex)
内核线程同步对象包括互斥、信号量和事件,它们使用时存在很多共通之处。下面先介绍在Windows平台利用互斥进行线程的同步。HANDLE WINAPI CreateMutex(
__in_opt LPSECURITY_ATTRIBUTES lpMutexAttributes,
__in BOOL bInitialOwner,
__in_opt LPCTSTR lpName
);
创建互斥对象并返回句柄值。
lpMutexAttributes —— 是互斥对象的安全属性,一般情况下设置为NULL即可。
bInitialOwner —— 标志互斥对象是否属于本线程的标志量,如果设置为True,互斥对象将被置为non-signaled无信号状态;如果设置为False,互斥对象将被置为signaled有信号状态。
lpName —— 设置互斥名称,若置为NULL,则设置无名互斥变量。
HRESULT CloseHandle(
HANDLE hHandle
);
关闭互斥对象,回收内存资源。这是关闭所有内核对象的通用API,接收参数为内核对象的句柄值。
DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,
__in DWORD dwMilliseconds
);
我们利用WaitForSingleObject进行互斥对象的加锁,和其他内核对象同步技术加锁通用。WaitForSingleObject在内核对象signaled时返回;而互斥量在WaitForSingleObject返回后其自动进入non-signaled状态,因为互斥量是“auto-reset”模式的内核对象。
BOOL WINAPI ReleaseMutex(
__in HANDLE hMutex
);
解锁互斥对象,接收参数为互斥对象句柄。
我们利用互斥对试验代码进行同步后,如下:
#include "stdafx.h"
#include "windows.h"
#include "iostream"
int g_i=0;
HANDLE mutex;
unsigned WINAPI AddThreadFunc(void* param)
{
WaitForSingleObject(mutex,INFINITE);
for (int i=0;i<1000000;i++)
{
g_i++;
}
ReleaseMutex(mutex);
return 0;
}
unsigned WINAPI DevThreadFunc(void* param)
{
WaitForSingleObject(mutex,INFINITE);
for (int i=0;i<1000000;i++)
{
g_i--;
}
ReleaseMutex(mutex);
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
mutex=CreateMutex(NULL,false,NULL);
HANDLE handles[2];
handles[0] = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)AddThreadFunc,NULL,0,NULL);
handles[1] = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)DevThreadFunc,NULL,0,NULL);
WaitForMultipleObjects(2,handles,true,INFINITE);
std::cout<<"g_i="<<g_i<<std::endl;
CloseHandle(mutex);
return 0;
}
五、内核同步对象—— 信号量(Semaphore)
类似于Linux中的信号量,Windows也提供内核级别的信号量。信号量创建时使用CreateSemaphore API:HANDLE WINAPI CreateSemaphore(
__in_opt LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
__in LONG lInitialCount,
__in LONG lMaximumCount,
__in_opt LPCTSTR lpName
);
lpSemaphoreAttributes —— 信号量的安全属性,设为NULL采用默认属性。
lInitialCount —— 信号量的初始值,不能小于0。
lMaximumCount —— 信号量的最大值。
lpName —— 信号量的名称,若设置为NULL,创建无名的信号量。
BOOL WINAPI ReleaseSemaphore(
__in HANDLE hSemaphore,
__in LONG lReleaseCount,
__out_opt LPLONG lpPreviousCount
);
hSemaphore —— 信号量的句柄值。
lReleaseCount —— 释放信号量的数值,我们知道信号量数值是可以大于1的,而这个参数就是本次释放的信号量的值,大于等于0。
lpPreviousCount —— 用来接收释放前的信号量的数值的指针,如果不需要,置为NULL即可。
将试验代码用信号量进行同步如下:#include "stdafx.h"
#include "windows.h"
#include "iostream"
int g_i=0;
HANDLE sema;
unsigned WINAPI AddThreadFunc(void* param)
{
WaitForSingleObject(sema,INFINITE);
for (int i=0;i<1000000;i++)
{
g_i++;
}
ReleaseSemaphore(sema,1,NULL);
return 0;
}
unsigned WINAPI DevThreadFunc(void* param)
{
WaitForSingleObject(sema,INFINITE);
for (int i=0;i<1000000;i++)
{
g_i--;
}
ReleaseSemaphore(sema,1,NULL);
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
sema = CreateSemaphore(NULL,1,1,NULL);
HANDLE handles[2];
handles[0] = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)AddThreadFunc,NULL,0,NULL);
handles[1] = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)DevThreadFunc,NULL,0,NULL);
WaitForMultipleObjects(2,handles,true,INFINITE);
std::cout<<"g_i="<<g_i<<std::endl;
CloseHandle(sema);
return 0;
}
六、内核同步对象 —— 事件(Event)
事件是Windows中不同于Linux,独有的一个线程同步机制。事件也属于内核同步对象,但是它却略不同于前面介绍过的互斥和信号量。主要表现在事件在创建时可选auto-reset模式和manual-reset模式。当创建auto-reset模式的事件时,它和互斥和信号量一样,WaitForSingleObject返回时,自动进入non-signaled状态;但是当创建manual-reset模式的事件时,在WaitForSingleObject返回后,它不会自动进入non-signaled状态,需要我们手动进行reset。
我们先来看下创建事件的API:HANDLE WINAPI CreateEvent(
__in_opt LPSECURITY_ATTRIBUTES lpEventAttributes,
__in BOOL bManualReset,
__in BOOL bInitialState,
__in_opt LPCTSTR lpName
);
lpEventAttributes —— 事件的默认安全属性,默认填写NULL即可。
bManualReset —— 创建事件的模式。若为True,创建manual-reset模式的事件;若为False,创建auto-reset模式的事件。
bInitialState —— 创建事件的初始化状态。若为True,创建完成后,事件被置为signaled状态;若为Flase,创建完成后,事件被置为non-signaled状态。
lpName —— 创建的事件名称,若为NULL,创建无名事件。
函数返回创建的事件句柄值。当我们创建manual-reset模式的事件时,需要我们手动改变事件的状态,我们需要用到以下的API:ResetEvent(
__in HANDLE hHandle
)
ResetEvent将事件置为non-signaled状态。
SetEvent(
__in HANDLE hHandle
)
SetEvent将事件置为signaled状态。
我们用下面的示例代码演示事件的用法,用于auto-reset模式的事件用法和互斥以及信号量一致,不再演示。这里只演示manual-reset模式的事件的用法。
#include "stdafx.h"
#include "windows.h"
#include "iostream"
int g_i=0;
HANDLE evt;
unsigned WINAPI AddThreadFunc(void* param)
{
WaitForSingleObject(evt,INFINITE);
ResetEvent(evt);
for (int i=0;i<1000000;i++)
{
g_i++;
}
SetEvent(evt);
return 0;
}
unsigned WINAPI DevThreadFunc(void* param)
{
WaitForSingleObject(evt,INFINITE);
ResetEvent(evt);
for (int i=0;i<1000000;i++)
{
g_i--;
}
SetEvent(evt);
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
evt=CreateEvent(NULL,true,false,NULL);
HANDLE handles[2];
handles[0] = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)AddThreadFunc,NULL,0,NULL);
handles[1] = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)DevThreadFunc,NULL,0,NULL);
SetEvent(evt);
WaitForMultipleObjects(2,handles,true,INFINITE);
std::cout<<"g_i="<<g_i<<std::endl;
return 0;
}
第32行,我们创建了一个manal-reset模式的事件,事件的起始的状态是non-signaled状态。
第36行,我们利用SetEvent将事件的状态由non-signaled改为signaled,这样WaitSingleObjec函数才能从阻塞状态返回。
第10行和21行,在两个线程函数中调用WaitSingleObjec等待事件变为signaled状态。
第11行和22行,利用ResetEvent将事件的状态由signaled变成non-signaled状态,这就实现了类似互斥和信号量中加锁功能。
第10行和第27行,利用SetEvent将事件的状态由non-signaled变成signaled,类似于互斥和信号量中的解锁的功能。
执行上述代码,我们也得到了期望的结果:
七、用户模式同步和内核模式同步的对比
用户模式同步:
1、由于不涉及和操作系统内核状态的切换,效率较高。
内核模式同步:
1、 和系统内核打交道,同步对象功能较多。
2、 可以设置超时等待。
Github位置:
https://github.com/HymanLiuTS/NetDevelopment
克隆本项目:
git clone git@github.com:HymanLiuTS/NetDevelopment.git
获取本文源代码:
git checkout NL48