多个线程访问共享资源时会发生同步问题。根本原因是因为Windows是抢占式多线程环境。
线程在执行的时候随时可能被中断(运行完一个cpu时间或有更高优先级的线程要运行)
例如:
//Define a global variable.
long g_x = 0;
DWORD WINAPI ThreadFunc1(PVOID pvParam)
{
g_x++;
return(0);
}
DWORD WINAPI ThreadFunc2(PVOID pvParam)
{
g_x++;
return(0);
}
在这个代码中,声明了一个全局变量g _ x,并将它初始化为0。现在,假设创建两个线程,一个线程执行T h r e a d F u n c 1,另一个线程执行T h r e a d F u n c 2。这两个函数中的代码是相同的,它们都将1添加给全局变量g _ x。因此,当两个线程都停止运行时,你可能希望在g _ x中看到2这个值。但是你真的看到了吗?回答是,也许看到了。根据代码的编写方法,你无法说明g _ x中最终包含了什么东西。下面我们来说明为什么会出现这种情况。假设编译器生成了下面这行代码,以便将g _ x递增1:
MOV EAX, [g_x] ;Move the value in g_x into a register.
INC EAX ;Increment the value in the register.
MOV [g_x], EAX ;Store the new value back in g_x.
两个线程不可能在完全相同的时间内执行这个代码。因此,如果一个线程在另一个线程的后面执行这个代码,那么下面就是实际的执行情况:
MOV EAX, [g_x] ;Thread 1: Move 0 into a register.
INC EAX ;Thread 1: Increment the register to 1.
MOV [g_x], EAX ;Thread 1: Store 1 back in g_x.
MOV EAX, [g_x] ;Thread 2: Move 1 into a register.
INC EAX ;Thread 2: Increment the register to 2.
MOV [g_x], EAX ;Thread 2: Store 2 back in g_x.
当两个线程都将g _ x的值递增之后, g _ x中的值就变成了2。这很好,并且正是我们希望的:即取出零( 0),两次将它递增1,得出的值为2。太好了。不过不要急,Wi n d o w s是个抢占式多线程环境。一个线程可以随时中断运行,而另一个线程则可以随时继续执行。这样,上面的代码就无法完全按编写的那样来运行。它可能按下面的形式运行:
MOV EAX, [g_x] ;Thread 1: Move 0 into a register.
INC EAX ;Thread 1: Increment the register to 1.
MOV EAX, [g_x] ;Thread 2: Move 0 into a register.
INC EAX ;Thread 2: Increment the register to 1.
MOV [g_x], EAX ;Thread 2: Store 1 back in g_x.
MOV [g_x], EAX ;Thread 1: Store 1 back in g_x.
如果代码按这种形式来运行, g _ x中的最后值就不是2,而是你预期的1。这使人感到非常担心,因为你对调度程序的控制能力非常小。实际上,如果有1 0 0个线程在执行相同的线程函数,当它们全部退出之后, g _ x中的值可能仍然是1。显然,软件开发人员无法在这种环境中工作。我们希望在所有情况下两次递增0产生的结果都是2。另外,不要忘记,编译器生成代码的方法,哪个C P U在执行这个代码,以及主计算机中安装了多少个C P U等因素,决定了产生的结果可能是不同的。这就是该环境的运行情况,我们对此无能为力。但是, Wi n d o w s确实提供了一些函数,如果正确地使用这些函数,就能确保产生应用程序的代码得到的结果。
这些函数就被称为互锁函数。
互锁函数保证线程以原子操作的方式来访问资源。原子操作方式是指当一个线程正在访问共享的内存资源时,另一个线程也要访问共享资源,那么就只能等待访问资源的线程访问完毕之后才能访问该共享资源。
也就是说原子操作方式同样是可以被中断的,这点理解很重要。只是中断之前打开了cpu的一个特殊标志位来标识共享资源的地址,另一个线程访问共享资源时会检查这个标识,如果标志位打开就等待,如果关闭就直接访问该资源。(本人结合Windows核心编程的个人理解,仅供参考)
循环锁的概念很重要,是为了解决互锁函数只能修改单个值这个缺陷所引入的概念。采用一个全局的bool类型变量来标识一个复杂的资源是否正在被线程访问。当另一个线程也要访问该资源时,会先测试这个变量的值,如果为假就设为真,并访问该资源。如果为真就继续测试,直到另一个线程改变了它的值,然后设置为真并访问该资源,访问完毕再设为假。这样就导致了一个问题,那就是线程有可能在一个cpu时间内什么事也没有做,一直在测试这个变量的值。这样就导致浪费了其他线程的cpu时间。
据此引入了关键代码段的概念。由于互锁函数本身的缺陷(只能修改单个值)才引入关键代码段的概念。其实内部还是采用了互锁函数来实现。具体的策略不详。