《C++多线程编程实战》——2.5 进程间通信(IPC)

2.5 进程间通信(IPC)

进程之间的通信非常重要。虽然操作系统提供了进程间通信的机制,但是在介绍这些机制之前,我们先来考虑一些与之相关的问题。如果航空预定系统中有两个进程在同时销售本次航班的最后一张机票,怎么办?这里要解决两个问题。第1个问题是,一个座位不可能卖两次。第2个问题是一个依赖性问题:如果进程A生成的某些数据是进程B需要读取的(如,打印这些数据),那么进程B在进程A准备好这些数据之前必须一直等待。进程和线程的不同在于,线程共享同一个地址空间,而进程拥有单独的地址空间。因此,用线程解决第1个问题比较容易。至于第2个问题,线程也同样能解决。所以,理解同步机制非常重要。

在讨论IPC之前,我们先来考虑一个简单的例子:CD刻录机。当一个进程要刻录一些内容时,会在特定的刻录缓冲区中设置文件句柄(我们立刻要刻录更多的文件)。另一个负责刻录的进程,检查待刻录的文件是否存在,如果存在,该进程将刻录文件,然后从缓冲区中移除该文件的句柄。假设刻录缓冲区有足够多的索引,分别编号为I0、I1、I2等,每个索引都能储存若干文件句柄。再假设有两个共享变量:p_next和p_free,前者指向下一个待刻录的缓冲区索引,后者指向缓冲区中的下一个空闲索引。所有进程都要使用这两个变量。在某一时刻,索引I0和I2为空(即文件已经刻录完毕),I3和I5已经加入缓冲。同时,进程5和进程6决定把文件句柄加入队列准备刻录文件。这一状况如图2.7所示。

图2.7

首先,进程5读取p_free,把它的值I6储存在自己的局部变量f_slot中。接着,发生了一个时钟中断,CPU认为进程5运行得太久了,决定转而执行进程6。然后,进程6也读取p_free,同样也把I6储存在自己的局部变量f_slot中。此时,两个进程都认为下一个可用的索引是I6。进程6现在继续运行,它把待拷贝文件的句柄储存在索引I6中,并更新p_free为I7。然后,系统让进程6睡眠。现在,进程5从原来暂停的地方再次开始运行。它查看自己的f_slot,发现可用的索引是I6,于是把自己待拷贝文件的句柄写到索引I6上,擦除了进程6刚写入的文件句柄。然后,进程5计算f_slot+1得I7,就把p_free设置为I7。现在,刻录缓冲区内部保持一致,所以刻录进程并未出现任何错误。但是,进程6再也接收不到任何输出。

进程6将被无限闲置,等待着再也不会有的输出。像这样两个或更多实体读取或写入某共享数据的情况,最终的结果取决于进程的执行顺序(即何时执行哪一个进程),这叫做竞态条件(race condition)。

如何避免竞态条件?大部分解决方案都涉及共享内存、共享文件以及避免不同的进程同时读写共享数据。换句话说,我们需要互斥(mutual exclusion)或一种能提供独占访问共享对象的机制(无论它是共享变量、共享文件还是其他对象)。当进程6开始使用进程5刚用完的一个共享对象时,就会发生糟糕的事情。

程序中能被访问共享内存的部分叫做临界区(critical section)。为了避免竞态条件,必须确保一次只能有一个进程进入临界区。这种方法虽然可以避免竞态条件,但是在执行并行进程时会影响效率,毕竟并行的目的是正确且高效地合作。要使用共享数据,必须处理好下面4个条件:

不允许同时有两个进程在临界区内;
不得对CPU的速度或数量进行假设;
在临界区外运行的进程不得阻碍其他进程;
不得有任何进程处于永远等待进入临界区。
以上所述如图2.8所示。过程A在T1时进入临界区。稍后,进程B在T2尝试进入其临界区,但是失败。因为另一个进程已经在临界区中,同一时间内只允许一个进程在临界区内。在T3之前,进程B必须被临时挂起。在进程A离开临界区时,进程B便可立即进入。最终,进程B离开临界区(T4时),又回到没有进程进入临界区的状态。

