《Windows via C/C++》学习笔记 —— 用户模式的“线程同步”之“互锁函数族”
线程需要相互通信,当多个线程访问共享资源的时候;当一个线程结束一个任务,然后要通知另一个线程的时候,我们需要进行线程同步的控制。
要让共享资源不被破坏,或者让另一个线程能够在适当的时候被调度,这就是线程同步需要解决的问题。
Windows Vista提供了多种线程同步的方法,这里主要介绍在用户模式下的线程同步。
1、互锁函数族:Interlocked*函数群(*表示省略后面的字符,即以Interlocked开头)。
2、关键代码段:Criticle Sections
3、读写锁:Slim Read-Write Locks
4、状态变量:Condition Variable
互琐函数族
本质上是对一个变量进行原子性的修改,在修改的时候不受到其他线程的干扰。也就是说,“读数”、“修改”、“写回”这3个操作是一个整体,中间没有停顿,不可分割。
考虑如下代码:
long g_x = 0;
DWORD WINAPI ThreadFunc1(PVOID pvParam) {
g_x++;
return(0);
}
DWORD WINAPI ThreadFunc2(PVOID pvParam) {
g_x++;
return(0);
}
两个线程共享一个全局变量,当这两个线程都执行完成之后,g_x会变成2,但是事实一定是这样么?
“g_x++”这条语句包含了三个过程:1、取得g_x的值,送到寄存器中;2、把寄存器中的值加1;3、将得到的新的值写回到原来g_x所对应的存储单元中。
设想如下情形:线程1首先取得g_x的值,送到EAX寄存器中,然后对EAX中的数值加1,然后由于某种原因暂停执行。这个时候,CPU分配给了线程2,然后线程2取得g_x的值,也把它送到EAX寄存器中,这样EAX里面原来的值就被这个时候的g_x(数值为0)覆盖了,然后线程2将EAX中的数值加1,当线程2写回的时候,把1写回到了g_x中。然后线程1恢复执行,将数据从EAX中写回,它写回的也是1,而不是2!
可见,这两个线程相互之间会干扰对方的行动,从而可能破坏共享的数据。上面讨论的这种情况,是典型的“丢失修改”。同时,还可能有“读脏数据”、“不可重复读”等同步问题。
可以使用“互锁函数族”解决上述问题,你可以调用下面两个函数来代替g_x++:
LONG InterlockedExchangeAdd(
PLONG volatile plAddend,
LONG lIncrement);
LONGLONG InterlockedExchangeAdd64(
PLONGLONG volatile pllAddend,
LONGLONG llIncrement);
这两个函数第一参数可以理解为“被加数”(其实传递的是一个数值的地址),第二个参数可以理解为“加数”。就相当于执行“*plAddend = *plAddend + lIncrement”。唯一的区别“取数”、“修改”、“写回”成了一个统一的整体,是一个不可分割原子操作。
你可以把上面代码中的“g_x++”换成“InterlockedExchangeAdd(&g_x, 1)”。
你也可以将第二个参数写为一个负数,就相当于执行减运算。
还有一些互锁函数:
LONG InterlockedExchange(
PLONG volatile plTarget,
LONG lValue);
LONGLONG InterlockedExchange64(
PLONGLONG volatile plTarget,
LONGLONG lValue);
PVOID InterlockedExchangePointer(
PVOID* volatile ppvTarget,
PVOID pvValue);
这三个函数将第一个参数(一个指针)指向的数据替换为第二个参数中的值。这三个函数都返回第一个参数指向数据的原始值。
实现循环锁的时候,InterlockedExchange函数比较有用。
BOOL g_fResourceInUse = FALSE; //全局变量,指明资源是否正在被访问
void Func1() { //线程函数
// 查看访问标志,如果当前值为TRUE表示正在被另一个线程使用,本线程等待
while (InterlockedExchange (&g_fResourceInUse, TRUE) == TRUE)
Sleep(0);
... // 访问资源
InterlockedExchange(&g_fResourceInUse, FALSE);// 将访问标志设为FALSE
}
还有几个用户比较并修改值或指针的互锁函数:
PVOID InterlockedCompareExchange(
PLONG plDestination,
LONG lExchange,
LONG lComparand);
LONGLONG InterlockedCompareExchange64(
LONGLONG pllDestination,
LONGLONG llExchange,
LONGLONG llComparand);
PVOID InterlockedCompareExchangePointer(
PVOID* ppvDestination,
PVOID pvExchange,
PVOID pvComparand);
这三个函数负责执行一个原子的“比较和修改”操作。
第一个参数:指向需要被修改的值的指针;第二个参数:修改的值;第三个参数:被比较的值。
拿InterlockedCompareExchange为例。
函数比较*plDestination(第一个参数指向的值)和lComparand(第三个)的值是否相等,如果相等,则将plDestination指向的值修改为lExchange,否则,就不进行修改。最后,函数返回plDestination代表的原来的值。
还有一些互锁函数:
LONG InterlockedIncrement(PLONG plAddend); //将参数指向的值加1
LONG InterlockedDecrement(PLONG plAddend); //将参数指向的值减1
在Windows XP及其以上版本系统中,除了可以原子地操纵整型和布尔型的值之外,还可以通过一系列函数来操纵一个堆栈,称为“Interlocked Singly Linked List”(互锁单链表)。每一个操作,比如压栈、出栈,都以原子的方式进行操作。
下表里出了一些作用在“互锁单链表”上的函数:
函数 |
描述 |
---|---|
InitializeSListHead |
创建一个空的堆栈 |
InterlockedPushEntrySList |
压栈 |
InterlockedPopEntrySList |
出粘 |
InterlockedFlushSList |
清空堆栈 |
QueryDepthSList |
查询堆栈中元素个数 |