关键代码段

关键代码段是指一个小代码段,在代码能够执行前,它必须独占对某些共享资源的访问权。这是让若干行代码能够“以原子操作方式”来使用资源的一种方法。所谓原子操作方式,是指该代码知道没有别的线程要访问该资源。当然,系统仍然能够抑制你的线程的运行,而抢先安排其他线程的运行。不过,在线程退出关键代码段之前,系统将不给想要访问相同资源的其他任何线程进行调度。

下面是个有问题的代码,它显示了不使用关键代码段会发生什么情况:

const int MAX_TIMES = 1000;
int   g_nIndex = 0;
DWORD g_dwTimes[MAX_TIMES];

DWORD WINAPI FirstThread(PVOID pvParam) 
{
   while(g_nIndex < MAX_TIMES) 
   {
      g_dwTimes[g_nIndex] = GetTickCount();
      g_nIndex++;
   }
   return(0);
}


DWORD WINAPI SecondThread(PVOID pvParam)
{
   while(g_nIndex < MAX_TIMES) 
   {
      g_nIndex++;
      g_dwTimes[g_nIndex - 1] = GetTickCount();
   }
   return(0);
}

如果分开来看,这两个线程函数将会产生相同的结果,不过每个函数的编码略有不同。如果F i r s t T h r e a d函数自行运行,它将用递增的值填入g _ d w Ti m e s数组。如果S e c o n d T h r e a d函数也是自行运行,那么情况也一样。在理想的情况下,我们希望两个线程能够同时运行,并且仍然使g _ d w Ti m e s数组能够产生递增的值。但是,上面的代码存在一个问题,那就是g _ d w Ti m e s不会被正确地填入数据,因为两个线程函数要同时访问相同的全局变量。

下面是如何出现这种情况的一个例子。比如说,我们刚刚在只有一个C P U的系统上启动执行两个线程。操作系统首先启动运行S e c o n d T h r e a d(这种情况很可能出现),当S e c o n d T h r e a d将g _ n I n d e x递增为1之后,系统就停止该线程的运行,而让F i r s t T h r e a d运行。这时F i r s t T h r e a d将g _ d w Ti m e s [ 1 ] 设置为系统时间,然后系统停止该线程的运行,将C P U 时间重新赋予S e c o n d T h r e a d线程。然后S e c o n d T h r e a d将g _ d w Times[1 -1 ]设置为新的系统时间。由于这个操作发生在较晚的时间,因此新系统时间的值大于放入F i r s t T h r e a d数组中的时间值,另外要注意,g _ d w Ti m e s的索引1填在索引0的前面。数组中的数据被破坏了。

应该说明的是,这个例子的设计带有一定的故意性,因为要设计一个实际工作中的例子而不使用好几页的源代码是很难的。不过,通过这个例子,能够看到这个问题在实际工作中有些什么表现。考虑一下管理一个链接对象列表的情况。如果对该链接列表的访问没有取得同步,那么一个线程可以将一个项目添加给这个列表,而另一个线程则试图搜索该列表中的一个项目。如果两个线程同时给这个列表添加项目,那么这种情况会变得更加复杂。通过运用关键代码段,就能够确保在各个线程之间协调对数据结构的访问。

既然已经了解了存在的所有问题,那么下面让我们用关键代码段来修正这个代码:

const int MAX_TIMES = 1000;
int   g_nIndex = 0;
DWORD g_dwTimes[MAX_TIMES];
CRITICAL_SECTION g_cs;

DWORD WINAPI FirstThread(PVOID pvParam) 
{
   while(g_nIndex < MAX_TIMES) 
   {
      EnterCriticalSection(&g_cs);
      g_dwTimes[g_nIndex] = GetTickCount();
      g_nIndex++;
      LeaveCriticalSection(&g_cs);
   }
   return(0);
}
DWORD WINAPI SecondThread(PVOID pvParam) 
{
   while(g_nIndex < MAX_TIMES)
   {
      EnterCriticalSection(&g_cs);
      g_nIndex++;
      g_dwTimes[g_nIndex - 1] = GetTickCount();
      LeaveCriticalSection(&g_cs);
   }
   return(0);

}

