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

Windows核心编程 用内核对象进行线程同步

2015-04-29 16:54 519 查看
用户模式的线程同步机制:
优点:速度快,可保持在用户模式,无需切换到内核模式。
缺点:不适用于许多应用程序,不支持多个进程间的同步。
内核对象的唯一缺点就是性能:X86上,一个空的系统调用会占约200个CPU周期(原因是用户模式切换到内核模式时,伴随调度新线程而来的刷新高速缓存以及错过高速缓存(未命中))。
几乎所有的内核对象都可以进行线程同步。这些内核对象都包括两种状态:一种是触发状态,另一种是未触发状态。例如,进程和线程对象在刚创建的时候是未触发状态,当进程和线程终止时就变成了触发状态。此状态转换是不可逆的。之所以能进行从未触发到触发的转变是因为内核对象内部有一个布尔变量。系统创建内核对象时会把它的初始值设为false,表示未触发。内核对象状态的切换就是对此值进行切换。
一些内核对象:
进程
线程
作业
文件以及控制台的标准输入流/输出流/错误流
事件
可等待的计时器(waitabletimer)
信号量
互斥量
9.1等待函数:
等待函数使一个线程进入等待状态,直到它所等待的内核对象被触发为止。
WaitForSingleObject系列等待函数。内部为原子操作
DWORD
WaitForSingleObject
(
HANDLE
hHandle,
//handletoobject
DWORD
dwMilliseconds
//time-outinterval毫秒,一般传入INFINITE(OXFFFFFFFF
-1)
);
返回值:

