牧者

大风起兮云飞扬

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

在一个多线程的应用程序中,所有线程共享进程资源,协同工作。所以,线程之间的通信是编写多线程应用的必不可少的环节。线程之间的通信包括互斥、同步等,它是多线程设计中最难控制的部分,也是关键部分。

1、线程间的互斥(a执行时b不能执行)

 (1) 临界区
  在一个多线程的应用程序中,可能存在这样的危险:一个线程以某种其他线程不可预料的方式修改资源。
  例如两个线程都对同一个文件进行读写,两个线程都在进行绘图,一个线程正在使用一段内存而另一个线程却正在修改这段内存的值等,都可能出现不可预料的结果。
  可以把不允许多个线程交叉运行的一段程序称为临界区。它是由于多个线程共享公用数据或公用变量而引起的。
  临界区也被称为:访问公用数据的那段程序。

 (2) 互斥
  为使多个线程在进入自己的临界区时不出现问题,需要实现线程的互斥运行。它应满足:
   ·各线程平等,都可随时进入临界区。
   ·一个不在临界区执行的线程,不可以阻止其他线程进入临界区。
   ·当一个线程正在临界区内执行时,必须阻止其他线程进入临界区。
   ·多个线程申请进入临界区时,只能允许一个线程进入。
   ·一个申请进入临界区的线程,应该能在有限时间内进入,不会发生死锁。

 (3) 临界区类
  MFC定义了临界区封装类CCriticalSection,可以比较方便的实现线程间的互斥。
  首先定义一个CCriticalSection类对象,该对象通常应被定义为全局变量,以便跨线程使用。
  当线程要进入临界区时,调用其成员函数Lock()。若临界区空闲,该函数将锁定临界区;若临界区已经被锁定,该函数将被阻塞(不耗费机时),直至临界区解锁。
  当线程要退出临界区时,调用其成员函数Unlock()解锁,以便其他线程可以进入临界区。

 

  使用临界区,实现线程互斥。

2、线程间的同步(b执行的前提是a执行完毕)

  有时一个线程的执行条件是另一个线程的执行结果,所以只有等待另一个线程完成某项操作后,该线程才可继续执行。例如计算线程和打印线程。
  这种由于线程间的功能逻辑关系引起的,称为线程间的直接制约。而由于共享资源引起的线程执行速度的制约,称为间接制约。
  存在直接制约关系的一个线程可以在另一个线程的执行条件满足后,给对方发送相应消息或信号。这样,被制约的线程可以在条件不满足时处于阻塞状态,条件满足后,被对方唤醒继续工作。
  这就是线程间的同步方式。 

      等待函数和事件对象、互斥对象、信号量对象等一起使用,实现线程同步。


 (1) 等待函数

DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,
__in DWORD dwMilliseconds
);

  函数WaitForMultipleObjects()可以同时等待多各对象的信号状态。

DWORD WaitForMultipleObjects(DWORD nCount, CONST HANDLE *lpHandles, BOOL fWaitAll, DWORD dwMilliseconds);

  其中:nCount为等待的对象的个数。

     lpHandles为存放等待的对象的数组。
     fWaitAll等于TRUE指明所有对象均有信号时返回。
     fWaitAll等于FALSE指明其中之一有信号时返回。

参数

  hHandle[in]对象句柄。可以指定一系列的对象,如Event、Job、Memory resource notification、Mutex、Process、Semaphore、Thread、Waitable timer等。
  当等待仍在挂起状态时,句柄被关闭,那么函数行为是未定义的。该句柄必须具有 SYNCHRONIZE 访问权限。
 
  线程的句柄在WIN32中可以作为信号量使用(创建线程后,不能马上CloseHandle)。当线程结束时,其状态由非信号状态转变为信号状态。可以使用WaitForSingleObject函数来等线程对象。
  创建线程后,是不是关闭线程句柄?不关闭不会引起内存泄露,不过会占用一些系统资源。如果不再使用(比如得到退出代码,等待退出之类),还是关了好,毕竟是良好的编程习惯。
 
  dwMilliseconds[in]定时时间间隔,单位为milliseconds(毫秒).如果指定一个非零值,函数处于等待状态直到hHandle标记的对象被触发,或者时间到了。如果dwMilliseconds为0,对象没有被触发信号,函数不会进入一个等待状态,它总是立即返回。如果dwMilliseconds为INFINITE,对象被触发信号后,函数才会返回。

返回值

执行成功,返回值指示出引发函数返回的事件。它可能为以下值:
  
WAIT_ABANDONED 0x00000080L
The specified object is a mutex object that was not released by the thread that owned the mutex object before the owning thread terminated. Ownership of the mutex object is granted to the calling thread and the mutex state is set to nonsignaled.
If the mutex was protecting persistent state information, you should check it for consistency.
WAIT_OBJECT_0 0x00000000L
The state of the specified object is signaled.
WAIT_TIMEOUT 0x00000102L
The time-out interval elapsed, and the object's state is nonsignaled.
WAIT_FAILED(DWORD)0xFFFFFFFF
The function has failed. To get extended error information, callGetLastError.

 

