Windows线程的同步与互斥

来源:互联网 发布:svs软件 编辑:程序博客网 时间:2024/06/09 22:46

 系统中的所有线程都必须拥有对各种系统资源的访问权,这些资源包括内存堆栈,串口,文件,窗口和许多其他资源。如果一个线程需要独占对资源的访问权,那么其他线程就无法完成它们的工作。反过来说,也不能让任何一个线程在任何时间都能访问所有的资源。如果在一个线程从内存块中读取数据时,另一个线程却想要将数据写入同一个内存块,那么这就像你在读一本书时另一个人却在修改书中的内容一样。这样,书中的内容就会被搞得乱七八糟,结果什么也看不清楚。

线程需要在下面两种情况下互相进行通信:

1.当有多个线程访问共享资源而不使资源被破坏时。
2.当一个线程需要将某个任务已经完成的情况通知另外一个或多个线程时。Windows下线程同步互斥常用的几种方法:

1)CriticalSection: 临界区

适用范围: 单一进程的各线程之间用来排它性占有
特性: 不是内核对象,快速而有效。无法监测是否被线程放弃。如果在Critical Sections中间突然程序crash或是exit而没有调用LeaveCriticalSection,则结果是该线程所对应的内核不能被释放,该线程成为死线程。
函数: EnterCriticalSection LeaveCriticalSection

很好的封装:

class CritSect{public:    friend class Lock;    CritSect() { InitializeCriticalSection(&_critSection); }    ~CritSect() { DeleteCriticalSection(&_critSection); }private:    void Acquire(){ EnterCriticalSection(&_critSection); }    void Release(){ LeaveCriticalSection(&_critSection); }    CRITICAL_SECTION _critSection;};class Lock{public:     Lock(CritSect& critSect):_critSect(critSect) { _critSect.Acquire(); }     ~Lock(){ _critSect.Release(); }private:    CritSect& _critSect;};

调用:CritSect sect; Lock lock(sect);

2)Mutex: 互斥内核对象

适用范围: 不同线程之间用来排它性占有
特性: 核心对象,哪个线程拥有mutex,那么该mutex的ID和此线程的ID一样。
函数: CreateMutex ReleaseMutex

3)Event: 事件内核对象

适用范围: 用来控制对象信号的接收,常与信号系统结合起来
特性: 核心对象,有两种不同类型的事件对象。一种是人工重置的事件,另一种是自动重置的事件。当人工重置的事件得到通知时(signaled),等待该事件的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。
Microsoft为自动重置的事件定义了应该成功等待的副作用规则,即当线程成功地调用wait函数等待到该对象时,自动重置的事件就会自动重置到未通知状态(nonsignaled)。通常没有必要为自动重置的事件调用ResetEvent()函数,因为系统会自动对事件进行重置。但是,Microsoft没有为人工重置的事件定义成功等待的副作用,所以需要调用ResetEvent()函数将Event设置为未通知状态(nonsignaled)。当调用SetEvent触发Auto-reset的Event条件时,如果没有被条件阻塞的线程,那么此函数仍然起作用,条件变量会处在触发状态(和Linux的pthread_cond_signal()不同)。
函数: CreateEvent OpenEvent PulseEvent SetEvent ResetEvent 

4)Semaphore: 信号内核对象

适用范围: 用来限制资源占用
特性: 核心对象,没有拥有者,任何线程都可释放。信号量(Semaphore)内核对象对线程的同步方式与前面几种方法不同,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用 CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过 ReleaseSemaphore()函数将当前可用资源计数加1。在任何时候当前可用资源计数决不可能大于最大资源计数。
函数: CreateSemaphore OpenSemaphore ReleaseSemaphore

等待函数

1)WaitForSingleObject()

等待函数可使线程自愿进入等待状态,直到一个特定的内核对象变为已通知状态为止。

DWORD dw = WaitForSingleObject(hProcess, 5000);switch(dw){   case WAIT_OBJECT_0:      // The process terminated.      break;   case WAIT_TIMEOUT:      // The process did not terminate within 5000 milliseconds.      break;   case WAIT_FAILED:      // Bad call to function (invalid handle?)      break;}

2)WaitForMultipleObjects()