ValueMeaning
WAIT_FAILED传入无效参数,可调用GetLastError查看
WAIT_OBJECT_0等待的对象被触发
WAIT_TIMEOUT时间耗尽
DWORD
WaitForMultipleObjects(//对多个对象操作

DWORD
nCount,
//numberofhandlesinarray
最大为
MAXIMUM_WAIT_OBJECTS
CONST
HANDLE
*lpHandles,
//object-handlearray

BOOL
bWaitAll,
//waitoption
true所有都触发false一个触发就返回

DWORD
dwMilliseconds
//time-outinterval);

bWaitAll为true时,返回值同上,为false时,当对象被触发时,返回WAIT_OBJECT_0+dwCount-1间的任何一个值。例:

HANDLE
h[3];

h[0]=hProcess1;

h[1=hProcess2;

h[2]=hProcess3;

DWORD
dw=WaitForMultipleOBjecs(3,h,
false
,5000);

switch
(dw)

{

case
WAIT_OBJEC_0:
//第一个对象被触发。

break
;

case
WAIT_OBJEC_0+1:
//第二个对象被触发。

break
;

case
WAIT_OBJEC_0+2:
//第三个对象被触发。

break
;

case
WAIT_TIMEOUT:
//超时

break
;

case
WAIT_FAILED:
//句柄无效。

break
;

}
9.2等待成功所引起的副作用:可能会改变对象的状态。9.3事件内核对象
事件包含使用计数,重置方式(自动,手动)触发标志(已触发状态还是未触发状态)所谓自动重置事件,就是当线程等待自动重置事件成功后,对象将自动重置成非触发状态。使用CreateEvent创建一个事件内核对象。
HANDLE
CreateEvent(//还有个CreateEventEX函数
LPSECURITY_ATTRIBUTESlpEventAttributes,
//SD
BOOL
bManualReset,
//resettype
true为手动重置false自动重置
BOOL
bInitialState,
//initialstate
true初始化为触发状态false初始化为未触发
LPCTSTR
lpName
//objectname
);

跨进程同步时,可以调用CreateEvent并在pszName中传入相同的值;使用继承;使用DuplicateHandle函数;调用OpenEvent并在pszName参数中指定与CreateEvent中相同的名字。
BOOL
SetEvent(
HANDLE
hEvent
//handletoevent);
把事件变为触发状态
BOOL
ResetEvent(
HANDLE
hEvent
//handletoevent);把事件变为非触发状态
例:
HANDLE
g_hEvent;
int
WINAPI_tWinMain(...)
{
g_hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
//创建一个手动重置的对象并将这个事件对象放在一个全局变量中
DWORD
dwThreadID;
HANDLE
hThrasd[3];
hThrasd[0]=_beginthreadex(NULL,0,WordCount,NULL,0,&dwThreadID);
hThrasd[1]=_beginthreadex(NULL,0,SpellCheck,NULL,0,&dwThreadID);
hThrasd[1]=_beginthreadex(NULL,0,Gramer,NULL,0,&dwThreadID);
OpeFileAndReadIntoMemory();
SetEvent(g_hEvent);
...
}
DWORD
WINAPIWordCount(
PVOID
pvParam)
{
WaitForSingleObject(g_hEvent,INFINITE);
return
0;
}
DWORD
WINAPISpellCheck(
PVOID
pvParam)
{
WaitForSingleObject(g_hEvent,INFINITE);
return
0;
}
DWORD
WINAPIGramer(
PVOID
pvParam)
{
WaitForSingleObject(g_hEvent,INFINITE);
return
0;
}
9.4可等待的计时器内核对象可等待的计时器是这样一种内核对象,它们会在某个指定的事件触发,或没隔一段时间触发一次。它们通常用来在某个时间执行一些操作。
HANDLE
CreateWaitableTimer(LPSECURITY_ATTRIBUTESlpTimerAttributes,
//SD
BOOL
bManualReset,
//resettype
同上,true手动重置;false自动重置
LPCTSTR
lpTimerName
//objectname);

对应的OpenWaitableTimer得到一个已经存在的可等待计时器的句柄。
当创建时,其总是处于未触发状态。
BOOL
SetWaitableTimer(//用来触发
HANDLE
hTimer,
//handletotimer
const
LARGE_INTEGER*pDueTime,
//timerduetime
什么时间触发,为负值时是相对时间
LONG
lPeriod,
//timerinterval
多久触发一次都是以毫秒为单位,为0时表示只触发一次
PTIMERAPCROUTINEpfnCompletionRoutine,
//completionroutine
LPVOID
lpArgToCompletionRoutine,
//completionroutineparameter
BOOL
fResume
//resumestate);

例一:
HANDLE
hTimer;
SYSTEMTIMESt;//表示本地时间
FILETIMEftLocal,ftUTC;
LARGE_INTEGERliUTC;
hTimer=CreateWaitableTimer(NULL,
false
,NULL);
St.wyear=2013;//年
st.wMonth=1;//月
st.wDayOfWeek=0;//忽略
st.wDay=1;//日
st.wHour=1;
st.wMinute=1;
st.wSecond=0;
st.wMillisecons=0;//精确到毫秒
SystemTimeToFileTime(&st,&ftLocal);
//SYSTEMTIME结构转换为FILETIME结构。
LocalFileTimeToFileTime(&ftLocal,&ftUTC);//将本地时间转换为UTC(全球标准时间)time
liUTC.LowPart=ftUTC.dwlowDateTime;
liUTC.HighPart=ftUTC.dwHighDateTime;
SetWaitableTimer(hTimer,&liUTC,6*60*60*1000,NULL,NULL,
false
);

注意:FILETIME和LARGE_INTEGER的二进制结构完全相同,但是对齐方式不同,前者是32位,后者是64位
例二:
HANDLE
hTimer;
LARGE_INTEGERli;
hTimer=CreateWaitableTimer(NULL,
false
,NULL);
Li.quadpart=-(5*10000000);//设置为相对时间,第一次触发事假为调用结束的5秒后
SetWaitableTimer(hTimer,&li,6*60*60*1000,NULL,NULL,
false
);

bResume用以支持挂起。一般都传入false。当传入true时,当计时器被触发时,系统就会使机器结束挂起模式,并唤醒等待该计时器的线程。当为false时,计时器会被触发,但是如果此时机器处于挂起态时,在机器继续执行之前,被唤醒的任何线程都得不到cpu。
当通过SetWaitTimer函数设置了一个等待定时器的属性之后,你可以通过CancelWaitableTimer函数来取消这些设置:
BOOLCancelWaitableTimer(HANDLEhTimer);9.4.1让可等待的计时器添加APC(异步过程调用)调用【暂时不理解】
9.4.2计时器的剩余问题
1.创建多个计时器内核对象会影响性能,可以利用CreateThreadpoolTimer函数改善。
2.可等待计时器和用户计时器(通过SetTimer函数来设置)的最大区别在于用户计时器需要在应用程序中使用大量的用户界面基础设施,从而消耗更多的资源。此外可等待计时器是内核对象,这意味着它们不仅可以在多个线程间共享,而且可以具备安全性。此外,用户计时器会产生WM_TIMER消息,这个消息被送到SetTimer设置的回调函数。此时只有一个线程得到通知。而可等待计时器对象可以被多个线程等待。如果打算在计时器被触发时执行与用户界面相关的操作。使用用户计时器可使代码更容易编写。
9.5信号量内核对象
信号量内核对象用来对资源进行计数(原子操作)。与其他内核对象相同,它包括一个使用计数。但是它还包含另外两个值:一个是最大资源计数和当前资源计数。最大资源计数表示信号量可以控制的最大资源数量。当前资源计数表示信号量当前可用资源的数量。
信号量的规则如下:
1:如果当前资源计数大于0,那么信号量处于触发状态(说明有资源可被使用,所有等待线程被调度)。
2:如果当前资源计数等于0,那么信号量处于未触发状态(没有可用资源,所有线程等待)。
3:当前资源计数不会小于0.
4:当前可用资源计数不会大于最大资源计数。
注意:使用信号量时不要将信号量的使用计数和当前资源计数混为一谈。
HANDLE
CreateSemaphore(
PSECURITY_ATTRIBUTEpsa,
//安全属性结构指针
LONG
lInitialCount,
//初始可用资源数
LONG
lMaximumCount,
//最大资源数
PCTSTR
pszName);
//信号量内核对象的名字

同样,可以打开一个指定名称的信号量,使用OpenSemaphore函数:
HANDLEOpenSemaphore(DWORDdwDesiredAccess/*一般传入SEMAPHORE_ALL_ACCESS*/,
BOOLbInheritHandle,PCTSTRpszName);

例:HANDLE
hSem=CreateSemaphore(NULL,0,5,NULL);
BOOL
ReleaseSemaphore(//
递增信号量的当前资源计数
等待函数会将资源数减一
HANDLE
hSemaphore,//信号量的句柄
LONG
lReleaseCount,
//增加个数,必须大于0且不超过最大资源数量
LPLONG
lpPreviousCount
//用来传出先前的资源计数,设为NULL表示不需要传出
);

例子(用于线程同步,信号量的用途很多):
摘取自http://blog.csdn.net/MoreWindows
#include<stdio.h>
#include<process.h>
#include<windows.h>
long
g_nNum;
unsigned
int
__stdcallFun(
void
*pPM);
const
int
THREAD_NUM=10;
//信号量与关键段
HANDLE
g_hThreadParameter;
CRITICAL_SECTIONg_csThreadCode;
int
main()
{
printf
(
"经典线程同步信号量Semaphore\n"
);
printf
(
"--byMoreWindows(http://blog.csdn.net/MoreWindows)--\n\n"
);
//初始化信号量和关键段
g_hThreadParameter=CreateSemaphore(NULL,0,1,NULL);
//当前0个资源,最大允许1个同时访问
InitializeCriticalSection(&g_csThreadCode);
HANDLE
handle[THREAD_NUM];
g_nNum=0;
int
i=0;
while
(i<THREAD_NUM)
{
handle[i]=(
HANDLE
)_beginthreadex(NULL,0,Fun,&i,0,NULL);
WaitForSingleObject(g_hThreadParameter,INFINITE);
//等待信号量>0
++i;
}
WaitForMultipleObjects(THREAD_NUM,handle,TRUE,INFINITE);
//删除信号量和关键段
DeleteCriticalSection(&g_csThreadCode);
CloseHandle(g_hThreadParameter);
for
(i=0;i<THREAD_NUM;i++)
CloseHandle(handle[i]);
return
0;
}
unsigned
int
__stdcallFun(
void
*pPM)
{
int
nThreadNum=*(
int
*)pPM;
ReleaseSemaphore(g_hThreadParameter,1,NULL);
//信号量++
Sleep(50);
//someworkshouldtodo
EnterCriticalSection(&g_csThreadCode);
++g_nNum;
Sleep(0);
//someworkshouldtodo
printf
(
"ID
=%dNUMS=%d\n"
,nThreadNum,g_nNum);
LeaveCriticalSection(&g_csThreadCode);
return
0;
}结果:


9.6互斥量内核对象
互斥量内核对象用来确保一个线程独占第一个资源的访问(
原子操作
)。互斥量对象包括一个使用计数、线程ID(标识占用这个互斥量的线程)以及一个递归计数(这个线程占用该互斥量的次数)。互斥内核对象的行为特征和关键代码段有点类似,但是它是属于内核对象,而关键代码段是用户模式对象,这导致了互斥内核对象的运行速度比关键代码段要低。所以,在考虑线程同步问题的时候,首先考虑用户模式的对象。但是,互斥内核对象可以跨进程使用,当需要实现多进程之间的线程同步,就可用考虑使用互斥内核对象。而这点,关键代码段无能为力。
互斥量的规则:
1、如果线程ID为0(无效线程ID),那么该互斥量不为任何线程所占用,它处于触发状态;
2、如果线程ID为非零值,那么有一个线程已经占用了该互斥量,它处于未触发状态;
3、与所有其它内核对象不同,操作系统对互斥量进行了特殊处理,允许它们违反一些常规的规则;
创建一个互斥量:
HANDLE
CreateMutex(
PSECURITY_ATTRIBUTESpsa,
BOOL
bInitialOwner,
//FALSE:ID和递归计数=0;TRUE:ID=调用线程ID,递归计数=1
PCTSTR
pszName);
OpenMutex来打开一个已存在的互斥量。
HANDLE
OpenMutex(
DWORD
dwDesiredAccess,
BOOL
bInheritHandle,
PCTSTR
pszName);
为了获得对被保护资源的访问权,线程要调用等待函数并传入互斥量句柄。在内部,等待函数会检查线程ID是否为0,如果为0,等待线程将互斥量对象线程ID设为当前线程ID,递归计数为1。
否则,主调线程将会被挂起。当其他线程完成对保护资源的互斥访问,释放对互斥量的占有时,互斥量的线程ID被设为0,原来被挂起的线程变为可调度状态,并将互斥量对象对象ID设为此线程ID,递归计数为1。
当线程试图等待一个未触发的互斥量对象,此时通常处于等待状态。但是系统会检查想要获得互斥量的线程的线程ID与互斥量对象内部记录的线程ID是否相同。如果相同,那么系统会让线程保持可调度状态,即使该互斥量尚未触发。每次线程等待成功一个互斥量,互斥对象的递归计数就会被设为1。因此,使递归对象大于1
的唯一途径是让线程多次等待同一个互斥量。
当目前占有互斥量的线程不再需要访问互斥资源时,它必须调用ReleaseMutex来释放互斥量。这是与其他内核对象不一样的地方
BOOL
ReleaseMutex(
HANDLE
hMutex);

9.6.1遗弃问题
互斥量具有线程所有权的问题。这就可能导致一些问题:如果占用互斥量的线程在释放互斥量之前终止,对于互斥量来说会发生什么情况呢?答案是:互斥量被遗弃。
系统会检测到互斥量被遗弃的情况,会自动的将互斥量对象的线程ID置为0,然后再检查有没有其他线程等待该互斥量。如果有线程等待,系统会从等待队列中选取一个,将其变为可调度状态。这一切都和原来没什么不同,唯一的区别是等待函数不再返回WAIT_OBJEC_0,而是返回WAIT_ABANDONED。这个特殊的返回值只适用于互斥量。
9.6.2互斥量和关键段的比较
特征
互斥量
关键段
性能


是否能跨进程使用


声明
HANDLEhmtx;
CRITICAL_SECTIONcs;
初始化
hmtx=CreateMutex(NULL,FALSE,NULL);InitializeCriticalSection(&cs);
清理
CloseHandle(hmtx);
DeleteCriticalSection(&cs);
无限等待
WaitForSingleObject(hmtx,INFINITE);
EnterCriticalSection(&cs);
0等待
WaitForSingleObject(hmtx,0);
TryEnterCriticalSection(&cs);
任意时间长度的等待
WaitForSingleObject(hmtx,dwMilliseconds);
不支持
释放
ReleaseMutex(hmtx);
LeaveCriticalSection(&cs);
是否能同时等待其它
内核对象
是(使用WaitForMultipleObjects或类似函数)

9.7线程同步对象速查表
对象
何时处于未触发状态
何时处于触发状态
成功等待的副作用
进程
当进程仍在运行的时候
当进程终止运行时(ExitProcess,
TerminateProcess)

线程
当线程仍在运行时
当线程终止运行时(ExitThread,
TerminateThread)

作业
当作业尚未超时的时候
当作业超时的时候

文件
当I/O请求正在处理时
当I/O请求处理完毕时

控制台输入
不存在任何输入
当存在输入时

文件修改通知
没有任何文件被修改
当文件系统发现修改时
重置通知
自动重置事件
ResetEvent,PulseEvent或等待成功
当调用SetEvent/PulseEvent时
重置事件
手动重置事件
ResetEvent或PulseEvent
当调用SetEvent/PulseEvent时

自动重置等待计时器
CancelWaitableTimer或等待成功
当时间到时(SetWaitableTimer)
重置定时器
手动重置等待计时器
CancelWaitableTimer
当时间到时(SetWaitableTimer)

信号量
等待成功
当数量>0时(ReleaseSemaphore)
数量递减1
互斥对象
等待成功
当未被线程拥有时(Release互斥对象)
将所有权赋予线程
关键代码段(用户模式)
等待成功((Try)EnterCriticalSection)
当未被线程拥有时(LeaveCriticalSection)
将所有权赋予线程
SRWLock(用户模式)等待成功的时候(AcquireSRWLock(Exclusive))不为线程占用的时候(ReleaseSRWLock(Exclusive))把所有权交给线程
条件变量(用户模式)等待成功地时候(SleepConditionVariable*)被唤醒的时候(Wake(All)ConditionVariable)没有
InterLocked系列函数(用户模式)从来不会使线程变成不可调度状态,它们只是修改一个值并返回。
9.8其他的线程同步函数(暂时跳过)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: