Windows Via C/C++ 读书笔记 6
1. 用内核对象做线程同步
1.1. Overview
Why use kernel object?
内核对象做同步操作要进入系统模式,比用户模式更消耗CPU,为什么还要用呢?
内核对象同步操作功能更多,比如跨进程的同步,非阻塞同步等待。
内核对象同步的关键
内核对象有两个状态:"signaled", "nonsignaled"。编程的时候可以"wait"一个内核对象到"signaled"状态才执行。
具有这两种状态的内核对象有:
1. Processes
2. Threads
3. Jobs
4. File and console standard input/output/error streams
5. Events
6. Waitable timers
7. Semaphores
8. Mutexes
1.2. Wait函数
前面提到可以"wait"内核对象到"signaled"状态。有2个函数支持这个操作:
DWORD WaitForSingleObject(
HANDLE hObject,
DWORD dwMilliseconds);
时间参数为"INFINITE"表示不超时,一直等待。
返回值可以表示等待超时(WAIT_TIMEOUT),还是成功(WAIT_OBJECT_0),或者异常(WAIT_FAILED)。
DWORD WaitForMultipleObjects(
DWORD dwCount,
CONST HANDLE* phObjects,
BOOL bWaitAll,
DWORD dwMilliseconds);
等待多个内核对象。如果把"bWaitAll"参数设为"false",表示任何一个内核对象变为"signaled",程序就返回,否则要等到所有的对象都"signaled"才返回。那么如何判断是哪个内核对象造成返回呢,可以通过返回结果判断:WAIT_OBJECT_0 + N,表示第N个对象,N从0开始。
case WAIT_OBJECT_0 + 0:
// The process identified by h[0] (hProcess1) terminated.
break;
1.3. Wait成功的副作用
Wait函数成功返回前,会把内核对象的状态由"signaled"变为"nonsignaled"。而失败返回是不会做修改操作的,失败包括超时和异常。
当然,不是所有的内核对象都有这种副作用,比如:Process,Thread。它们在结束的时候变为"signaled",而且永远不会再变回来。
"WaitForMultipleObjects"操作是原子的,否则可能会死锁。
如果不是原子的会怎么样呢?比如有2个线程等待2个对象,对象1变为signaled,线程1把它置为"nonsignaled"。对象2变为"signaled",而线程2先发现,并且把它置为"nosignaled"。死锁发生了,线程1在等待对象2,线程2在等待对象1。
为了避免这种死锁,"WaitForMultipleObjects"是会把所有对象锁定住,不让其它线程修改。
等待策略
多个线程等待一个对象,谁会先得到通知呢?答案是操作系统不做任何调度,没有优先级考虑,因此就看各个线程的造化了。
1.4. Event内核对象
事件内核对象分"auto-reset" "manual-reset"两种。Auto表示wait操作返回前会把对象reset成"nonsignaled"(见前文 副作用),因此只有一个线程能执行。Manul则会唤醒多个线程执行。
线程可以通过名称访问同一个事件对象,也可以通过前面讲的内核对象句柄的inheritance和DuplicateHandle。全局变量则是另一种最简单的办法。
1.5. Waitable Timer
Timer对象有两种用法:
1. Timer对象会在某个时间后,每隔一定时间变成"signaled"状态。
2. 可以设置一个触发执行函数,当时间到了时候,调用设置函数的线程会执行这个函数。
它的类型也有两种,auto-set和manua-set,跟Event作用相同。
不想讲太多api,可以查msdn。
Tips:
1.创建timer后,timer自动为"nonsignaled"。
2.设置的时间是绝对时间的话,是允许这个时间已经过去了。比如调用时刻是5点30分,是可以把起始时间设为4点30分的。
3.如果设置参数起始时间为负,表示调用设置函数那个时间过一定时间开始计时。比如传入一个-t时间,T时刻调用函数。那么计时起始时间是t+T。不明白就看msdn吧。
贴2个例子:
绝对时间:
// Declare our local variables.
HANDLE hTimer;
SYSTEMTIME st;
FILETIME ftLocal, ftUTC;
LARGE_INTEGER liUTC;
// Create an auto-reset timer.
hTimer = CreateWaitableTimer(NULL, FALSE, NULL);
// First signaling is at January 1, 2008, at 1:00 P.M. (local time).
st.wYear = 2008; // Year
st.wMonth = 1; // January
st.wDayOfWeek = 0; // Ignored
st.wDay = 1; // The first of the month
st.wHour = 13; // 1PM
st.wMinute = 0; // 0 minutes into the hour
st.wSecond = 0; // 0 seconds into the minute
st.wMilliseconds = 0; // 0 milliseconds into the second
SystemTimeToFileTime(&st, &ftLocal);
// Convert local time to UTC time.
LocalFileTimeToFileTime(&ftLocal, &ftUTC);
// Convert FILETIME to LARGE_INTEGER because of different alignment.
liUTC.LowPart = ftUTC.dwLowDateTime;
liUTC.HighPart = ftUTC.dwHighDateTime;
// Set the timer.
SetWaitableTimer(hTimer, &liUTC, 6 * 60 * 60 * 1000,
NULL, NULL, FALSE); ...
调用函数的时刻过相对时间开始计时:
// Declare our local variables.
HANDLE hTimer;
LARGE_INTEGER li;
// Create an auto-reset timer.
hTimer = CreateWaitableTimer(NULL, FALSE, NULL);
// Set the timer to go off 5 seconds after calling SetWaitableTimer.
// Timer unit is 100 nanoseconds.
const int nTimerUnitsPerSecond = 10000000;
// Negate the time so that SetWaitableTimer knows we
// want relative time instead of absolute time.
li.QuadPart = -(5 * nTimerUnitsPerSecond);
// Set the timer.
SetWaitableTimer(hTimer, &li, 6 * 60 * 60 * 1000,
NULL, NULL, FALSE); ...
1.6. Semaphore 信号量内核对象
信号量,操作系统讲很多了。PV操作,减少资源,增加资源。当资源数量大于1,允许线程进入,并消耗一个资源。
没有任何办法在不改变信号量资源数目的前提下获取当前信号量的可用资源数量。
1.7. Mutex互斥体内核对象
Mutex可以说是用得最多的同步对象了。作用跟critical section类似。
Mutex有3个重要属性:
1. 引用计数
内核对象引用计数,表示有多少个地方打开了这个对象。
2. 线程id
如果id=0,表示没有任何线程占用。否则为占用线程的id。操作系统就是通过id是否为0判断是否有线程占用。
3. 递归次数
表示这个线程调用了几次wait函数获取这个对象。比如函数递归调用中,函数A需要获取mutexA,函数B也需要获取mutexA。如果函数A在获取mutexA后又调用了函数B,递归计数会加一。如果不这样,会死锁,即锁不能递归。
1.7.1. 线程非正常退出WAIT_ABANDONED
如果线程在释放mutex前非正常退出。操作系统会发现这种情况,然后把mutex的线程id设为0,递归次数设为0,再唤醒一个等待的线程。唯一不同的是,wait函数的返回值不是WAIT_OBJECT_0,而是WAIT_ABANDONED。
1.7.2. Mutex比较critical section
Characteristic |
Mutex |
Critical Section |
Performance |
Slow |
Fast |
Can be used across process boundaries |
Yes |
No |
Declaration |
HANDLE hmtx; |
CRITICAL_SECTION cs; |
Initialization |
hmtx = CreateMutex (NULL, FALSE, NULL); |
InitializeCriticalSection(&cs); |
Cleanup |
CloseHandle(hmtx); |
DeleteCriticalSection(&cs); |
Infinite wait |
WaitForSingleObject (hmtx, INFINITE); |
EnterCriticalSection(&cs); |
0 wait |
WaitForSingleObject (hmtx, 0); |
TryEnterCriticalSection(&cs); |
Arbitrary wait |
WaitForSingleObject (hmtx, dwMilliseconds); |
Not possible |
Release |
ReleaseMutex(hmtx); |
LeaveCriticalSection(&cs); |
Can be waited on with other kernel objects |
Yes (use WaitForMultipleObjects or similar function) |
No |
1.8. 内核对象手册
Object |
When Nonsignaled |
When Signaled |
Successful Wait Side Effect |
Process |
While process is still active |
When process terminates (Exit-Process, TerminateProcess) |
None |
Thread |
While thread is still active |
When thread terminates (Exit-Thread, TerminateThread) |
None |
Job |
When job's time has not expired |
When job time expires |
None |
File |
When I/O request is pending |
When I/O request completes |
None |
Console input |
No input exists |
When input is available |
None |
File change notifications |
No files have changed |
When file system detects changes |
Resets notification |
Auto-reset event |
ResetEvent, PulseEvent, or successful wait |
When SetEvent/PulseEventis called |
Resets event |
Manual-reset event |
ResetEvent or PulseEvent |
When SetEvent/PulseEventis called |
None |
Auto-reset waitable timer |
CancelWaitableTimer or successful wait |
When time comes due (SetWaitableTimer) |
Resets timer |
Manual-reset waitable timer |
CancelWaitableTimer |
When time comes due (SetWaitableTimer) |
None |
Semaphore |
Successful wait |
When count > 0 (ReleaseSemaphore) |
Decrements count by 15 |
Mutex |
Successful wait |
When unowned by a thread (ReleaseMutex) |
Gives ownership to a thread |
Critical section (user-mode) |
Successful wait ((Try)Enter-Critical-Section) |
When unowned by a thread (LeaveCriticalSection) |
Gives ownership to a thread |
SRWLock (user-mode) |
Successful wait (Acquire-SRWLock(Exclusive)) |
When unowned by a thread (ReleaseSRWLock(Exclusive)) |
Gives ownership to a thread |
Condition variable (user-mode) |
Successful wait (SleepConditionVariable*) |
When woken up (Wake(All)ConditionVariable) |
None |
1.9. 其它线程同步函数
异步IO
用异步IO,可以把读写工作交给操作系统后,继续执行其它工作。然后通过wait IO对象的"signaled"状态等待IO操作结束。
WaitForInputIdle
DWORD WaitForInputIdle(
HANDLE hProcess,
DWORD dwMilliseconds);
可以等待一个进程没有输入,例如,需要等待一个进程初始化窗口完毕后做一些工作,可以用这个函数。
MsgWaitForMultipleObjects(Ex)
WaitForDebugEvent
等待Debug事件,用于编写自己的debugger,太高级了,没用过。
SignalObjectAndWait
Signale一个对象,然后等待一个对象。整个操作是原子的。可以被signale的对象只能是mutex, semaphore, event。
目的:
1.提高效率,如果分两步走,需要两次用户模式到系统模式的切换。
2.第二个原因有点极端。函数用在当一个线程不知道另一个线程什么时候会进入wait函数的时候。
书上举了一个例子:
有2个线程,线程A通知线程B完成一定工作,然后wait线程B工作完成的通知。代码如下:
线程A
// Perform some work. ... SetEvent(hEventWorkerThreadDone);
WaitForSingleObject(hEventMoreWorkToBeDone, INFINITE);
// Do more work. ...
线程B
WaitForSingleObject(hEventWorkerThreadDone);
PulseEvent(hEventMoreWorkToBeDone);
PulseEvent表示signale一个事件后立即nonsignale一个事件。如果这个时候线程A不在wait状态,那这个事件就丢失了。
修改后的代码
线程A
// Perform some work. ... SignalObjectAndWait(hEventWorkerThreadDone,
hEventMoreWorkToBeDone, INFINITE, FALSE);
// Do more work. ...
区别就是线程A通知B开始工作后立即进入wait状态。
Wait Chain Traversal(WCT) API
Vista提供了跟踪锁状态的API,可以有效侦测到死锁。