图2.8

下面是一个进程间通信的程序示例。我们创建的这个程序一开始就有两个进程,它们要在一个普通窗口中完成绘制矩形的任务。从某种程度上看,这两个进程需要相互通信,即当一个进程正在画矩形时,另一个进程要等待。

准备就绪
确定安装并运行了Visual Studio。

操作步骤
1. 创建一个新的默认C++控制台应用程序,命名为IPCDemo

2. 右键单击【解决方案资源管理器】,并选择【添加】-【新建项目】。选择C++【Win32控制台应用程序】,添加一个新的默认C++控制台应用程序,命名为IPCWorker

3. 在IPCWorker.cpp文件中输入下面的代码:

#include "stdafx.h"
#include <Windows.h>

#define COMMUNICATION_OBJECT_NAME TEXT("__FILE_MAPPING__")
#define SYNCHRONIZING_MUTEX_NAME TEXT( "__TEST_MUTEX__" )

typedef struct _tagCOMMUNICATIONOBJECT
{
  HWND hWndClient;
  BOOL bExitLoop;
  LONG lSleepTimeout;
} COMMUNICATIONOBJECT, *PCOMMUNICATIONOBJECT;

int _tmain(int argc, _TCHAR* argv[])
{
  HBRUSH hBrush = NULL;

  if (_tcscmp(TEXT("blue"), argv[0]) == 0)
  {
    hBrush = CreateSolidBrush(RGB(0, 0, 255));
  }
  else
  {
    hBrush = CreateSolidBrush(RGB(255, 0, 0));
  }

  HWND hWnd = NULL;
  HDC hDC = NULL;
  RECT rectClient = { 0 };
  LONG lWaitTimeout = 0;
  HANDLE hMapping = NULL;
  PCOMMUNICATIONOBJECT pCommObject = NULL;
  BOOL bContinueLoop = TRUE;

  HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, SYNCHRONIZING_MUTEX_NAME);
  hMapping = OpenFileMapping(FILE_MAP_READ, FALSE, COMMUNICATION_OBJECT_NAME);

  if (hMapping)
  {
    while (bContinueLoop)
    {
      WaitForSingleObject(hMutex, INFINITE);
      pCommObject = (PCOMMUNICATIONOBJECT)MapViewOfFile(hMapping,
        FILE_MAP_READ, 0, 0, sizeof(COMMUNICATIONOBJECT));

      if (pCommObject)
      {
        bContinueLoop = !pCommObject->bExitLoop;
        hWnd = pCommObject->hWndClient;
        lWaitTimeout = pCommObject->lSleepTimeout;
        UnmapViewOfFile(pCommObject);
        hDC = GetDC(hWnd);
        if (GetClientRect(hWnd, &rectClient))
        {
          FillRect(hDC, &rectClient, hBrush);
        }

        ReleaseDC(hWnd, hDC);
        Sleep(lWaitTimeout);
      }
      ReleaseMutex(hMutex);
    }
  }

  CloseHandle(hMapping);
  CloseHandle(hMutex);
  DeleteObject(hBrush);

  return 0;
}```
4. 打开`IPCDemo.cpp`,并输入下面的代码:

include "stdafx.h"

include

include

using namespace std;

define COMMUNICATION_OBJECT_NAME TEXT("_ FILE_MAPPING _")

define SYNCHRONIZING_MUTEX_NAME TEXT( "_ TEST_MUTEX _" )

define WINDOW_CLASS_NAME TEXT( "_ TMPWNDCLASS _" )

define BUTTON_CLOSE 100

typedef struct _tagCOMMUNICATIONOBJECT
{
  HWND hWndClient;
  BOOL bExitLoop;
  LONG lSleepTimeout;
} COMMUNICATIONOBJECT, *PCOMMUNICATIONOBJECT;

LRESULT CALLBACK WndProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
HWND InitializeWnd();
PCOMMUNICATIONOBJECT pCommObject = NULL;
HANDLE hMapping = NULL;

int _tmain(int argc, _TCHAR* argv[])
{
  cout << "Interprocess communication demo." << endl;
  HWND hWnd = InitializeWnd();
  if (!hWnd)
  {
    cout << "Cannot create window!" << endl << "Error:t" <<
      GetLastError() << endl;
    return 1;
  }
  HANDLE hMutex = CreateMutex(NULL, FALSE, SYNCHRONIZING_MUTEX_NAME);
  if (!hMutex)
  {
    cout << "Cannot create mutex!" << endl << "Error:t" <<
      GetLastError() << endl;
    return 1;
  }
  hMapping = CreateFileMapping((HANDLE)-1, NULL, PAGE_READWRITE, 0,
      sizeof(COMMUNICATIONOBJECT), COMMUNICATION_OBJECT_NAME);
  if (!hMapping)
  {
    cout << "Cannot create mapping object!" << endl << "Error:t"
      << GetLastError() << endl;
    return 1;
  }
  pCommObject = (PCOMMUNICATIONOBJECT)MapViewOfFile(hMapping,
    FILE_MAP_WRITE, 0, 0, 0);
  if (pCommObject)
  {
    pCommObject->bExitLoop = FALSE;
    pCommObject->hWndClient = hWnd;
    pCommObject->lSleepTimeout = 250;
    UnmapViewOfFile(pCommObject);
  }

  STARTUPINFO startupInfoRed = { 0 };
  PROCESS_INFORMATION processInformationRed = { 0 };
  STARTUPINFO startupInfoBlue = { 0 };
  PROCESS_INFORMATION processInformationBlue = { 0 };

  BOOL bSuccess = CreateProcess(TEXT("..\Debug\IPCWorker.exe"),
    TEXT("red"), NULL, NULL, FALSE, 0, NULL, NULL, &startupInfoRed,
    &processInformationRed);
  if (!bSuccess)
  {
    cout << "Cannot create process red!" << endl << "Error:t" <<
      GetLastError() << endl;
    return 1;
  }
  bSuccess = CreateProcess(TEXT("..\Debug\IPCWorker.exe"),
    TEXT("blue"), NULL, NULL, FALSE, 0, NULL, NULL, &startupInfoBlue,
    &processInformationBlue);
  if (!bSuccess)
  {
    cout << "Cannot create process blue!" << endl << "Error:t" <<
      GetLastError() << endl;
    return 1;
  }
  MSG msg = { 0 };
  while (GetMessage(&msg, NULL, 0, 0))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  UnregisterClass(WINDOW_CLASS_NAME, GetModuleHandle(NULL));
  CloseHandle(hMapping);
  CloseHandle(hMutex);
  cout << "End program." << endl;
  return 0;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch (uMsg)
  {
    case WM_COMMAND:
    {
      switch (LOWORD(wParam))
      {
        case BUTTON_CLOSE:
        {
           PostMessage(hWnd, WM_CLOSE, 0, 0);
           break;
        }
      }
      break;
    }
    case WM_DESTROY:
    {
      pCommObject = (PCOMMUNICATIONOBJECT)MapViewOfFile(hMapping,
              FILE_MAP_WRITE, 0, 0, 0);
      if (pCommObject)
      {
        pCommObject->bExitLoop = TRUE;
        UnmapViewOfFile(pCommObject);
      }
      PostQuitMessage(0);
      break;
    }
    default:
    {
      return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
  }
  return 0;
}

HWND InitializeWnd()
{
  WNDCLASSEX wndEx;
  wndEx.cbSize = sizeof(WNDCLASSEX);
  wndEx.style = CS_HREDRAW | CS_VREDRAW;
  wndEx.lpfnWndProc = WndProc;
  wndEx.cbClsExtra = 0;
  wndEx.cbWndExtra = 0;
  wndEx.hInstance = GetModuleHandle(NULL);
  wndEx.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
  wndEx.lpszMenuName = NULL;
  wndEx.lpszClassName = WINDOW_CLASS_NAME;
  wndEx.hCursor = LoadCursor(NULL, IDC_ARROW);
  wndEx.hIcon = LoadIcon(wndEx.hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
  wndEx.hIconSm = LoadIcon(wndEx.hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
  if (!RegisterClassEx(&wndEx))
  {
    return NULL;
  }
  HWND hWnd = CreateWindow(wndEx.lpszClassName,
    TEXT("Interprocess communication Demo"),
    WS_OVERLAPPEDWINDOW, 200, 200, 400, 300, NULL, NULL,
    wndEx.hInstance, NULL);
  if (!hWnd)
  {
    return NULL;
  }
  HWND hButton = CreateWindow(TEXT("BUTTON"), TEXT("Close"),
    WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | WS_TABSTOP,
    275, 225, 100, 25, hWnd, (HMENU)BUTTON_CLOSE, wndEx.hInstance,
    NULL);
  HWND hStatic = CreateWindow(TEXT("STATIC"), TEXT(""), WS_CHILD |
    WS_VISIBLE, 10, 10, 365, 205, hWnd, NULL, wndEx.hInstance, NULL);
  ShowWindow(hWnd, SW_SHOW);
  UpdateWindow(hWnd);

  return hStatic;
}`
示例分析
这次演示的示例有点难。我们需要两个单独的线程,所以在同一个解决方案中创建了两个项目。

