5. 如何使用关键代码段实现线程的同步?
如果需要一小段代码以原子操作的方式执行,这时简单的互锁函数已不能满足需要,必须使用关键代码段来解决问题。不过使用关键代码段时,很容易陷入死锁状态,因为在等待进入关键代码段时无法设定超时值。关键代码段是通过对共享资源设置一个标志来实现的,就像厕所门上的“有人/没人”标志一样。这个标志就是一个CRITICAL_SECTION变量。该变量在任何一个线程使用它之前应当进行初始化。初始化可以有两种方法,使用InitializeCriticalSection函数和InitializeCriticalSectionAndSpinCount函数。然后在每个使用共享资源的线程函数的关键代码段前使用EnterCriticalSection函数或者使用TryEnterCriticalSection函数。在关键代码段使用之后调用LeaveCriticalSection函数。在所有的线程都不再使用该共享资源后应当调用DeleteCriticalSection函数来清除该标志。举例说明:
const int MAX_TIMES = 1000;
int g_intIndex = 0;
DWORD g_dwTimes[MAX_TIMES];
CRITICAL_SECTION g_cs;
void Init )
{
……
InitializeCriticalSection &g_cs );
……
}
DWORD WINAPI FirstThread PVOID lpParam )
{
while g_intIndex < MAX_TIMES )
{
EnterCriticalSection &g_cs );
g_dwTimes[g_intIndex] = GetTickCount );
g_intIndex++;
LeaveCriticalSection &g_cs );
}
return 0;
}
DWORD WINAPI SecondThread PVOID lpParam )
{
while g_intIndex < MAX_TIMES )
{
EnterCriticalSection &g_cs );
g_intIndex++;
g_dwTimes[g_intIndex - 1] = GetTickCount );
LeaveCriticalSection &g_cs );
}
return 0;
}
void Close )
{
……
DeleteCriticalSection &g_cs );
……
}
使用关键代码段应当注意一些技巧:
(1) 每个共享资源使用一个CRITICAL_SECTION变量。
这样在当前线程占有一个资源时,另一个资源可以被其他线程占有。
EnterCriticalSection &g_cs );
for intLoop = 0; intLoop < 100; intLoop++ )
{
g_intArray[intLoop] = 0;
g_uintArray[intLoop] = 0;
}
LeaveCriticalSection &g_cs );
改为:
EnterCriticalSection &g_csInt );
for intLoop = 0; intLoop < 100; intLoop++ )
{
g_intArray[intLoop] = 0;
}
LeaveCriticalSection &g_csInt );
EnterCriticalSection &g_csUint );
for intLoop = 0; intLoop < 100; intLoop++ )
{
g_uintArray[intLoop] = 0;
}
LeaveCriticalSection &g_csUint );
(2) 同时访问多个资源,必须始终按照完全相同的顺序请求对资源的访问。
这样才能避免死锁状态产生。离开的顺序没有关系。
Thread1:
EnterCriticalSection &g_csInt );
EnterCriticalSection &g_csUint );
for intLoop = 0; intLoop < 100; intLoop++ )
{
g_uintArray[intLoop] = g_intArray[intLoop];
}
LeaveCriticalSection &g_csInt );
LeaveCriticalSection &g_csUint );
Thread2:
EnterCriticalSection &g_csUint );
EnterCriticalSection &g_csInt );
for intLoop = 0; intLoop < 100; intLoop++ )
{
g_uintArray[intLoop] = g_intArray[intLoop];
}
LeaveCriticalSection &g_csInt );
LeaveCriticalSection &g_csUint );
改为:
Thread1:
EnterCriticalSection &g_csInt );
EnterCriticalSection &g_csUint );
for intLoop = 0; intLoop < 100; intLoop++ )
{
g_uintArray[intLoop] = g_intArray[intLoop];
}
LeaveCriticalSection &g_csInt );
LeaveCriticalSection &g_csUint );
Thread2:
EnterCriticalSection &g_csInt );
EnterCriticalSection &g_csUint );
for intLoop = 0; intLoop < 100; intLoop++ )
{
g_uintArray[intLoop] = g_intArray[intLoop];
}
LeaveCriticalSection &g_csInt );
LeaveCriticalSection &g_csUint );
(3) 不要长时间运行关键代码段。
EnterCriticalSection &g_cs );
SendMessage hWnd, WM_SOMEMSG, &g_s, 0 );
LeaveCriticalSection &g_cs );
改为:
EnterCriticalSection &g_cs );
sTemp = g_s;
LeaveCriticalSection &g_cs );
SendMessage hWnd, WM_SOMEMSG, &sTemp, 0 );
6. InitializeCriticalSection/InitializeCriticalSectionAndSpinCount差别?
InitializeCriticalSection函数的返回值为空并且不会创建事件内核对象,比较节省系统资源,但是一旦发生两个或多个线程争用关键代码段的情况,如果内存不足,关键代码段可能被争用,同时系统可能无法创建必要的事件内核对象。这时EnterCriticalSection函数将会产生一个EXCEPTION_INVALID_HANDLE异常。这个错误非常少见。如果想对这种情况有所准备,可以有两种选择。可以使用结构化异常处理方法来跟踪错误。当错误发生时,既可以不访问关键代码段保护的资源,也可以等待某些内存变成可用状态,然后再次调用EnterCriticalSection函数。
另一种选择是使用InitializeCriticalSectionAndSpinCount,第二个参数dwSpinCount中,传递的是在使线程等待之前它试图获得资源时想要循环锁循环迭代的次数。这个值可以是0至0x00FFFFFF之间的任何数字。如果在单处理器计算机上运行时调用该函数,该参数被忽略,并且始终设置为0。使用InitializeCriticalSectionAndSpinCount函数创建关键代码段,确保设置了dwSpinCount参数的高信息位。当该函数发现高信息位已经设置时,它就创建该事件内核对象,并在初始化时将它与关键代码段关联起来。如果事件无法创建,该函数返回FALSE。可以更加妥善地处理代码中的这个事件。如果事件创建成功,EnterCriticalSection将始终都能运行,并且决不会产生异常情况(如果总是预先分配事件内核对象,就会浪费系统资源。只有当代码不能容许EnterCriticalSection运行失败,或者有把握会出现争用现象,或者预计进程将在内存非常短缺的环境中运行时,才能预先分配事件内核对象)。
7. TryEnterCriticalSection和EnterCriticalSection的差别是什么?
如果EnterCriticalSection将一个线程置于等待状态,那么该线程在很长时间内就不能再次被调度。实际上,在编写得不好的应用程序中,该线程永远不会再次被赋予CPU时间。TryEnterCriticalSection函数决不允许调用线程进入等待状态。它的返回值能够指明调用线程是否能够获得对资源的访问权。TryEnterCriticalSection发现该资源已经被另一个线程访问,它就返回FALSE。在其他所有情况下,它均返回TRUE。运用这个函数,线程能够迅速查看它是否可以访问某个共享资源,如果不能访问,那么它可以继续执行某些其他操作,而不必进行等待。如果TryEnterCriticalSection函数确实返回了TRUE,那么CRITICAL_SECTION的成员变量已经更新。Windows98没有可以使用的TryEnterCriticalSection函数的实现代码。
|