这里指定了一个C R I T I C A L _ S E C T I O N数据结构g _ c s,然后在对E n t e r C r i t i c a l S e c t i o n和L e a v e C r i t i c a lS e c t i o n函数调用中封装了要接触共享资源(在这个例子中为g _ n I n d e x和g _ d w Ti m e s)的任何代码。注意,在对E n t e r C r i t i c a l S e c t i o n和L e a v e C r i t i c a l S e c t i o n的所有调用中,我传递了g _ c s的地址。 有一个关键问题必须记住。当拥有一项可供多个线程访问的资源时,应该创建一个C R I T I C A L _ S E C T I O N结构。由于我是在飞行旅途上编写这个代码的,让我描绘下面这个模拟情况。C R I T I C A L _ S E C T I O N就像飞机上的厕所,抽水马桶是你要保护的数据。由于厕所很小,每次只能一个人(线程)进入厕所(关键代码段)使用抽水马桶(受保护的资源)。 如果有多个资源总是被一道使用,可以将它们全部放在一个厕所里,也就是说可以创建一个C R I T I C A L _ S E C T I O N结构来保护所有的资源。 如果有多个不是一道使用的资源,比如线程1和线程2访问一个资源,而线程1和线程3访问另一个资源,那么应该为每个资源创建一个独立的厕所,即C R I T I C A L _ S E C T I O N结构。 现在,无论在何处拥有需要访问资源的代码,都必须调用E n t e r C r i t i c a l S e c t i o n函数,为它传递用于标识该资源的C R I T I C A L _ S E C T I O N结构的地址。这就是说,当一个线程需要访问一个资源时,它首先必须检查厕所门上的“有人”标志。C R I T I C A L _ S E C T I O N结构用于标识线程想要进入哪个厕所,而E n t e r C r i t i c a l S e c t i o n函数则是线程用来检查“有人”标志的函数。 如果E n t e r C r i t i c a l S e c t i o n函数发现厕所中没有任何别的线程(门上的标志显示“无人”),那么调用线程就可以使用该资源。如果E n t e r C r i t i c a l S e c t i o n发现厕所中有另一个线程正在使用,那么调用函数必须在厕所门的外面等待,直到厕所中的另一个线程离开厕所。 当一个线程不再执行需要访问资源的代码时,它应该调用L e a v e C r i t i c a l S e c t i o n函数。这样,它就告诉系统,它准备离开包含该资源的厕所。如果忘记调用L e a v e C r i t i c a l S e c t i o n,系统将认为该线程仍然在厕所中,因此不允许其他正在等待的线程进入厕所。这就像离开了厕所但没有换上“无人”的标志。 注意最难记住的一件事情是,编写的需要使用共享资源的任何代码都必须封装在E n t e r C r i t i c a l S e c t i o n和L e a v e C r i t i c a l S e c t i o n函数中。如果忘记将代码封装在一个位置,共享资源就可能遭到破坏。例如,如果我删除了F r i s t T h r e a d线程对E n t e r C r i t i c a l S e c t i o n和L e a v e C r i t i c a l S e c t i o n的调用, g _ n I n d e x和g _ d w Ti m e s变量就会遭到破坏。即使S e c o n d T h r e a d线程仍然正确地调用E n t e r C r i t i c a l S e c t i o n和L e a v e C r i t i c a l S e c t i o n,也会出现这种情况。 忘记调用E n t e r C r i t i c a l S e c t i o n和L e a v e C r i t i c a l S e c t i o n函数就像是不请求允许进入厕所。线程只是想努力挤入厕所并对资源进行操作。可以想象,只要有一个线程表现出这种相当粗暴的行为,资源就会遭到破坏。

你可能感兴趣的:(关键代码段)