WaitForMultipleObjects与WaitForSingleObject函数很相似,区别在于它允许调用线程同时查看若干个内核对象的已知状态。WaitForMultipleObjects函数的返回值告诉调用线程,为什么它会被重新调度。可能的返回值是WAIT_FAILED和WAIT_TIMEOUT,这两个值的作用是很清楚的。如果为fWaitAll参数传递TRUE,同时所有对象均变为已通知状态,那么返回值是WAIT_OBJECT_0。如果为fWaitAll传递FALSE,那么一旦任何一个对象变为已通知状态,该函数便返回。在这种情况下,你可能想要知道哪个对象变为已通知状态。返回值是WAIT_OBJECT_0与(WAIT_OBJECT_0+dwCount-1)之间的一个值。换句话说,如果返回值不是WAIT_TIMEOUT,也不是WAIT_FAILED,那么应该从返回值中减去WAIT_OBJECT_0。产生的数字是作为第二个参数传递给WaitForMultipleObjects的句柄数组中的索引。该索引说明哪个对象变为已通知状态。下面是说明这一情况的一些示例代码:

HANDLE h[3];h[0] = hProcess1;h[1] = hProcess2;h[2] = hProcess3;DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000);switch(dw) {   case WAIT_FAILED:      // Bad call to function (invalid handle?)      break;   case WAIT_TIMEOUT:      // None of the objects became signaled within 5000 milliseconds.      break;   case WAIT_OBJECT_0 + 0:      // The process identified by h[0] (hProcess1) terminated.      break;   case WAIT_OBJECT_0 + 1:      // The process identified by h[1] (hProcess2) terminated.      break;   case WAIT_OBJECT_0 + 2:      // The process identified by h[2] (hProcess3) terminated.      break;}

3)SingleObjectAndWait()

DWORD SingleObjectAndWait(HANDLE hObjectToSignal,HANDLE hObjectToWaitOn,DWORD dwMilliseconds,BOOL fAlertable);
函数用于在单个原子方式的操作中发出关于内核对象的通知并等待另一个内核对象:hObjectToSignal参数必须标识一个互斥对象、信号对象或事件对象。hObjectToWaitOn参数用于标识下列任何一个内核对象:互斥对象、信标、事件、定时器、进程、线程、作业、控制台输入和修改通知。与平常一样,dwMilliseconds参数指明该函数为了等待该对象变为已通知状态,应该等待多长时间,而fAlertable标志则指明线程等待时该线程是否应该能够处理任何已经排队的异步过程调用。

4)MsgWaitForMultipleObjects(Ex)

MsgWaitForMultipleObjects和MsgWaitForMultipleObjectsEx这些函数与WaitForMultipleObjects函数十分相似。差别在于它们允许线程在内核对象变成已通知状态或窗口消息需要调度到调用线程创建的窗口中时被调度。
创建窗口和执行与用户界面相关的任务的线程,应该调用MsgWaitForMultipleObjectsEx函数,而不应该调用WaitForMultipleObjects函数,因为后面这个函数将使线程的用户界面无法对用户作出响应。

Windows下的生产者消费者问题:

View Code
#include "StdAfx.h"#include <windows.h>#include <stdio.h>#define BUFFER_SIZE 10typedef struct Prodcon{    int readpos;    int writepos; //position for reading and writing    int buffer[BUFFER_SIZE];}Prodcon;bool isOver = false;HANDLE hmutex; HANDLE hfullsemaphore; HANDLE hemptysemaphore; void init(Prodcon * pb){    memset(pb->buffer,0,sizeof(pb->buffer));    pb->readpos = 0;    pb->writepos = 0;}//store an integer in the buffervoid put(Prodcon* pb,int data){    WaitForSingleObject(hemptysemaphore,INFINITE);    WaitForSingleObject(hmutex,INFINITE);    pb->buffer[pb->writepos] = data;    pb->writepos++;    pb->writepos %= BUFFER_SIZE;    ReleaseMutex(hmutex);    ReleaseSemaphore(hfullsemaphore,1,NULL);}//read an integer from the bufferint get(Prodcon* pb){    int data;    WaitForSingleObject(hfullsemaphore,INFINITE);    WaitForSingleObject(hmutex,INFINITE);    data = pb->buffer[pb->readpos];    pb->readpos ++;    pb->readpos %= BUFFER_SIZE;    ReleaseMutex(hmutex);    ReleaseSemaphore(hemptysemaphore,1,NULL);                           return data;}DWORD WINAPI produce(LPVOID lppara){    Prodcon* pb = (Prodcon*)lppara;    while(1)    {        for(int i=1; i<=50; ++i)        {            put(pb,i);            printf("Thread %d put a data: %d\n",GetCurrentThreadId(),i);            Sleep(10); //producer is fast        }        isOver = true;        break;    }    return NULL;}DWORD WINAPI consume(LPVOID lppara){    Prodcon* pb = (Prodcon*)lppara;    while(1)    {        int d = get(pb);        printf("Thread %d get data: %d\n",GetCurrentThreadId(),d);        if(isOver == true && pb->readpos == pb->writepos)        {            printf("OVER!\n");            break;        }        Sleep(100); //consumer is slow    }    return NULL;}int main(){    DWORD writerdata;    DWORD readerdata;    DWORD readerdata1;    Prodcon pb;    init(&pb);    hmutex = CreateMutex(NULL,false,NULL);    //test produce/consume semaphore trigger    hfullsemaphore = CreateSemaphore(NULL,0,BUFFER_SIZE,NULL);    hemptysemaphore = CreateSemaphore(NULL,BUFFER_SIZE,BUFFER_SIZE,NULL);        if(CreateThread(NULL,0,produce,&pb,0,&writerdata)==NULL)        return -1;    if(CreateThread(NULL,0,consume,&pb,0,&readerdata)==NULL)        return -1;    if(CreateThread(NULL,0,consume,&pb,0,&readerdata1)==NULL)        return -1;    char ch;    while(1)    {        ch = getchar(); //press "e" to exit        if(ch == 'e') break;    }    printf("Program ends successfully\n");    CloseHandle(hmutex);    CloseHandle(hfullsemaphore);    CloseHandle(hemptysemaphore);    return 0;}

 

