-概述
1.内核对象线程同步
优点:
可跨进程使用
可指定超时时间
缺点:
相比用户模式线程同步更耗时
2.进程,线程,作业…几乎所有这些内核对象都可以用来同步。
对线程同步来说,这些内核对象中的每一种要么处于触发状态,要么处于未触发状态。
进程内核对象,在进程终止时,操作系统自动使进程内核对象变为触发状态。
线程内核对象在创建的时候处于未触发状态,当线程终止时,操作系统自动将线程对象的状态改为已触发。
3.下面的内核对象即可处于触发状态,也可处于未触发状态
进程
线程
作业
文件以及控制台的标准输入流、输出流、错误流
事件
可等待的计时器
信号量
互斥量
线程可以自己切换到等待状态,直到另一个对象被触发为止。
-线程同步函数–等待函数
1.等待函数使一个线程自愿进入等待状态,直到指定的内核对象被触发为止。
// 返回值
// WAIT_OBJECT_0:等待的对象被触发
// WAIT_TIMEOUT:超时
// WAIT_FAILED:出错
DWORD WaitForSingleObject(
// 要等待的内核对象
HANDLE hObject,
// 超时时间
// INFINITE
// > 0 微妙单位
// 0 立即返回
DWORD dwMilliseconds
);
// 返回值:
// WAIT_FAILED:出错
// WAIT_TIMEOUT:超时
// 如给bWaitAll传TRUE且所有对象都被触发了,则,返回值是WAIT_OBJECT_0,如传给bWaitAll是FALSE,那么只要任何一个对象被触发,函数就会立即返回。返回值是 WAIT_OBJECT_0--WAIT_OBJECT_0 + dwCount - 1之间的任何一个值。
DWORD WaitForMultipleObjects(
// 希望检查的内核对象的数量:1--MAXIMUM_WAIT_OBJECTS
DWORD dwCount,
// 指向一个内核对象句柄的数组
CONST HANDLE* phObjects,
// TRUE 须等待数组内所有内核对象被触发
// FALSE 等待数组内任何一个内核对象被触发
BOOL bWaitAll,
// 超时时间
DWORD dwMilliseconds
);
WaitForMultipleObjects能以原子方式执行所有操作,当线程调用WaitForMultipleObjects时,函数会测试所有对象的触发状态,并引发相应的副作用,所有这些都是作为一个操作来完成的。
当函数检查内核对象的状态时,任何其它线程都不能在背后修改对象的状态。
如多个线程等待同一个内核对象,当对象被触发的时候,系统公平地选择一个线程来唤醒。
2.异步设备I/O
异步设备I/O允许线程开始读取操作或写入操作,但不必等待读取操作或写入操作完成。
设备对象是可同步的对象,这意味着我们可以调用WaitForSingleObject,并传入文件句柄,套接字,通信端口,等等。当系统执行异步I/O的时候,设备对象处于未触发状态。一旦操作完成,系统会将对象变成触发状态。
3.WaitForInputIdle函数
DWORD WaitForInputIdle(
HANDLE hProcess,
DWORD dwMilliseconds
);
线程可调用WaitForInputIdle函数来将自己挂起。
这个函数会等待由hProcess标识的进程,直到创建应用程序第一个窗口的线程中没有待处理的输入为止。
对父进程比较有用,父进程可创建一个子进程来完成一些工作。父进程调用CreateProcess时,父进程可一边继续执行,一边让子进程初始化。父进程能够知道子进程已经初始化完毕的唯一方法,是等待子进程,直到它不再处理任何输入为止。
4.MsgWaitForMultipleObjects(Ex)函数
DWORD MsgWaitForMultipleObjects(
DWORD dwCount,
PHANDLE phObjects,
BOOL bWaitAll,
DWORD dwMilliseconds,
DWORD dwWakeMask
);
DWORD MsgWaitForMultipleObjectsEx(
DWORD dwCount,
PHANDLE phObjects,
DWORD dwMilliseconds,
DWORD dwWakeMask,
DWORD dwFlags
);
内核对象被触发时,调用线程会变成可调度状态。
窗口消息需要被派送到一个由调用线程创建的窗口时,它们也会变成可调度状态。
5.WaitForDebugEvent
当调试器开始执行时,它会将自己附着到被调试程序。
然后,调试器只是在一边闲着,等待操作系统通知它有关与被调试程序相关的事件发生。
调试器通过调用
BOOL WaitForDebugEvent(
PDEBUG_EVENT pde,
DWORD dwMilliseconds
);
调试器调用这个函数时,调试器的线程挂起。
6.SignalObjectAndWait
DWORD SignalObjectAndWait(
// 互斥量,信号量,事件
HANDLE hObjectToSignal,
// 可等待内核对象
HANDLE hObjectToWaitOn,
DWORD dwMilliseconds,
// 可提醒等待标志。
// 可提醒等待:等待中,可以处理添加到线程队列的异步过程调用
BOOL bAlertable
);
优点:
触发+等待,以原子操作完成。
相比分别调用,具备更高效率。
7.使用等待链遍历API来检测死锁
等待链遍历API,用以帮助列出所有锁,检测进程内部,进程间的死锁。
WCT所记录的同步机制的类型:
可能的锁 | 描述 |
---|---|
关键段 | windows会记录那个线程正占用那个关键段 |
互斥量 | windows会记录那个线程正在占用那个互斥量 |
进程和线程 | windows会记录那个线程正在等待进程终止或线程终止 |
SendMessage调用 | 知道那个线程正在等待SendMessage调用返回 |
COM初始化和调用 | windows会记录对CoCreateInstance的调用以及对COM对象的方法的调用 |
高级本地过程调用 |
-等待成功所引起的副作用
如线程在等待一个自动重置事件对象,当事件对象被触发时,函数会检测到这一情况,这时它可直接返回WAIT_OBJECT_0给调用对象,但,在函数返回前,它会使事件变为非触发的状态–这就是等待成功所引起的副作用。
-事件内核对象
1.在所有内核对象中,事件相比其他对象更简单。
包含:
使用计数
自动重置/手动重置标志
触发标志
2.手动重置事件被触发时,正在等待该事件的所有线程都将变成可调度状态。自动重置事件被触发时,只有一个在等待该事件的线程会变成可调度状态。
// 返回与当前进程关联的事件内核对象句柄
HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
// TRUE 触发
// FALSE 非触发
BOOL bInitialState,
PCTSTR pszName
);
HANDLE CreateEventEx(
PSECURITY_ATTRIBUTES psa,
PCTSTR pszName,
// CREATE_EVENT_INITIAL_SET 初始为触发
// CREATE_EVENT_MANUAL_RESET 手动重置
DWORD dwFlags,
// 指定创建事件时返回的句柄对事件有何种访问权限。非Ex版总是要求全部权限。
DWORD dwDesiredAccess
);
//
HANDLE OpenEvent(
DWORD dwDesiredAccess,
BOOL bInherit,
PCTSTR pszName
);
// 在不需要事件内核对象时用CloseHandle来关闭。
// 设置事件为触发状态
BOOL SetEvent(HANDLE hEvent);
// 设置事件为未触发状态
BOOL ResetEvent(HANDLE hEvent);
//
BOOL PulseEvent(HANDLE hEvent);
PulseEvent会先触发事件,然后立刻将其恢复到未触发状态,这就相当于调用SetEvent之后立即调用ResetEvent一样。如对一个手动重置事件调用PulseEvent,那么当事件被脉冲触发的时候,所有正在等待该事件的线程都会变成可调度状态。如果对一个自动重置事件调用PulseEvent,那么只有一个正在等待该事件的线程会变成可调度状态。如果当事件被脉冲触发的时候没有线程在等待该事件,那么将不会产生任何效果。
-可等待的计时器内核对象
1.可等待的计时器是这样一种内核对象,它们会在某个指定的时间触发,或每隔一段时间触发一次。通常用来在某个时间执行一些操作。
HANDLE CreateWaitableTimer(
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
PCTSTR pszName
);
HANDLE OpenWaitableTimer(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName
);
BOOL SetWaitableTimer(
// 要触发的计时器句柄
HANDLE hTimer,
// 第一次触发时间。
// 绝对时间:
// 相对时间:传入一个负值,传入的值须是100纳秒的整数倍。1微妙=1000纳秒
// 表示调用结束后指定时间间隔后,开始。
const LARGE_INTEGER *pDueTime,
// 在第一次触发后,计时器应该以怎样的频度触发。毫秒。
// 0,只触发一次
LONG lPeriod,
PTIMERAPCROUTINE pfnCompletionRoutine,
PVOID pvArgToCompletionRoutine,
BOOL bResume
);
// 关闭计时器
CloseHandle
// 重置计时器(先取消),给它设置一个新的触发时间
SetWaitableTimer
// 取消计时器
// 取消的计时器就不会再触发了
BOOL CancelWaitableTimer(HANDLE hTimer);
2.在创建的时候,可等待的计时器对象总是处于未触发状态。当我们想要触发计时器的时候,须调用SetWaitableTimer函数来告诉它。
例:
设置计时器第一次触发时间为2008年1月1日下午1:00,之后每隔6小时触发一次。
HANDLE hTimer;
LARGE_INTEGER liUTC;
hTimer = CreateWaitableTimer(NULL, FALSE, NULL);
SYSTEMTIME st;
st.wYear = 2008;
st.wMonth = 1;
st.wDayOfWeek = 0;
st.wDay = 1;
st.wHour = 13;
st.wMinute = 0;
st.wSecond = 0;
st.wMilliseconds = 0;
FILETIME ftLocal;
SystemTimeToFileTime(&st, &ftLocal);
FILETIME ftUTC;
LocalFileTimeToFileTime(&ftLocal, &ftUTC);
liUTC.LowPart = ftUTC.dwLowDateTime;
liUTC.HighPart = ftUTC.dwHighDateTime;
SetWaitableTimer(
hTimer,
&liUTC,
6 * 60 * 60 * 1000,
NULL,
NULL,
FALSE
);
系统时间–》本机文件时间–》标准文件时间–》大整数
前述代码先初始化了一个SYSTEMTIME结构,来表示计时器第一次触发的时间。这个时间是本地时间,是根据本机的时区校正后的时间。
对SetWaitableTimer来说,它认为传入的时间始终是全球标准时间。LocalFileTimeToFileTime,把本机文件时间转化为全球标准文件时间。
3.虽然FILETIME结构和LARGE_INTEGER结构具有完全相同的二进制格式,但是,这两个结构的对齐方式是不同的。
所有FILETIME结构的地址必须对齐到32位边界,而所有LARGE_INTEGER结构的地址则必须对齐到64位边界。
x86处理器会对未经对齐的数据引用进行处理,而不会报错。
4.对支持挂起和继续执行的计算机来说,给SetWaitableTimer最后一个参数bResume指定TRUE,当计时器被触发的时候,系统会使机器结束挂起模式,并唤醒正在等待该计时器的线程。如传给bResume的值是FALSE,那么计时器会被触发,但在机器继续执行前,被唤醒的任何线程都得不到CPU时间。
-让可等待的计时器添加APC调用
1.当计时器触发的时候,Microsoft允许计时器把一个异步过程调用放到SetWaitableTimer的调用线程的队列里。
2.
给SetWaitableTimer的pfnCompletionRoutine和pvArgToCompletionRoutine传NULL,
此时,时间一到,就触发计时器对象。
如果希望时间一到就让计时器把一个APC(异步过程调用)添加到队列中去,就必须实现一个计时器APC函数,并把函数的地址传入。
VOID APIENTRY TimerAPCRoutine(
PVOID pvArgToCompletionRoutine,
// 1.
DWORD dwTimerLowValue,
// 2.
// 1,2表示计时器被触发的时间
DWORD dwTimerHighValue
)
{
//
}
VOID APIENTRY TimerAPCRoutine(
PVOID pvArgToCompletionRoutine,
// 1.
DWORD dwTimerLowValue,
// 2.
// 1,2表示计时器被触发的时间
DWORD dwTimerHighValue
)
{
//
FILETIME ftUTC, ftLocal;
SYSTEMTIME st;
TCHAR szBuf[256];
ftUTC.dwLowDateTime = dwTimerLowValue;
ftUTC.dwHighDateTime = dwTimerHighValue;
FileTimeToLocalFileTime(&ftUTC, &ftLocal);
FileTimeToSystemTime(&ftLocal, &st);
GetDateFormat(LOCALE_USER_DEFAULT, DATE_LONGDATE, &st, NULL, szBuf, _countof(szBuf));
_tcscat_s(szBuf, _countof(szBuf), TEXT(" "));
GetTimeFormat(LOCALE_USER_DEFAULT, 0, &st, NULL, _tcschr(szBuf, TEXT('\0')), (int)(_countof(szBuf) - _tcslen(szBuf)) );
MessageBox(NULL, szBuf, TEXT("Timer went off at ..."), MB_OK);
}
// 使用计时器+APC示例
void SomeFunc()
{
HANDLE hTimer = CreateWaitableTimer(NULL, TRUE, NULL);
LARGE_INTEGER li = {0};
SetWaitableTimer(hTimer, &li, 5000, TimerAPCRoutine, NULL, FALSE);
SleepEx(INFIINTE, TRUE);
// WaitForSingleObjectEx(hTimer, INFINITE, TRUE);
// 计时器被触发时,使线程退出了可提醒状态,APC函数不会被调用
CloseHandle(hTimer);
}
计时器被触发的时候,当且仅当SetWaitableTimer的调用线程正处于可提醒状态时,这个函数会被 调用SetWaitableTimer的线程调用。
要使线程处于可提醒状态,线程必须是由于调用SleepEx,WaitForSingleObjectEx,WaitForMultipleObjectsEx,MsgWaitForMultipleObjectsEx或SignalObjectAndWait而进入的等待状态。
如果线程并非在其中的一个函数内等待,那么系统不会把计时器的APC函数添加到队列中。
这样可避免线程的APC队列因为计时器的APC通知而负荷过多,从而避免浪费系统中的大量内存。
只有当所有的APC函数都处理完毕后,才返回可警告函数。
-计时器的剩余问题
1.计时器可以将APC函数添加到线程的队列中,但现今大多数的应用程序并没有使用APC,而是使用I/O完成端口机制。
2.可等待计时器和用户计时器(用SetTimer设置)
两者最大的区别在于用户计时器需要在应用程序中使用大量的用户界面基础设施,从而消耗更多的资源。
另外,可等待计时器是内核对象,它们可在多个进程间共享,具备安全属性。
用户计时器会产生WM_TIMER消息,这个消息被送回调用SetTimer的线程(对回调计时器),或被送回创建窗口的线程(对基于窗口的计时器),故,一个用户计时器触发时,只有一个线程会得到通知。另一方面,多个线程可等待可等待计时器,如果计时器是手动重置计时器,那么有多个线程可变成可调度状态。
WM_TIMER消息总是优先级最低的,只有当线程的消息队列中没有其它消息的时候才会被处理。可等待的计时器的处理方式与其它内核对象没有任何不同,如果计时器被触发且线程正在等待,那么系统将唤醒线程。
-信号量内核对象
1.信号量内核对象用来对资源进行计数。
与其它所有内核对象相同,它们也包含一个使用计数。
但它们还包含另外两个32位值:一个最大资源计数和一个当前资源计数。
2.信号量的规则如下:
-如果当前资源计数大于0,那么信号量处理触发状态。
-如果当前资源计数等于0,那么信号量处于未触发状态。
-系统绝对不会让当前资源计数变为负数。
-当前资源计数绝对不会大于最大资源计数。
// 创建信号量
HANDLE CreateSemaphore(
PSECURITY_ATTRIBUTE psa,
LONG lInitialCount,
LONG lMaximumCount,
PCTSTR pszName
);
// 限定权限方式创建
HANDLE CreateSemaphoreEx(
PSECURITY_ATTRIBUTES psa,
LONG lInitialCount,
LONG lMaximumCount,
PCTSTR pszName,
DWORD dwFlags, // 保留,设为0
DWORD dwDesiredAccess
);
// 得到一个已经存在的信号量的句柄
HANDLE OpenSemaPhore(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName
);
3.为获得对被保护资源的访问权,线程要调用一个等待函数并传入信号量的句柄。在内部,等待函数会检查信号量的当前资源计数,如果它的值大于0,那么函数会把计数器减1并让调用线程继续运行。
信号量的最大优势在于它们会以原子方式来执行这些测试和设置工作,也就是说,当我们向信号量请求一个资源的时候,操作系统会检查资源是否可以,并将资源的数量递减,整个过程不会被别的线程打断。只有当资源计数递减完成后,系统才允许另一个线程请求对资源的访问。
如果等待函数发现信号量的当前资源计数为0,那么系统会让调用线程进入等待状态,当另一个线程将信号量的当前资源计数递增时,系统会记得那个还在等待的线程,使它们变成可调度状态。
// 递增信号量的当前资源计数
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
// 当前资源计数的递增量
LONG lReleaseCount,
PLONG plPreviousCount
);
-互斥量内核对象
1.互斥量(mutex)内核对象用来确保一个线程独占对一个资源的访问。
2.互斥量对象包含一个使用计数,线程ID以及一个递归计数。
3.互斥量与关键段的行为完全相同,但是,互斥量是内核对象,而关键段是用户模式下的同步对象。
线程ID来标识当前占用这个互斥量的是系统中的那个线程,递归计数表示这个线程占用该互斥量的次数。
4。互斥量规则
-如果线程ID为0(无效线程ID),那么该互斥量不为任何线程所占用,它处于触发状态。
-如果线程ID为非零值,那么有一个线程已经占用了该互斥量,它处于未触发状态。
-与所有其它内核对象不同,操作系统对互斥量进行了特殊处理,允许它们违反一些常规的规则。
5.使用
// 创建
HANDLE CreateMutex(
PSECURITY_ATTRIBUTES psa,
//
BOOL bInitialOwner,
PCTSTR pszName
);
//
HANDLE CreateMutexEx(
PSECURITY_ATTRIBUTES psa,
PCTSTR pszName,
// CREATE_MUTEX_INITIAL_OWNER等价于TRUE
DWORD dwFlags,
DWORD dwDesiredAccess
);
HANDLE OpenMutex(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName
);
5.为了获得对被保护资源的访问权,线程要调用一个等待函数并传入互斥量的句柄。在内部,等待函数会检查线程ID是否为0,如果为0,那么函数会把线程ID设为调用线程的线程ID,把递归计数设为1,然后让调用线程继续运行。
如等待函数检测到线程ID不为0,那么调用线程将进入等待状态。当另一个线程将互斥量的线程ID设为0的时候,系统会记得有一个线程正在等待,于是它把线程ID设为正在等待的那个线程的线程ID,把递归计数设为1,使正在等待的线程变成可调度状态。这些对互斥量内核对象的检查和修改都是以原子方式进行的。
在用来触发普通内核对象和撤销触发普通内核对象的规则中,有一条不适合互斥量。
假设线程试图等待一个未触发的互斥量对象,这时,线程通常会进入等待状态。但是,系统会检查想要获得互斥量的线程的线程ID与互斥量内核对象内部记录的线程ID是否相同,如果线程ID一致,那么系统会让线程保持可调度状态—即使该互斥量尚未触发。
每次线程成功地等待了一个互斥量,互斥量对象的递归计数会递增。使递归计数大于1的唯一途径是利用这个例外,让线程多次等待同一个互斥量。
// 释放互斥量
BOOL ReleaseMutex(HANDLE hMutex);
这个函数会将对象的递归计数减1。如果线程成功地等待了互斥量对象不止一次,那么线程必须调用ReleaseMutex相同的次数才能使对象的递归计数变为0。当递归计数变为0的时候,函数还会将线程ID设为0,这样就触发了对象。
6.遗弃问题
互斥量具备线程所有权的概念。
当线程调用ReleaseMutex的时候,函数会检查调用线程的线程ID与互斥量内部保存的线程ID是否一致。如果线程ID一致,那么,递归计数会递减。如果线程ID不一致,那么ReleaseMutex将不执行任何操作并返回FALSE给调用者。GetLastError会返回ERROR_NOT_OWNER。
如果占用互斥量的线程在释放互斥量之前终止,那对互斥量和正在等待该互斥量的线程来说,系统认为互斥量被遗弃–由于占用互斥量的线程已经终止,因此,再也无法释放它。
因为系统会记录所有的互斥量和线程内核对象,所以它确切地直到互斥量何时被遗弃。当互斥量被遗弃时,系统会自动将互斥量对象的线程ID设为0,将它的递归计数设为0。然后,系统会检查有没有其它线程正在等待该互斥量。如果有,系统会“公平地”选择一个正在等待的线程,把对象内部的线程ID设为所选择的那个线程的线程ID,并把递归计数设为1。这一切都和从前一样,唯一的不同之处在于等待函数不再返回通常的WAIT_OBJECT_0,而是返回一个特殊的值WAIT_ABANDONED。这个特殊的返回值(只适用与互斥量),表示线程正在等待的互斥量为其它线程所占用,但该线程在完成对共享资源的使用之前终止了。这时共享资源可能已被破坏。