1. 依赖关系
当线程或进程相互间需要通信或合作来完成一个共同目标时,它们就具有依赖关系(dependency relationship)。
1.1 通信依赖性
当线程A需要来自线程B的数据进行操作时,就发生第一种类型的依赖性,即通信依赖性(communication dependency)。线程B必须在线程A继续执行前与线程A所获取的数据进行通信。
1.2 合作依赖性
当线程A需要线程B拥有的资源,而且在线程A可以使用这些资源前,线程B必须释放它,此时就发生第二种依赖性,即合作依赖性。如果有两个并发执行的进程,它们都试图同时访问相同的资源,那么这些进程在成功执行前需要合作。
1.3 计数线程与进程依赖性
不管一个进程有多少线程,我们都可以通过比较进程内线程与其它每个线程的依赖性来理解整体线程关系。一旦理解了进程内所有线程对的关系,该进程的整体线程结构就知道了。
线程依赖性图对于说明一套线程或进程间的依赖关系有用。依赖性图可以用于与应用程序中相依赖的线程通信,而不需要通过源代码来搜索这种关系。这种信息可用于理解、保持以及测试多线程程序。
2. 进程间和线程间通信
程序员必须让拥有依赖关系的进程集或线程集协调,这样才能达到进程或线程的共同目标。可以使用两种技术来达到协调。第一种技术在具有通信依赖关系的两个进程间传递信息,这种技术称做进程间或线程间通信(interprocess or interthread communication)。第二种技术是同步,当线程或进程相互间具有合作依赖性时使用。
2.1 进程间通信
某个进程中的数据对于其它进程来说是受保护的。为了让一个进程访问另一个进程的数据,必须最终使用操作系统调用。与之类似,为了让一个进程知道另一个进程中文本片断中发生的事,必须在进程间建立一种通信方式,这也需要来自己OS API的帮助。当进程将数据发送到另一个进程时,称做IPC(进程间通信)。
2.2 进程间通信类型
当一个进程派生出其它进程时,我们说派生的进程是相关联的。关联进程使用一套技术来执行进程间通信,而不相关联的进程通常使用另一套技术来通信。
2.2.1环境变量、文件描述符
当创建一个子进程时,它接受了父进程许多资源的拷贝像文本、堆栈以及数据片断的拷贝。同时也接受了父进程的环境数据以及所有文件描述符的拷贝。子进程从父进程继承资源的过程中创造了进程间通信的一个机会。父进程可在它的数据片断或环境中设置一定的变量,然后执行fork()。了进程接受这些值。同样,父进程也可打开一个文件,推进到文件内的期望位置,然后执行fork(),子进程接着就可以在父进程离开读/写指针的准确位置访问该文件。
这种通信的缺陷在于它是单向的、一次性的。除了文件描述符外,如果子进程继承了任何其他数据,也仅仅是父进程数据拷贝的所有数据。一旦创建了子进程,由子进程对这些变量的任何改变都不会反映到父进程的数据中同样,父进程对它数据的任何改变也不会反映到子进程中。
2.2.2 命令行参数
命令行参数在调用一个exec或派生调用操作系统时传递给子进程。命令行参数通常在其中一个参数中作为NULL终止字符串传递给exec或派生函数调用。
2.2.3 管道
管道可用于在关联进程间以及无关联进程间进行通信。
管道是一种数据结构,像一个序列化文件一样访问,它形成了两个进程间的一种通信渠道。管道结构通过使用文件读/写方式来进行访问。它有两种基本类型:匿名管道(anonymous pipe)与命名管道(named pipe)。只有关联进程可以使用匿名管道来通信,无关联进程必须使用命名管道。
通过文件描述符或文件句柄提供对匿名管道的访问。对系统API的调用创建一个管道,并返回一个文件描述符。这个文件描述符用作read()或write函数的一个参数。当通过文件描述符调用read()或writer()函数时,数据的源和目标就是管道。
将管道用作两个无关联进程间的通信渠道,程序员必须使用命名管道,它可以看作一种具有某名字的特殊类型文件。进程可以根据它的名字访问这个管道。因为无关进程不能访问彼此的文件描述符,所以不能使用匿名管道。命名管道可以持久,创建他的程序退出后,它们仍然可以存在,还可在网络或分布环境中使用,可用于多对一关系中。
服务器程序负责建立客户与服务器进程间管道通信的协议。服务器程序必须指定访问模式、阻塞模式、管道类型、读取模式、缓冲器大小以及管道使用的即时计数。
命名管道不仅可用于无关联进程间、位于不同机器上的两进程间的通信,而且可用于多对一通信。可以建立服务器进程,允许同时通过多个客户访问命名管道。命名管道常常用于多线程服务器。
2.2.4 共享内存
共享内存被映射到使用它的每个进程的地址空间,所以看起来像另一个在进程内声明的变量。当一个进程写共享内存,所有的进程都立即知道写入的内容,而且可以访问。进程间共享内存的关系与函数间全局变量的关系相似,它可以被正在执行的所有进程访问。
OS/2环境中,共享内存可以是匿名的,也可以是有名的。如果共享内存为匿名的,那么必须使用某些形式的进程间通信来给希望应用共享内存的其它进程传递共享内存地址。如果内存是有名的,则没有必要在进程间形成通信渠道,其它进程可通过名字访问共享内存。通常使用共享内存比使用管道或队列更简单,也更有效,它可以用于保存大数据结构,然后进而可被从任何数量的进程中有效访问。这种内存可以充当面向对象数据库、集合对象或容器对象的一种永久存储空间。
2.2.5 动态数据交换(dynamic data exchange)
这是当今可用的进程间通信最强大和完善的形式之一。它使用消息传递、共享内存、事务协议、客户/服务器范例、同步规则以及会话协议来让数据和控制信息在进程间流动。动态数据交换对话(dynamic data exchange session, DDE)的基本模型是客户/服务器。服务器对来自客户的数据或动作做出反应。
一个服务器可以与任意数量的客户通信。一个客户也可以与任意数量的服务器通信。进程间通信的DDE形式使用消息系统来完成,它是由OS提供的。
所有DDE交互中主要有4种组件。它们的结合提供了进程间通信的能力。其中客户和服务器均代表系统内的进程。客户间的会话组成了通信的实质以及进程间传递的内容。进程间的共享内存用于保存任何种类的数据,从简单数据类型到复杂、面向对象数据库。
3. 线程间通信
进程和线程间的巨大的区别就是单个进程有自己的地址空间,而线程没有。当两个进程需要通信时,一般使用某种两进程外部的、两进程都能访问的数据结构来实现。使用这种数据结构传递数据或命令,两个进程可以通信。当两个线程通信时,它们一般使用属于同一进程的部分数据结构来实现。
线程相对于进程的一个重要优点是,线程可以共享全局变量(global variable)。
完成线程间通信的基本工具可包括:全局数据、全局变量、全局数据结构、参数和文件句柄(线程间共享文件的文件句柄。如果一个线程推进读/写指针,其它访问该文件的所有线程都获得相同的偏移)。
线程的参数是某些数据位置的地址。对线程中数据的任何修改都将反映在创建该数据的进程中。
与将全局变量或全局数据结构用作线程间通信机制相比,将参数用作线程间通信的一种形式可能更为理想,因为进程中的所有线程都可以访问全局数据,控制哪个线程什么时候做什么是困难的。通过限制只能访问那些可以访问全局数据,控制哪个线程什么时候做什么是困难的。而通过限制只能访问那些作为参数接受数据的线程,程序员可以控制对数据值和数据结构的访问,更好地控制同步。当在多线程环境中声明数据为全局时,程序的增强,维护和调试都变得更为困难。
当多线程间的共享文件作为一种线程间的通信形式时,同样要小心全局变量。如果threadA移动了文件指针,threadB也会受到影响。如果文件被threadB关闭,而threadA试图写入此文件,则显然会有冲突。在多线程环境中,序列化或同步化文件访问时也要小心,因为线程可以共享实际的地址、文件读指针、文件写指针、全局变量以及数据结构,所以必须使用同步和合作技术。