上篇讲到了应用层下的线程同步,接下来看下内核模式下的几种同步方式。
第一种方式称为互斥量。互斥量呢是一个内核对象,它可以确保一个线程对一个资源的独占访问。互斥量和关键段(上篇提到的)的行为是一样的,区别在于一个是内核对象,一个是应用层的对象。因此,互斥量要比关键段慢很多,但是互斥量可以进行跨进程的同步,而关键段并不行。
那么,如何创建一个互斥量呢,MSDN提供了一个名叫做CreateMutex的API,用于创建一个互斥量。其声明如下:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, // 安全属性,一般为NULL
BOOL bInitialOwner, // 创建后是否拥有该互斥量
LPCTSTR lpName // 对象名
);
接下来看一个非常重要的函数,名为WaitForSingleObject,用于等待一个对象是否有信号,其声明如下:
DWORD WaitForSingleObject(
HANDLE hHandle, // 要等待的对象
DWORD dwMilliseconds // 等待多长时间
);
在windows中,内核对象有两种状态,一种是有信号的,一种是无信号的。当我们在程序中调用WaitForSingleObject时,若等待的对象是无信号的,那么程序就会阻塞在这里,直到该对象变成有信号或者等待时间到了,若我们在第二个参数传了一个INFINITE(-1)参数时,表示无限等待,若等待的对象一直是无信号的,那么程序将一直阻塞在这里,幸运的是,WaitForSingleObject并不会浪费CPU的时间片。
互斥量其实用的并不是很多,因为它可以进行跨进程的进行同步,那么我们可以用它来进行防止程序的多开,以下是最简单的防多开的代码:
#include <stdio.h>
#include <windows.h>
int _tmain(int argc, _TCHAR* argv[])
{
//创建一个互斥量
HANDLE hMutex = CreateMutex(NULL, TRUE, L"testName");
//若该互斥量已经存在了,则直接退出
if (ERROR_ALREADY_EXISTS == GetLastError())
{
printf("only one instance can run this mode\n");
return -1;
}
printf("dong something.......");
system("pause");
return 0;
}
第二种进行同步的方式叫做事件内核对象。
事件内核对象呢,它有两种类型,第一种是自动重置事件,第二种是手动重置事件.。两者之间的区别在于,当手动重置有信号的时候呢,等待该事件的所有线程都变成可调度的状态,而当自动重置的时候,只有所有等待线程中的一个才能变成可调度的状态。
创建事件,需要调用CreateEvent,其声明如下:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性,一般为NULL
BOOL bManualReset, // 是否手动重置
BOOL bInitialState, // 初始的状态,有信号或无信号
LPCTSTR lpName // 对象名
);
如果是手动重置的事件,我们应该将调用ResetEvent和SetEvent来将事件变为无信号和有信号,它们的声明如下:
BOOL ResetEvent(
HANDLE hEvent // 事件对象
);
BOOL SetEvent(
HANDLE hEvent // 事件对象
);
事件通常的用法,就是一个线程要执行,必须先让另一个线程执行完,以下是一个简单的测试代码:
#include <stdio.h>
#include <windows.h>
DWORD WINAPI TestThread(LPVOID lpParam)
{
//等待事件对象变成有信号状态
WaitForSingleObject(lpParam, INFINITE);
printf("sub thread do something...\n");
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
//创建一个事件对象
HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, L"testName");
//检查多开
if (ERROR_ALREADY_EXISTS == GetLastError())
{
printf("only one instance can run this mode\n");
return -1;
}
//创建一个线程
HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)TestThread, hEvent, NULL, NULL);
printf("main thread do something...\n");
//休息一秒,手动重置事件对象变成有信号,让TestThread运行
Sleep(1000);
SetEvent(hEvent);
system("pause");
return 0;
}
第三种同步的方式叫做信号量,信号量和其它的内核对象有点不同,我们先来看下创建信号量的API,CreateSemaphore,声明如下:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 安全属性,一般为NULL
LONG lInitialCount, // 初始的个数
LONG lMaximumCount, // 最大个数
LPCTSTR lpName // 对象名
);
如果当前的计数大于0,信号量将处于有信号的状态,若等于0,信号量处于无信号的状态。
我们可以调用ReleaseSemaphore来递增信号量当前的计数,其声明如下:
BOOL ReleaseSemaphore(
HANDLE hSemaphore, // 信号量对象
LONG lReleaseCount, // 增加的个数
LPLONG lpPreviousCount // 原先的计数,用不到给NULL
);
以下是信号量的简单测试代码:
#include <stdio.h>
#include <windows.h>
DWORD WINAPI TestThread(LPVOID lpParam)
{
//等待事件对象变成有信号状态
WaitForSingleObject(lpParam, INFINITE);
printf("sub thread do something...\n");
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
//创建一个信号量,初始个数是1个
HANDLE hSema = CreateSemaphore(NULL, 1, 10, L"testName");
if (ERROR_ALREADY_EXISTS == GetLastError())
{
printf("only one instance can run this mode\n");
return -1;
}
//由于当前个数1大于0,所以是有信号的,直接运行下面代码,且当前个数变为0,即无信号状态
WaitForSingleObject(hSema, INFINITE);
printf("main thread do something...\n");
//创建一个线程
HANDLE hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)TestThread, hSema, NULL, NULL);
//休息1秒后,当前个数变为1,有信号,子线程运行
Sleep(1000);
ReleaseSemaphore(hSema, 1, NULL);
system("pause");
return 0;
}