本节书摘来自异步社区出版社《c++多线程编程实战》一书中的第2章,第2.5节,作者: 【黑山共和国】milos ljumovic(米洛斯 留莫维奇),更多章节内容可以访问云栖社区“异步社区”公众号查看。
进程之间的通信非常重要。虽然操作系统提供了进程间通信的机制,但是在介绍这些机制之前,我们先来考虑一些与之相关的问题。如果航空预定系统中有两个进程在同时销售本次航班的最后一张机票,怎么办?这里要解决两个问题。第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读取<code>p_free</code>,把它的值i6储存在自己的局部变量<code>f_slot</code>中。接着,发生了一个时钟中断,cpu认为进程5运行得太久了,决定转而执行进程6。然后,进程6也读取<code>p_free</code>,同样也把i6储存在自己的局部变量<code>f_slot</code>中。此时,两个进程都认为下一个可用的索引是i6。进程6现在继续运行,它把待拷贝文件的句柄储存在索引i6中,并更新<code>p_free</code>为i7。然后,系统让进程6睡眠。现在,进程5从原来暂停的地方再次开始运行。它查看自己的<code>f_slot</code>,发现可用的索引是i6,于是把自己待拷贝文件的句柄写到索引i6上,擦除了进程6刚写入的文件句柄。然后,进程5计算<code>f_slot+1</code>得i7,就把<code>p_free</code>设置为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++控制台应用程序,命名为<code>ipcdemo</code>。
2. 右键单击【解决方案资源管理器】,并选择【添加】-【新建项目】。选择c++【win32控制台应用程序】,添加一个新的默认c++控制台应用程序,命名为<code>ipcworker</code>。
3. 在<code>ipcworker.cpp</code>文件中输入下面的代码:
using namespace std;
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" <<
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;
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" <<
bsuccess = createprocess(text("..\debug\ipcworker.exe"),
text("blue"), null, null, false, 0, null, null, &startupinfoblue,
&processinformationblue);
cout << "cannot create process blue!" << endl << "error:t" <<
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);
default:
return defwindowproc(hwnd, umsg, wparam, lparam);
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);
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;
}<code>`</code>
示例分析
这次演示的示例有点难。我们需要两个单独的线程,所以在同一个解决方案中创建了两个项目。
为了简化这个示例,我们在主应用程序<code>ipcdemo</code>中创建了两个进程<code>ipcdemo</code>将在应用程序窗口中绘制一个区域。如果没有正确的通信和进程同步,就会发生多路访问共享资源的情况。考虑到操作系统会在进程间快速切换,而且大部分pc都有多核cpu,这很可能会导致两个进程同时画一个区域,即多个进程同时访问未保护的区域。先来看<code>ipcworker</code>,这个名称的意思是,需要进程为我们处理一些工作。
我们使用了一个映射对象(即,内存中为进程分配读取或写入的区域)。<code>ipcworker</code>或简称<code>worker</code>,要请求获得一个已命名的互斥量。如果获得互斥量,该进程就能处理并获取一个指向内存区域(文件映射)的指针,信息将储存在这个区域。必须获得互斥量,才能进行独占访问。进程在<code>waitforsingleobject</code>返回后获得互斥量。请看下面的语句:
pcommobject = ( pcommunicationobject )
mapviewoffile( hmapping, file_map_read, 0, 0, sizeof( communicationobject ) );<code>`</code>
调用<code>mapviewoffilewin32 api</code>获得指向文件映射对象的句柄(指针)。现在,进程可以从共享内存对象中读取并获得所需的信息了。该进程要读取<code>bexitloop</code>变量才能获悉是否继续执行。然后,该进程要读取待绘制区域窗口的句柄(<code>hwnd</code>)。最后,还需要<code>lsleeptimeout</code>变量记录进程睡眠多久。我们故意添加了sleep时间,因为进程间切换太快根本注意不到。
`
releasemutex( hmutex );`
调用<code>releasemutex win32 api</code>释放互斥量的所有权,让其他进程可以获得互斥量,继续执行其他任务。分析完<code>ipcworker</code>,我们来看<code>ipcdemo</code>项目。该项目定义了<code>_tagcommunicationobject</code>结构,用于整个文件映射过程中对象之间的通信。
正是因为<code>ipcdemo</code>在运行<code>worker</code>进程之前就创建了文件映射,所以从<code>worker</code>进程询问文件映射之前不用检查文件映射是否存在。<code>ipcdemo</code>创建并初始化应用程序窗口和待绘制区域后,创建了一个已命名的互斥量和文件映射。然后,用不同的命令行参数(用以区别)创建不同的进程。
<code>wndproc</code>例程处理<code>wm_command和wm_destroy</code>消息。当我们需要通知应用程序安全地关闭时,<code>wm_command</code>触发按钮按下事件,而<code>wm_destroy</code>则释放用过的文件映射,并向主线程消息队列寄送关闭消息:
postquitmessage( 0 );`
更多讨论
文件映射要与常驻磁盘的文件和常驻内存的文件视图一起运作。用内存的文件视图比用硬盘驱动的读写速度快。如果要用共享对象在进程之间处理一些简单的事情,选用文件映射是很好的编程习惯。如果把<code>createfilemapping api</code>的第1个参数设置为-1,磁盘中就不会有文件存在:
bsuccess = createprocess( text( "..\debug\ipcworker.exe" ),
text( "red" ), null, null, false, 0, null, null,
&startupinfored, &processinformationred );<code>`</code>`
visual studio在调试模式中只会从项目文件夹开始启动,不会从程序的<code>exe</code>文件夹开始启动。而且,visual studio默认把所有的win32项目都输出到同一个文件夹中。所以,在文件路径中,我们必须从项目文件夹返回上一级(文件夹),然后找到debug文件夹,整个项目的输出(<code>exe</code>)就在这个文件夹中。如果不想让vs这样启动<code>exe</code>,就必须改变<code>createprocess</code>调用的路径,或者添加通过命令行或其他类似方法访问文件路径的功能。