为了简化这个示例,我们在主应用程序IPCDemo中创建了两个进程IPCDemo将在应用程序窗口中绘制一个区域。如果没有正确的通信和进程同步,就会发生多路访问共享资源的情况。考虑到操作系统会在进程间快速切换,而且大部分PC都有多核CPU,这很可能会导致两个进程同时画一个区域,即多个进程同时访问未保护的区域。先来看IPCWorker,这个名称的意思是,需要进程为我们处理一些工作。

我们使用了一个映射对象(即,内存中为进程分配读取或写入的区域)。IPCWorker或简称Worker,要请求获得一个已命名的互斥量。如果获得互斥量,该进程就能处理并获取一个指向内存区域(文件映射)的指针,信息将储存在这个区域。必须获得互斥量,才能进行独占访问。进程在WaitForSingleObject返回后获得互斥量。请看下面的语句:

HANDLE hMutex = OpenMutex( MUTEX_ALL_ACCESS, FALSE, SYNCHRONIZING_MUTEX_NAME );```
我们要为互斥量(`hMutex`)分配一个句柄,调用`OpenMutex Win32 API`获得该已命名互斥量的句柄(如果有互斥量的话)。请看下面的语句:
`
WaitForSingleObject( hMutex, INFINITE );`
执行完这条语句后,当WaitForSingleObjectAPI返回时继续执行。

pCommObject = ( PCOMMUNICATIONOBJECT )
  MapViewOfFile( hMapping, FILE_MAP_READ, 0, 0, sizeof( COMMUNICATIONOBJECT ) );`
