进程间和线程间通信(原文章出自http://blog.sina.com.cn/s/blog_4a93ccea0102ea1w.html)
1.许多程序和应用一起工作达到某个共同目的的任务集。每个任务在开始执行前等待前一个任务完成。为了完成共同目标,相关线程或进程必须相互合作与通信。
2.依赖关系:对于任意两个线程或进程,存在4种依赖关系(如图)
(1)通信依赖性:当线程A需要来自线程B的数据进行操作时(单向依赖)
(2)合作依赖性:当线程A需要线程B拥有的资源,而且在线程A可以使用这些资源前,线程B必须释放它。如count计数:findFiles()搜索列表中的文件,搜索一个文件count+1,findWords()搜索文件中的单词列表,搜到一个文件后count-1,两者必须合作完成。
(3)计数线程与进程依赖性:依赖性图仅对少量线程或进程有用。
3.进程间和线程间通信:
2种技术实现进程或线程的共同目标:
(1)在具有通信依赖关系的两个进程间传递信息。
(2)同步。当线程或进程相互间具有合作依赖性时使用。
4.进程间通信:数据对于其他进程来说是受保护的,为了让一个进程访问另一进程的数据,必须最终使用操作系统调用。当进程将数据发送到另一个进程时,必须在进程间建立一种通信方式IPC(进程间通信)。
关联(派生)进程和非关联(非父子关系)进程实现通信的技术
(1)关联进程实现进程间通信技术:继承资源和命令行参数。
当创建一个子进程,它接收了父进程许多资源(如文本、堆栈以及数据片段)的拷贝。父进程可在数据片段或环境中设置变量,然后执行fork(),子进程接收这些值,同样父进程定位一个文件期望的位置,然后执行fork(),子进程就可以在父进程离开读/写指针的准确位置访问该文件。缺点:单向、一次性通信。通信像指挥棒传递。一旦父进程传递了某些资源的拷贝,子进程对他的使用就是独立的,必须使用原始传递资源。另一种单向、一次性通信是命令行参数,它是最简单形式的进程间通信。只限制于关联进程。
(2)非关联进程实现进程间通信技术:管道(pipe).
用于在关联进程间以及无关联进程间进行通信。管道是一种像序列化文件一样访问的数据结构。管道结构通过使用文件和写方式来访问。如进程A希望发送数据给进程B,那么进程A向管道写入数据,进程B必须读取管道接收数据。管道可用于双向通信。写入的数据和读取的数据是由一系列字节简单表示。管道可以在程序的整个执行期间使用,在进程间发送和接收数据。
两种管道类型:a.匿名管道(关联进程)——借助于文件描述符实现(子进程从父进程继承了文件描述符);
b.命名管道(非关联进程)——具有某名字的特殊类型文件,可以建立一个IPC客户/服务器模型,可以跨网络访问命令管道。
命令管道相对于匿名管道的优点:命名管道可以被无关联进程使用、命名管道可以持久、命名管道可以再网络或分布环境中使用、命名管道容易用于多对一关系中。
两者都是通过read()或write()函数来访问。
WIN32环境命名管道工作所需的API调用:
CallNamePipe():连接到一个管道实例,然后写入一条消息、读取消息,并关闭管道句柄。只能被客户进程和消息管道使用
PeekNamedPipe():读取通过管道传输的数据,而不会从字节管道或消息管道取出数据。返回管道实例的信息。
DisconnectNamedPipe():客户和进程使用完管道实例后,服务器进程调用此函数关闭客户进程的连接,客户句柄变成无效,并抛弃管道中的所有未读数据。
TransactNamedPipe():写请求消息并读答复消息。如调用进程的管道句柄设置成消息读取模式,就可以与消息管道一起使用。
FlushFileBuffers():服务器进程调用该函数确保客户进程读取写入管道的所有字节和消息。如果函数没有返回到客户进程,它就从管道读取了所有的数据。
GetNamedPipeInfo():获取命名管道的信息。它返回管道的类型、输入和输出缓冲器的大小以及创建管道实例的最大数。
GetNamedPipeHandleState():提供句柄,包括管道的读和等待模式、管道实例的当前数以及通过网络进行通信的管道的其他所有重要信息。
SetNamedPipeHandleState():设置管道句柄的读和等待模式。它还控制搜集字节的最大数,或者客户进程与远程服务器通信时传输消息前等待的最长时间。
ReadFile()、WriteFile():用于字节和消息管道。使用一个事件对象来通知其完成。
【注意】服务器程序负责建立客户与服务器进程间管道通信的协议。服务器程序必须指定访问模式、阻塞模式、管道类型、读取模式、缓冲器大小及管道使用的即时计数。
服务器建立管道来发送和接收数据(如下图)。服务器制定这个管道将处于阻塞模式。如果请求read(),而且管道式空的,则read()阻塞,直到数据可以被读取为止。如果请求的是管道的write(),且管道已满,则write()阻塞,直到管道中腾出更多的空间放入数据为止。
命名管道常常用于多线程服务器。
(3)共享内存:实现进程间通信的一种方式。希望访问该内存块的其他进程必须请求对它的访问,或由创建它的进程授予访问内存块的权限。共享内存被映射到使用它的每个进程的地址空间。当一个进程写共享内存,所有进程都立即知道写入的内容,并且可以访问。相当于函数间全局变量的关系类似。进程可能共享一个逻辑地址,也可以共享某些物理地址。
通常使用共享内存比使用管道或队列更简单更有效。共享内存块可用于保存大数据结构。可用于映射文件到内存,使得应用程序减轻了常规文件访问的I/O操作代价。
WIN32 环境创建和使用共享内存所需API:
CreateFileMapping() 创建一个文件映射对象,对文件无限制。
OpenFileMapping() 获取映射对象的句柄
MapViewOfFile() 获取共享内存的起始地址
(4)动态数据交换(DDE):当今最强大最完善的进程间通信形式之一。
使用消息传递、共享内存、事务协议、客户/服务器范例、同步规则以及会话协议来让数据和控制信息在进程间流动。
DDE基本模型:客户/服务器。一对多或多对一。DDE代理既可以是客户,也可以是服务器。进程可以从一个正为另一个进程执行服务的DDE代理请求服务。
DDE交互的4种组件:DDE服务器(应用、主题、项)、DDE客户、DDE会话链接(互相传递的一系列消息,热链接和冷链接、5种主要类型的事务——请求、建议、取消建议、执行、发送)、共享内存和DDE会话。
DDE客户和服务器使用应用标识、主题、项以及共享内存的结合进行会话,一旦对主题-项对达到一致,就可以在客户进程和服务器进程间交换数据。
5.线程间通信
当两个线程通信时,一般使用属于同一个进程的部分数据结构来实现。进程内的两个线程可以访问同一个数据片段,可以相互从堆栈片段传递值。在需要并发的多种编程场合,线程创建以及线程间通信机制的效率比进程间通信的效率高很多,使得线程成为更具吸引力的方案。
线程间通信类型:a.全局数据、b.全局变量、c.全局数据结构 d.线程间通信的参数 e.文件句柄
[注意]如果多个线程用于写入全局数据结构,必须使用合作技术。
[程序]创建2个线程threadA、threadB,其中4个全局变量(1个有理对象M和3个集迭代器A\B\C)和3个全局数据结构(SetA,SetB,SetC)。 threadA()功能:将有理数插入SetA和SetB,使用set_intersection()求SetA和SetB的交集,交集结果为SetC。threadB()功能:SetA和SetB中插入有理数,使用set_union()求SetC和SetB的并集,并集结果为SetA。主线程pthead_jion()等待threadA()和threadB()完成,完成后遍历SetA、SetB、SetC,将他们的成员发送到标准输出。
【注意】在多线程环境中,同时访问全局变量和数据结构必须同步化(synchronized)。
6.线程间通信的参数传递:
创建线程:pthread_create(ThreadId,NULL,threadFunction,(void *)X);
当使用线程创建API给一个线程传递指针时,该指针就像exec函数家族的参数一样使用。exec函数家族用于创建子进程,并包含作为形参传递给子进程的参数。
使用exec函数家族与使用线程创建API传递参数的区别:
exec函数中的参数表示一种单向通信,子进程只复制在exec调用中传递的参数值。当线程接收到参数时,它并不是一个拷贝,而是某些数据位置的地址。对线程中数据的任何修改都将反应在创建该数据的进程中,这与在exec调用中子进程所接收的参数相反,子进程对参数所做的任何修改不会反映在父进程中。
[程序]演示线程参数的使用:
注意:由threadA()和threadB()对N指针对象的任何修改都将反映到threadA()、threadB()、main()。因为所有的3个线程可能并发执行,所以不要修改保存在N中的对象,除非修改是同步的。但在此程序中,保存在 N 中的对象只用在读或复制的场合,不必担心数据竞争的发生。
【重点】将参数用作线程间通信的一种形式可能较之将全局变量或全局数据结构用作线程间通信机制更理想。通过监视那些作为参数接收数据的线程可以控制同步。
7.文件句柄:
在多线程间的共享文件作为一种线程的通信形式时,要小心全局变量。在多线程环境中,序列化或同步化文件访问时也要小心,必须使用同步和合作技术。