参考资料:
http://msdn2.microsoft.com/en-us/library/ms686360(VS.85).aspx
http://www.cppblog.com/mzty/archive/2008/07/29/57470.html

 

 

1. 仅一条语句用不用考虑线程同步的问题?  
  当使用高级语言编程时,我们往往会认为一条语句是最小的原子访问,CPU不会在这条语句中间运行其他的线程。这是错误的,因为即使非常简单的一条高级语言的语句,经编译器编译后也可能变成多行代码由计算机来执行。因此必须考虑线程同步的问题。任何线程都不应该通过调用简单的C语句来修改共享的变量。  
  2. 互锁函数有那些?  
  (1) LONG   InterlockedExchangeAdd   (   LPLONG   Addend,   LONG   Increment   );  
  Addend为长整型变量的地址,Increment为想要在Addend指向的长整型变量上增加的数值(可以是负数)。这个函数的主要作用是保证这个加操作为一个原子访问。  
  (2) LONG   InterlockedExchange(   LPLONG   Target,   LONG   Value   );  
  用第二个参数的值取代第一个参数指向的值。函数返回值为原始值。  
  (3) PVOID   InterlockedExchangePointer(   PVOID   *Target,   PVOID   Value   );  
  用第二个参数的值取代第一个参数指向的值。函数返回值为原始值。  
  (4) LONG   InterlockedCompareExchange(    
  LPLONG   Destination,   LONG   Exchange,   LONG   Comperand     );  
  如果第三个参数与第一个参数指向的值相同,那么用第二个参数取代第一个参数指向的值。函数返回值为原始值。  
  (5) PVOID   InterlockedCompareExchangePointer   (  
  PVOID   *Destination,   PVOID   Exchange,   PVOID   Comperand   );  
  如果第三个参数与第一个参数指向的值相同,那么用第二个参数取代第一个参数指向的值。函数返回值为原始值。  
  3. 为什么单CPU的计算机不应该使用循环锁?  
  举例说明:  
  BOOL   g_bResourceUse   =   FALSE;  
  ……  
  void   ThreadFunc1(     )  
  {  
  BOOL   bResourceUse   =   FALSE;  
  while(   1   )  
  {  
  bResourceUse   =   InterlockedExchange(   &g_bResourceUse,   TRUE   );  
  if(   bResourceUse   ==   FALSE   )  
  {  
  break;  
  }  
  Sleep(   0   );  
  }  
  ……  
  ……  
  ……  
  InterlockedExchange(   &g_bResourceUse,   FALSE   );  
  }  
  首先循环锁会浪费CPU时间。CPU必须不断地比较两个值,直到一个值由于另一个线程而“奇妙地”改变为止。而且使用该循环锁的线程都应该为同一优先级,并且应当使用SetProcessPriorityBoost函数或SetThreadPriorityBoost函数禁止线程优先级的动态提高功能,否则优先级较低的线程可能永远不能被调用。  
  4. 如何使用volatile声明变量?  
  如果是对共享资源的地址进行使用如&g_Resource那么可以不使用volatile,因为将一个变量地址传递给一个函数时,该函数必须从内存读取该值。优化程序不会对它产生任何影响。如果直接使用变量,必须有一个volatile类型的限定词。它告诉编译器,变量可以被应用程序本身以外的某个东西进行修改,这些东西包括操作系统,硬件或同时执行的线程等。volatile限定词会告诉编译器,不要对该变量进行任何优化,并且总是重新加载来自该变量的内存单元的值。否则编译器会把变量的值存入CPU寄存器,每次对寄存器进行操作。线程就会进入一个无限循环,永远无法唤醒。  
  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函数的实现代码。

原创粉丝点击