上一章作者介绍了用户模式下的线程同步,虽然它们有很好的性能,但是他们功能有限并不是能够胜任实际工作中出现的复杂情况,以及特殊的要求,于是Windwos提供了一些用于线程同步的内核对象,它们根据不同的应用场景而设计,满足不同的环境下的线程同步的要求,因为是基于内核对象来完成同步,因此线程同步时可以跨进程的。
这些内核对象包括,事件,可等待计时器,信号量,互斥量。
在具体介绍每个线程同步内核对象之前,需要了解两个基础的用于等待内核对象的函数WaitForSingleObjectEx 和 WaitForMultipleObjects.
DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle, //内核对象句柄
__in DWORD dwMilliseconds //等待的时间、单位:毫秒 如果指定(INFINITE)表示无信号一直等待。
);
返回值
WAIT_ABANDONED (只在对象为互斥量类型时用到, 表示上一个线程因为错误,线程异常终止,内核对象才变为了有信号的)
WAIT_OBJECT_0 (内核对象有信号)
WAIT_TIMEOUT (等待时间到了)
WAIT_FAILED (参数错误,函数调用失败)
DWORD WINAPI WaitForMultipleObjects(
__in DWORD nCount, (多少个要等待的对象)
__in const HANDLE *lpHandles, 内核对象句柄数组
__in BOOL bWaitAll, (是否等待所有对象有信号才返回)
__in DWORD dwMilliseconds (等待时间)
);
该API函数等待一个或者一组线程同步内核对象,由 bWaitAll指定等待方式, bWaitAll == 1在所有对象处于有信号状态返回, bWaitAll=0 只有其中一个处于有信号状态就返回
返回值
WAIT_OBJECT_0 to (WAIT_OBJECT_0 + nCount– 1) (WAIT_OBJECT_0+N)表示第N个对象处于有信号状态在bWaitAll为1时,如果bWaitAll为0 WAIT_OBJECT_0表示所有对象处于有信号状态,成功返回。
WAIT_ABANDONED_0 to (WAIT_ABANDONED_0 + nCount– 1) (如果bWaitAll为1,WAIT_ABANDONED_0表示句柄数组中至少有一个是互斥量对象的句柄,如果bWaitAll为0,那么WAIT_ABANDONED_0+N表示句柄数组中第N个是一个互斥量句柄,上一个线程没有释如果放互斥量的情况下异常终止)
WAIT_TIMEOUT (同WaitForSingleObject)
WAIT_FAILED (WaitForSingleObject)
(一)事件内核对象
事件内核对象在上一章的用户模式的同步对象 CriticalSection 中已经提到,CriticalSection只是用了它的自动重置模式,自动重置模式在WaitForSingleObject获取到有信号后自动将对象设置为无信号状态并返回。
临界区对象在EnterCriticalSection中调用执WaitForSingleObjec行等待,在LeaveCriticalSection中调用SetEvent将事件对象重置为有信号状态。
此外事件内核对象可以使用手动重置模式,在手动模式下,WaitForSingleObject并改变其状态,需要手动调用SetEvent将对象置为有信号状态,或者调用ResetEvent将对象置为无信号状态,在将对象置为有信号状态时,所有使用WaitXX执行等待的线程都可以返回WAIT_OBJECT_0 得到调度。
API:
HANDLE WINAPI CreateEvent(
__in_opt LPSECURITY_ATTRIBUTES lpEventAttributes,
__in BOOL bManualReset, //TRUE自动模式 FALSE手动模式
__in BOOL bInitialState,//初始值是否是有信号的,TRUE (是),FALSE(不是)
__in_opt LPCTSTR lpName
);
BOOL WINAPI SetEvent(
__in HANDLE hEvent
);
BOOL WINAPI ResetEvent(
__in HANDLE hEvent
);
适应:在自动模式下与 临界区对象 互斥量 SRWLock的互斥模式 功能是一样的,都保证对共享资源的访问在同一时间只运行一个线程独占。
区别 :临界区对象在使用内核机制前有个一个旋转锁等待循环,如果在确定对共享资源独占期间占用很少的CPU周期,应该使用临界区对象。
如果区分写共享资源和读共享资源的线程,即存在生产和消费两种类型的线程,使用SRWLock
如果要进行进程间的同步,或者在独占共享资源之后的操作可能占用很多的CPU周期,如等待用户输入并把用户的输入写入共享资源,应该使用事件或者互斥量,这两个内核同步对象,使线程在无信号状态不参与调度,不占用CPU,而不是SRWLock那样循环的使用原子访问和NOP
互斥量和事件对象的区别很小,互斥量允许一个在异常终止后系统自己是否它对对象的占用,从而避免死锁。事件对象则多了一个手动模式,允许多个线程同时得到执行
可等待计时器,类似于SetTimer,它设置一个定时器,并可以设置周期循环触发的时间,在时间到期时所有等待线程可以得到执行(手动模式),或者一个等待线程被唤醒(自动模式)。同时它还可以设置一个APC回调函数,在时间到期时将ACP挂入 SetWaitableTimer 主调线程的APC队列中。
API:
HANDLE WINAPI CreateWaitableTimer(
__in_opt LPSECURITY_ATTRIBUTES lpTimerAttributes,
__in BOOL bManualReset, //自动还是手动模式
__in_opt LPCTSTR lpTimerName //内核对象名字
);
BOOL WINAPI SetWaitableTimer(
__in HANDLE hTimer,
__in const LARGE_INTEGER *pDueTime, //触发的时间,如果为负数表示相对时间,单位纳秒
__in LONG lPeriod, //触发后多久循环触发一次一次
__in_opt PTIMERAPCROUTINE pfnCompletionRoutine, //APC回调函数地址
__in_opt LPVOID lpArgToCompletionRoutine, //回调函数参数
__in BOOL fResume //是否唤醒睡眠中的系统,利用电源管理
);
取消对象的活动状态,不再触发。
BOOL WINAPI CancelWaitableTimer(
__in HANDLE hTimer
);
测试代码 使用自动重置模式,10秒后触发,之后每秒触发一次
#include <iostream>
#include <stdio.h>
#include <windows.h>
#include <thread>
#include <winbase.h>
//using namespace std;
using std::cout;
using std::endl;
HANDLE hTimer = NULL;
volatile LONG g_IsPassed = 0;
void ThreadForTimer(int pParameter)
{
//int ID = *static_cast<int*>(pParameter);
int ID = pParameter;
InterlockedAdd(&g_IsPassed,1);
while(1)
{
cout.flush();
if(WaitForSingleObject(hTimer,INFINITE) == WAIT_OBJECT_0)
{
cout<<"Thread "<<ID<<" has obtained the single Time:"<<GetTickCount()<<endl;
}
else
{
cout<<"Thread exception occurred"<<endl;
}
}
}
int main(int argc, char *argv[])
{
LARGE_INTEGER liDueTime;
liDueTime.QuadPart = -100000000L;
// Create an unnamed waitable timer.
hTimer = CreateWaitableTimer(NULL, FALSE, NULL);
if (NULL == hTimer)
{
printf("CreateWaitableTimer failed (%d)\n", GetLastError());
return 1;
}
printf("Waiting for 10 seconds Time:%d \n ",GetTickCount());
if (!SetWaitableTimer(hTimer, &liDueTime, 1000, NULL, NULL, 0))
{
printf("SetWaitableTimer failed (%d)\n", GetLastError());
return 2;
}
for(int i = 0; i != 5; ++i)
{
new std::thread(ThreadForTimer,i);
while(1)
{
if(InterlockedCompareExchange(&g_IsPassed,0,1) == 1) break;
YieldProcessor();
}
}
SleepEx(INFINITE,TRUE);
return 0;
}
运行结果
hTimer = CreateWaitableTimer(NULL, FALSE, NULL); 第二个参数改为TRUE 手动模式后
每个线程一直都处于可调度状态
可以等待计数器内核对象的适用情况非常的明确,当需要一个定时器在某和时间点唤醒等待线程,或者按一定时间循环唤醒时。
(三)信号量内核对象
信号量内核对象用来对资源进行计数,在需要对资源使用做限制的时候信号量非常有用,在内部它存放了一个当前可用资源数的变量,和一个最大资源数的变量,每个等待线程使用等待函数时,'如果可以资源数大于0,将其值减一并返回,如果可以资源数已经为0则等待函数进入睡眠,可以通过ReleaseSemaphore增加可以用资源数量,对计数访问修改都是使用的原子操作,保证了计数在多线程下不被破坏。
API:
HANDLE WINAPI CreateSemaphore(
__in_opt LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
__in LONG lInitialCount, 可用数量
__in LONG lMaximumCount, 最大数量
__in_opt LPCTSTR lpName
);
BOOL WINAPI ReleaseSemaphore(
__in HANDLE hSemaphore,
__in LONG lReleaseCount, 增加多少
__out_opt LPLONG lpPreviousCount
);
测试代码:
#include <iostream>
#include <stdio.h>
#include <windows.h>
#include <thread>
#include <winbase.h>
#include <string>
#include <random>
#include <vector>
//using namespace std;
using std::cout;
using std::endl;
HANDLE g_hSemaphore;
HANDLE g_hEvent;
std::vector<int> vtrBuffer;
void ThreadForSemaphoreInc()
{
std::random_device rd;
while(1)
{
if(WaitForSingleObject(g_hEvent,INFINITE) == WAIT_OBJECT_0)
{
for(int i = 0; i !=10; ++i)
{
vtrBuffer.push_back(rd()%100);
}
ReleaseSemaphore(g_hSemaphore,10,NULL);
SetEvent(g_hEvent);
}
cout<<"The buffer has been full"<<endl<<endl;
Sleep(5*1000);
}
}
void ThreadForSemaphoreDec()
{
HANDLE handles[2] = {g_hEvent,g_hSemaphore};
while(1)
{
if(WaitForMultipleObjects(2,handles,TRUE,INFINITE) == WAIT_OBJECT_0)
{
cout<<"Decreasing the buffer,the popuped value is "<<vtrBuffer.back()<<endl;
vtrBuffer.pop_back();
SetEvent(g_hEvent);
}
}
}
int main(int argc, char *argv[])
{
g_hEvent = CreateEvent(NULL,FALSE,TRUE,NULL);
if(!g_hEvent) cout<<"CreateEvent failed and the error code is "<<GetLastError()<<endl;
g_hSemaphore = CreateSemaphore(NULL,0,10,NULL);
if(!g_hEvent) cout<<"CreateSemaphore failed and the error code is "<<GetLastError()<<endl;
new std::thread(ThreadForSemaphoreInc);
new std::thread(ThreadForSemaphoreDec);
Sleep(1000*1000);
return 0;
}
运行结果
(四)互斥量对象
互斥量内核对象用来确保一个线程独占对一个资源的访问。互斥量对象包含一个使用计数,线程ID,和一个递归计数。互斥量与关键段的行为完全相同,但互斥量是内核对象,而关键段是用户模式下的同步对象。
互斥量规则:
(1)如果线程ID为0(无效线程ID),那么该互斥量不为任何线程占用,它处于触发在状态
(2)如果线程ID为非零值,那么有一个线程已经占用了该互斥量,它处于未触发在状态
(3)与其他内核对象不同,操作系统对互斥量进行了特殊处理,允许它们违反一些常规的规则。
为了获得对被保护资源的访问权,线程要调用等待函数并传入互斥量句柄。在内部,等待函数会检查线程ID是否为0,如果为0,等待线程将互斥量对象线程ID设为当前线程
ID,递归计数为1。否则,主调线程将会被挂起。当其他线程完成对保护资源的互斥访问,释放对互斥量的占有时,互斥量的线程ID被设为0,原来被挂起的线程变为可调度状态,并将互斥量对象对象ID设为此线程ID,递归计数为1。
前面一直提到递归计数,却没有解释它的意思。当线程试图等待一个未触发的互斥量对象,此时通常处于等待状态。但是系统会检查想要获得互斥量的线程的线程ID与互斥量对象内部记录的线程ID是否相同。如果相同,那么系统会让线程保持可调度状态,即使该互斥量尚未触发。每次线程等待成功一个互斥量,互斥对象的递归计数就会被设为1。因此,使递归对象大于1 的唯一途径是让线程多次等待同一个互斥量。
当目前占有互斥量的线程不再需要访问互斥资源时,它必须调用ReleaseMutex来释放互斥量。
API:
HANDLE WINAPI CreateMutex(
__in_opt LPSECURITY_ATTRIBUTES lpMutexAttributes,
__in BOOL bInitialOwner,
__in_opt LPCTSTR lpName
);
BOOL WINAPI ReleaseMutex(
__in HANDLE hMutex
);
(五)各种同步对象的概括
每种同步内核对象都有个OpenXX版本的函数,用来打开已经创建过的对象,如OpenMutex,OpenEvent,
每种创建同步内核对象的函数都有一个对应的后缀为Ex,的版本 如CreateSemaphoreEx,CreateEventEx, 带后缀的版本多了一个__in DWORD dwDesiredAccess参数,用来设置创建的对象可以用什么访问权限来使用。
每种同步内核对象都是可以具名的,所有的内核对象名字同在一个命名空间内,如果要创建的对象名字已存在,创建函数失败,GetLastError 返回ERROR_ALREADY_EXISTS
每种同步内核对象在不使用的时候都要调用CloseHandle关闭,不然会造成内核对象泄露。
每种同步内核对象都可以拥有不同进程间的线程同步,可以使用DuplicationHandle,句柄继承,打开同名对象的方式,让两个进程中各自的线程引用同一个内核对象来完成同步工作。
(六)其他等待函数
DWORD WaitForInputIdle(
HANDLE hProcess, // handle to process
DWORD dwMilliseconds // time-out interval
);
用于等待一个进程,第一次创建窗口。
DWORD WINAPI MsgWaitForMultipleObjects(
__in DWORD nCount,
__in const HANDLE *pHandles,
__in BOOL bWaitAll,
__in DWORD dwMilliseconds,
__in DWORD dwWakeMask
);
线程在等待内核对象变为有信号的同时,如果有窗口消息到来,线程也可以变为可调度状态。
BOOL WaitForDebugEvent(
LPDEBUG_EVENT lpDebugEvent, // debug event information
DWORD dwMilliseconds // time-out value
);
被调试进程有调试事件时触发,否则一直等待
DWORD WINAPI SignalObjectAndWait(
__in HANDLE hObjectToSignal,
__in HANDLE hObjectToWaitOn,
__in DWORD dwMilliseconds,
__in BOOL bAlertable
);
将一个对象置为有信号的同时,等待另一个对象,原子操作在内部。