WaitForSingleObject函数用来检测hHandle事件的信号状态,在某一线程中调用该函数时,线程暂时挂起,如果在挂起的dwMilliseconds毫秒内,线程所等待的对象变为有信号状态,则该函数立即返回;如果超时时间已经到达dwMilliseconds毫秒,但hHandle所指向的对象还没有变成有信号状态,函数照样返回。参数dwMilliseconds有两个具有特殊意义的值:0和INFINITE。若为0,则该函数立即返回;若为INFINITE,则线程一直被挂起,直到hHandle所指向的对象变为有信号状态时为止。
返回值:
WAIT_ABANDONED 0x00000080:当hHandle为mutex时,如果拥有mutex的线程在结束时没有释放核心对象会引发此返回值。
WAIT_OBJECT_0 0x00000000 :核心对象已被激活
WAIT_TIMEOUT 0x00000102:等待超时
WAIT_FAILED 0xFFFFFFFF :出现错误,可通过GetLastError得到错误代码
在这里举个例子:
先创建一个全局Event对象g_event:
CEvent g_event;
在程序中可以通过调用CEvent::SetEvent设置事件为有信号状态。
下面是一个线程函数MyThreadPro()
UINT MyThreadProc( LPVOID pParam )
{
    WaitForSingleObject(g_event,INFINITE);
    For(;;)
    {
      ………….
     }
     return 0;
}

  

在这个线程函数中只有设置g_event为有信号状态时才执行下面的for循环,因为g_event是全局变量,所以我们可以在别的线程中通过g_event. SetEvent控制这个线程。
还有一种用法就是我们可以通过WaitForSingleObject函数来间隔的执行一个线程函数的函数体
UINT MyThreadProc( LPVOID pParam )
{
    while(WaitForSingleObject(g_event,MT_INTERVAL)!=WAIT_OBJECT_0)
    {
        ………………
    }
    return 0;
}

  

在这个线程函数中可以通过设置MT_INTERVAL来控制这个线程的函数体多久执行一次,当事件为无信号状态时函数体隔MT_INTERVAL执行一次,当设置事件为有信号状态时,线程就执行完毕了。

  (2) CEvent事件对象

  MFC定义了CEvent类封装了系统的事件对象。它可以延迟一个线程的运行以等待另一个线程。
  事件对象可以是有信号状态或无信号状态,可以被等待函数调用。函数SetEvent()和ResetEvent()用于将事件置为有信号状态或无信号状态。
  事件对象有两种复位方式:
   ·手工复位:当有信号时,无论释放了多少等待的线程,只有在ResetEvent()后才置为无信号。
   ·自动复位:当有信号时,只要释放了一个等待的线程,就自动转为无信号。其他等待线程或同一线程的下一次等待不被释放。


  事件对象的初始化:
    CEvent(BOOL bInitiallyOwn = FALSE, BOOL bManualReset = FALSE);
  其中:bInitiallyOwn =FALSE初始化为无信号状态,=TRUE初始化为有信号状态。
     bManualReset =FALSE为自动复位,=TRUE为手工复位。

  当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程,同时操作系统会将该事件对象设置为无信号状态,这样,当对所保护的代码执行完成后,需要调用SetEvent函数将该事件对象设置为有信号状态。而人工重置的事件对象,在一个线程得到该事件对象之后,操作系统不会将该事件对象设置为无信号状态,除非显式地调用ResetEvent函数将其设置为无信号状态,否则该对象会一直是有信号状态。

(3)互斥对象

  MFC定义了CMutex类封装了系统的互斥对象。它可以保护共享资源防止其为多个线程同时访问。
  互斥对象与临界区非常相似,只是互斥对象可在进程间使用,而临界区只能用于同一进程的线程,但其速度相对较快。
  互斥对象在未被任何线程拥用时为有信号状态,当被任一个线程拥有时为无信号。
  当关于互斥对象的等待函数返回成功时,互斥对象变为无信号。当进程结束后,则需调用Unlock()函数以使互斥对象变为有信号

。(注意互斥对象和事件对象的相似和不同之处)
  互斥对象的初始化:
  CMutex(BOOL bInitiallyOwn = FALSE);
  其中:bInitiallyOwn 为互斥对象的初始状态。

(4)信号量

      MFC定义了CSemaphore类封装了系统的信号量对象。它是维护一个从0到指定的最大值的计数的同步对象。
当信号量对象创建时,拥有一个初始计数,当线程使用和释放信号量时,计数增减。只要计数大于0,信号量都处于有信号状态,若计数为0,信号量处于无信号状态。
信号量主要用于限制线程的最大数目。当关于信号量的等待函数返回成功时,信号量的计数自动减一。当进程结束后,则需调用Unlock()函数以使计数加一。
信号量对象的初始化:
   CSemaphore(LONG lInitialCount = 1, LONG lMaxCount = 1);
其中: lInitialCount 为信号量的初始计数。
     lMaxCount 为信号量的最大计数。

(5)互斥对象、事件对象与临界区的比较

互斥对象和事件对象都属于内核对象,利用内核对象进行线程同步时,速度较慢,但是利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。

临界区工作在用户方式下,同步速度较快,但在使用临界区时,很容易进入死锁状态,因为在等待进入临界区时无法设定超时值。

 

posted on 2018-03-20 18:10  牧者.D  阅读(483)  评论(0编辑  收藏  举报