线程间的通信实例

时间:2022-10-10 20:32:17
(一)实验目的:
     学习线程间的通信

(二)线程间的通信:

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

1、线程间的互斥

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

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

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

2、线程间的同步

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

 (2) 等待函数
  被制约的线程等待条件的满足时,可以使用Win32 API的信号等待函数。
  若等到信号或在时限内未等到信号,等待函数都将返回,否则线程阻塞。
  函数WaitForSingleObject()等待一个对象的信号状态。
    DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
  其中:hHandle可以是事件对象、互斥对象或信号量等。
     dwMilliseconds为等待时限,可以为0,也可以为INFINITE表示无限期等待。
  函数返回值为WAIT_OBJECT_0表明有信号或WAIT_TIMEOUT表明超时。
  函数WaitForMultipleObjects()可以同时等待多各对象的信号状态。
    DWORD WaitForMultipleObjects(DWORD nCount, CONST HANDLE *lpHandles, BOOL fWaitAll, DWORD dwMilliseconds);
  其中:nCount为等待的对象的个数。
     lpHandles为存放等待的对象的数组。
     fWaitAll等于TRUE指明所有对象均有信号时返回。
     fWaitAll等于FALSE指明其中之一有信号时返回。

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

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

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

(三)实验步骤:

1、建立一个MFC应用程序。

2、打开stdafx.h文件,加入:
    #include < afxtempl.h >
    #include < afxmt.h >

3、在View类的头文件中添加:
    #define BEZIER_POINTS 6 //曲线顶点数
    UINT DrawThread(LPVOID pView);

4、在View类中添加成员变量:
    HDC m_hDC; //为辅线程准备的绘图设备句柄
    int m_nLineNumber; //曲线总数,即线程总数
    CTypedPtrList< CObList, CWinThread* > m_listThread; //保存所有辅线程对象

5、在View类的构造函数中加入代码:
    m_hDC=NULL;
    m_nLineNumber=0;

6、添加菜单StartThread并在View类映射函数OnStartThread,加入代码:
    if(m_hDC==NULL) m_hDC=::GetDC(m_hWnd); //该设备句柄将在所有线程*享
    CWinThread* pp=AfxBeginThread(DrawThread,this);
    m_listThread.AddTail(pp); //将线程对象保存在集合中

7、添加菜单KillThread并在View类映射函数OnKillThread,加入代码:
    g_eventCloseThreads.SetEvent();
    while (!m_listThread.IsEmpty())
    {
      CWinThread* pThread = m_listThread.RemoveHead();
      WaitForSingleObject(pThread->m_hThread, INFINITE); //等待所有线程关闭
    }
    g_eventCloseThreads.ResetEvent();
    m_nLineNumber=0;
    Invalidate();

8、在View类中加入WM_DESTROY消息映射函数OnDestroy(),并加入代码:
    OnKillThreads();
    if(m_hDC!=NULL)
    {
      ::ReleaseDC(m_hWnd,m_hDC);
      m_hDC=NULL;
    }
    CView::OnDestroy();
9、在View类的cpp文件中手工加入全局变量:
    CEvent g_eventCloseThreads(FALSE,TRUE); //手工复位
    CCriticalSection g_criDrawing;

10、在View类的cpp文件中加入线程函数:
  UINT DrawThread(LPVOID pParam)
  {
    CMyView *pView=(CMyView*)pParam;
    srand(pView->m_nLineNumber*100);

    RECT rcCanvas;
    HPEN oldPen;
    HPEN Pen=::CreatePen(PS_SOLID,1,RGB(rand()%256,rand()%256,rand()%256)); //以随机颜色创建画笔

    int m_nMoveDirect=pView->m_nLineNumber++%2;
    POINT m_pointsForLine[BEZIER_POINTS];
    for(int i=0;i< BEZIER_POINTS;i++) //随机生成曲线顶点
    {
      m_pointsForLine[i].x=rand()%1000;
      m_pointsForLine[i].y=rand()%1000;
    }
    while(WaitForSingleObject(g_eventCloseThreads,0) == WAIT_TIMEOUT)//绘图中监视关闭事件
    {
      pView->GetClientRect(&rcCanvas);
      for(int i=0;i< BEZIER_POINTS;i++) //移动曲线(相对上次位置)
      {
        if(m_nMoveDirect)
        {
          if((m_pointsForLine[i].y+=2)>abs(rcCanvas.bottom-rcCanvas.top))
            m_pointsForLine[i].y=rcCanvas.top;
          if((m_pointsForLine[i].x+=2)>abs(rcCanvas.right-rcCanvas.left))
            m_pointsForLine[i].x=rcCanvas.left;
        } 
        else
        {
          if((m_pointsForLine[i].y-=2) < rcCanvas.top)
            m_pointsForLine[i].y=rcCanvas.bottom;
          if((m_pointsForLine[i].x-=2) < rcCanvas.left)
            m_pointsForLine[i].x=rcCanvas.right;
        }
      }

      g_criDrawing.Lock();
      ///////////////////// 绘制临界区(因为共用同一设备句柄)
      oldPen=(HPEN)::SelectObject(pView->m_hDC,Pen);
      ::PolyBezierTo(pView->m_hDC,m_pointsForLine,6);
      ::SelectObject(pView->m_hDC,oldPen);
      ::GdiFlush();
      ////////////////////////
      g_criDrawing.Unlock();
      Sleep(10);
    }
    AfxEndThread(0);
    return 0;
  }
11、编译运行,查看结果。

说明:
 ·在菜单StartThread响应函数中开启线程,可开多个。
 ·在菜单KillThread响应函数中关闭所有线程。
 ·在线程中利用CCriticalSection对象管理临界区
 ·利用CEvent对象实现线程的安全关闭