调用MapViewOfFileWin32 API获得指向文件映射对象的句柄(指针)。现在,进程可以从共享内存对象中读取并获得所需的信息了。该进程要读取bExitLoop变量才能获悉是否继续执行。然后,该进程要读取待绘制区域窗口的句柄(hWnd)。最后,还需要lSleepTimeout变量记录进程睡眠多久。我们故意添加了sleep时间,因为进程间切换太快根本注意不到。
`
ReleaseMutex( hMutex );`
调用ReleaseMutex Win32 API释放互斥量的所有权,让其他进程可以获得互斥量,继续执行其他任务。分析完IPCWorker,我们来看IPCDemo项目。该项目定义了_tagCOMMUNICATIONOBJECT结构,用于整个文件映射过程中对象之间的通信。

文件映射(file mapping)是把文件的内容与一个进程的一部分虚拟地址空间相关联。操作系统创建一个文件映射对象(也叫做区域对象[section object])来维护这种关联。文件视图(file view)是进程用于访问文件内容的虚拟地址空间部分。有了文件映射,进程不仅能使用随机I/O和顺序I/O,而且无需把整个文件映射到内存中就能高效地使用大型数据文件(如,数据库)。多个进程还可以用已映射的内存文件来共享数据。详见MSDN(http://msdn.microsoft.com/en-us/library/windows/desktop/aa366883%28v=vs.85%29.aspx)。

正是因为IPCDemo在运行Worker进程之前就创建了文件映射,所以从Worker进程询问文件映射之前不用检查文件映射是否存在。IPCDemo创建并初始化应用程序窗口和待绘制区域后,创建了一个已命名的互斥量和文件映射。然后,用不同的命令行参数(用以区别)创建不同的进程。

WndProc例程处理WM_COMMAND和WM_DESTROY消息。当我们需要通知应用程序安全地关闭时,WM_COMMAND触发按钮按下事件,而WM_DESTROY则释放用过的文件映射,并向主线程消息队列寄送关闭消息:
`
PostQuitMessage( 0 );`
更多讨论
文件映射要与常驻磁盘的文件和常驻内存的文件视图一起运作。用内存的文件视图比用硬盘驱动的读写速度快。如果要用共享对象在进程之间处理一些简单的事情,选用文件映射是很好的编程习惯。如果把CreateFileMapping API的第1个参数设置为-1,磁盘中就不会有文件存在:

CreateFileMapping( ( HANDLE ) -1, NULL, PAGE_READWRITE, 0,
   sizeof( COMMUNICATIONOBJECT ), COMMUNICATION_OBJECT_NAME );```
这真是再好不过了,因为我们正打算使用一部分内存,这样更快,而且也够用了。

调用`IPCWorker`进程时要注意。像下面这样设置`CreateProcess`,以供调试:

bSuccess = CreateProcess( TEXT( "..\Debug\IPCWorker.exe" ),
   TEXT( "red" ), NULL, NULL, FALSE, 0, NULL, NULL,
   &startupInfoRed, &processInformationRed );``
Visual Studio在调试模式中只会从项目文件夹开始启动,不会从程序的exe文件夹开始启动。而且,Visual Studio默认把所有的Win32项目都输出到同一个文件夹中。所以,在文件路径中,我们必须从项目文件夹返回上一级(文件夹),然后找到Debug文件夹,整个项目的输出(exe)就在这个文件夹中。如果不想让VS这样启动exe,就必须改变CreateProcess调用的路径,或者添加通过命令行或其他类似方法访问文件路径的功能。

时间: 2024-07-31 14:35:06

《C++多线程编程实战》——2.5 进程间通信(IPC)的相关文章

《C++多线程编程实战》导读

前言 C++多线程编程实战多线程编程正逐渐成为IT行业和开发人员关注的焦点.开发商希望开发出用户友好.界面丰富,而且能并发执行的应用程序.强大的C++语言和本地Win32 API特性为多线程编程提供了良好开端.有了强大的C++,可以轻松地创建不同类型的应用程序,执行并行,而且还能优化现有的工作. 本书是一本实践为主.通俗易懂的Windows多线程编程指导.你将学到如何从多线程方案中受益,增强你的开发能力,构建更好的应用程序.本书不仅讲解了创建并行代码时遇到的问题,而且还帮助读者详细理解同步技术.

《C++多线程编程实战》——第2章 进程和线程的概念2.1 简介

第2章 进程和线程的概念 C++多线程编程实战本章介绍以下内容: 进程和线程解释进程模型进程的实现进程间通信(IPC)解决典型的IPC问题线程模型的实现线程的用法在用户空间实现线程在内核实现线程 2.1 简介 现在的计算机能同时处理多件事,许多Windows用户还没有完全意识到这一点.我们举例说明一下.当启动PC系统时,许多进程都在后台启动(例如,管理电子邮件的进程.负责更新病毒库的进程等).通常,用户在执行其他任务时(如,上网),还会打印文件或播放CD.这些活动都需要管理.支持多进程的多任务系

Java多线程编程实战之不提倡的方法

不提倡使用的方法是为支持向后兼容性而保留的那些方法,它们在以后的版本中可能出现,也可能不出现.Java 多线程支持在版本 1.1 和版本 1.2 中做了重大修订,stop().suspend() 和 resume() 函数已不提倡使用.这些函数在 JVM 中可能引入微妙的错误.虽然函数名可能听起来很诱人,但请抵制诱惑不要使用它们. 调试线程化的程序 在线程化的程序中,可能发生的某些常见而讨厌的情况是死锁.活锁.内存损坏和资源耗尽. 死锁 死锁可能是多线程程序最常见的问题.当一个线程需要一个资源而

《C++多线程编程实战》——第1章 C++概念和特性简介1.1 介绍

第1章 C++概念和特性简介 C++多线程编程实战 本章介绍以下内容: 创建一个C++项目 程序结构.执行流.运行时对象 结构编程方法 理解面向对象编程方法 解释继承.重载和覆盖 理解多态 事件处理器和消息传递接口 链表.队列.栈示例 1.1 介绍 系统所执行的程序的进程或抽象是所有操作系统的核心概念.现在,绝大多数的操作系统在同一时间内都可以进行多项操作.例如,计算机在用户编辑Word文档时,还可以打印该文档.从硬盘缓冲区读数据.播放音乐等.在多任务操作系统中,中央处理单元(CPU)在程序中快

《JAVA多线程编程实战指南》之Two-phase Termination(两阶段终止)模式

本文是<JAVA多线程编程实战指南>的样章,感谢作者授权并发网(ifeve.com)发表此文.感谢demochen整理此文. 5.1Two-phase Termination模式简介 停止线程是一个目标简单而实现却不那么简单的任务.首先,Java没有提供直接的API用于停止线程.此外,停止线程还有一些额外的细节需要考虑,如停止的线程处于阻塞(如等待锁)或者等待状态(等待其他线程),尚有未处理完的任务等. Two-phase Termination模式通过将停止线程这个动作分解为准备阶段和执行阶

《C++多线程编程实战》——2.6 解决典型的IPC问题

2.6 解决典型的IPC问题 进程间通信非常重要,它的实现也很复杂.操作系统的设计人员和开发人员要面临各种问题.接下来,我们讲解一些最常见的问题. 2.6.1 哲学家就餐问题 本节讨论的哲学家就餐问题的定义,选自Andrew S. Tanenbaum所著的Mordern Operating Systems(<现代操作系统>)第三版.作者在书中提供了解决方案. 1965年,Dijkstra提出并解决了一个同步问题,他称之为哲学家就餐问题.这个问题简单地描述如下:5位哲学家围坐在一张圆桌边.每位哲

Java多线程编程实战之基础

在 Java 程序中使用多线程要比在 C 或 C++ 中容易得多,这是因为 Java 编程语言提供了语言级的支持.本文通过简单的编程示例来说明 Java 程序中的多线程是多么直观.读完本文以后,用户应该能够编写简单的多线程程序. 为什么会排队等待? 下面的这个简单的 Java 程序完成四项不相关的任务.这样的程序有单个控制线程,控制在这四个任务之间线性地移动.此外,因为所需的资源 - 打印机.磁盘.数据库和显示屏 -- 由于硬件和软件的限制都有内在的潜伏时间,所以每项任务都包含明显的等待时间.因

Java多线程编程实战之限制优先级

限制线程优先级和调度 Java 线程模型涉及可以动态更改的线程优先级.本质上,线程的优先级是从 1 到 10 之间的一个数字,数字越大表明任务越紧急.JVM 标准首先调用优先级较高的线程,然后才调用优先级较低的线程.但是,该标准对具有相同优先级的线程的处理是随机的.如何处理这些线程取决于基层的操作系统策略.在某些情况下,优先级相同的线程分时运行:在另一些情况下,线程将一直运行到结束.请记住,Java 支持 10 个优先级,基层操作系统支持的优先级可能要少得多,这样会造成一些混乱.因此,只能将优先

Java的多线程编程实战

 在 Java 程序中使用多线程要比在 C 或 C++ 中容易得多,这是因为 Java 编程语言提供了语言级的支持.本文通过简单的编程示例来说明 Java 程序中的多线程是多么直观.读完本文以后,用户应该能够编写简单的多线程程序. 为什么会排队等待? 下面的这个简单的 Java 程序完成四项不相关的任务.这样的程序有单个控制线程,控制在这四个任务之间线性地移动.此外,因为所需的资源 ? 打印机.磁盘.数据库和显示屏 -- 由于硬件和软件的限制都有内在的潜伏时间,所以每项任务都包含明显的等待时间.