假设我有一段代码不能同时执行。我尝试线程锁的(假定是天真的)方法看起来是这样的:
int lock = 0;
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
if (lock)
return 1;
lock = 1;
/* code goes here */
lock = 0;
return 0;
}
当使用以下内容进行测试时:
for (i = 0; i < 2; i++)
thandle[i] = CreateThread(NULL, 0, ThreadProc, NULL, 0, &tid[i]);
WaitForMultipleObjects(2, thandle, TRUE, INFINITE);
我总是得到所需的效果,即只有要创建的第一个线程才能真正到达代码并返回0。然而,我不断遇到这样的建议,即这种方法可能会失败,因为锁不是使用原子操作实现的。
我看不到同时创建线程的方法,所以,从实际角度来看,这里真的需要原子性吗?有人能提供一个导致上述失败的例子吗?
您需要能够同时原子地检查锁和获取锁。
可能出现以下情况:
-
线程1检查锁定:
if (lock) // lock = 0 so skips the body of the if statement
-
线程2同时检查锁定:
if (lock) // lock = 0 so skips the body of the if statement
-
线程1分配锁:
lock = 1
-
线程2分配锁:
lock = 1
-
线程1运行其代码:
/* code goes here */ // Thread 1 starts running the critical section code
-
线程2同时运行其代码:
/* code goes here */ // Thread 2 starts running the critical section code
由于没有使用原子"测试和获取锁",在线程1检查锁和设置锁之间,线程2能够检查锁,因此两个线程可以同时处于关键部分。
在单核系统上,如果在线程1检查锁和设置锁之间发生到线程2的任务切换,就会发生这种情况。
在多核系统上,如果两个线程都在同一个核心上,并且任务切换到线程2,就会发生这种情况;如果线程在不同的核心上运行,也会发生这种情况。
是的,我知道这怎么会失败。
如果你在单核/单处理器机器上运行它,你可能会永远运行它,永远不会出现故障。
在一台有多个核心的机器上,看到故障可能是家常便饭。
如果机器上的所有核心都很忙,它就会失败。创建新线程的一个线程将创建多个线程,但没有一个线程立即开始运行,因为核心都很忙。
然后,许多其他线程同时完成处理,并且您的一些线程都同时开始。它们现在都在锁步骤中执行,所以它们都读取相同的值,都试图写入相同的值(产生未定义的行为),都同时执行代码,都将锁设置为0,并且都返回0。
除非你有很多线程和核心,否则在给定的运行中发生这种情况的几率可能相当低。事实上,这是有疑问的,它最终会发生——涉及的线程和内核越多,发生的时间就越早、频率就越高。
大多数时候,您的代码都是可以的,但在某些情况下,它会失败,两个或多个线程可能会同时执行受锁保护的代码。
想象两个线程同时开始执行您的函数。他们都将执行
if (lock)
return 1;
在到达之前
lock = 1;
从而两者都进入受保护的代码。这就是为什么锁必须是原子锁。
该解决方案非常简单,只需使用Win32函数InitializeCriticalSection创建一个关键节,并将其与EnterCriticalSection和LeaveCriticalSection一起使用即可。
您的代码充满了问题:
-
lock
不是volatile
,因此编译器可以自由地将其读取到寄存器中,并在寄存器中继续使用/更新它,而无需将更改写回内存(在内存中,至少有机会引起其他线程的注意,尽管在许多CPU上,需要显式内存屏障(同步操作码)来保证其他线程的可见性)- 请注意,当您使用适当的同步机制(互斥或原子操作)时,也不需要使用
volatile
- 请注意,当您使用适当的同步机制(互斥或原子操作)时,也不需要使用
-
指令可能会被重新排序,以便在您尝试锁定之前或尝试解锁之后执行一些
/* code [that] goes here */
-
CCD_ 5测试与CCD_。。。即使线程正在写入一个原子变量,您也需要一个Compare and Swap/Compare and Exchange风格的操作来保证它的安全性,如果失败了,您需要旋转等待-燃烧CPU-或者在多次尝试后屈服,然后重试(你猜怎么着-到那时你已经重新实现了互斥,而不是使用系统的互斥)
由于代码使用的是像CreateThread()、WaitForMultipleObjects()这样的Windows函数,所以不妨使用Windows互斥或信号量,并在线程中使用WaitForSingleObject(),假设您希望线程等待而不是中止。