您的位置:首页 > 编程语言

Windows核心编程笔记(九)使用内核对象进行线程同步

2017-01-13 04:52 609 查看
上一章作者介绍了用户模式下的线程同步,虽然它们有很好的性能,但是他们功能有限并不是能够胜任实际工作中出现的复杂情况,以及特殊的要求,于是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
);


将一个对象置为有信号的同时,等待另一个对象,原子操作在内部。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: