进程线程通信方法总结

时间:2021-12-10 23:57:08

多线程编程需面临的问题总结:
1)安全的数据共享(通信)问题:同时一方写,一方读或者同时一个写,多个读。
为了保证数据能安全的共享,引出了线程间同步的问题。不可避免的用到了锁。而一旦引入锁,编程就变得复杂了起来。所以,先人们想了很多种方法来避免使用锁而实现数据通信,引入了环形缓冲区队列实现;本质上,环形缓冲区队列是通过把数据往缓冲区里备份了一份来实现。同样,我们也可以通过发送消息的方式来实现线程间数据的通信。事实上,有人直接利用windows自带的消息通信机制解决了复杂的线程间的数据通信。
2)线程固有的同步需求
由于把一项任务拆分到几个线程里面去做,就算没有数据通信,也面临着线程之间同步的问题。比如只有A做了某件事后,B才能接着往下走。
不过,这里的A做完某件事通知B这个过程本身,本质上也属于线程间数据通信的问题。
如果通过消息的方式通知,那么B在得到A的消息通知之前,B还可以做一些别的事情。注意:这个时候,B的消息队列是仍然要运转的,不然他怎么知道收到了A的消息?通常这是一种更高级的同步方式,当收到A的通知后,B才去做原本的A做完B才能做的事。如果既没有别的事做,也没有收到A的通知,那么只好 Sleep(),因为,空转着就浪费了CPU。
这种方式实现的同步,我称之为不会陷入睡眠的同步(通过消息的同步),或高级的同步。
如果通过内核对象的方式通知,那么B在得到A的消息通知之前,B可以直接陷入被OS睡眠。当A完成事之后,设置内核变量状态为已通知,则OS会自动唤醒线程B。
通过这种方式实现的同步,我称之为陷入睡眠的同步,或低级的同步
(注意,在等待内核变量时,可以设置超时,如果一段时间内还没有收到A的通知,B也可以做别的事,等过一段时间再来检查该内核变量,查看是否A做完该做的事。通过这种方式,也可以实现高级的同步,只是复杂了点)
3)线程的开销需求限制
开启一个线程,结束一个线程,多个线程间的切换,都是消耗OS较多的CPU资源的,所以,如何降低大量的开辟/关闭线程的需求?
线程池的引入:线程池的核心是 Task + Thread pool, 通过把任务抽象为Task,原本一个Task需要开一个线程来着,现在在一个线程里面死循环,不断的执行线程的Task队列里的Task。当有执行 Task的需求时,把Task构造好塞入某个线程的Task队列里即可。线程池仅仅是为了减低频繁的创建、消耗线程的开销,充分利用以后的线程引入的技术,所以,池中的线程会不可避免的会遇到所有多线程都必须面临的安全数据访问、线程同步的问题。切记!
4)关于线程间通信,涉及到同一进程内的所有线程通信和不同进程的线程间通信; 因为同一进程中的所有线程均可以访问所有的全局变量,因而全局变量成为同一进程间多线程通信的最简单方式。但如何访问全局变量是多线程程序必须考虑的一个问题.而对于不同进程间线程通信问题实质上就是进程间通信问题.因此本文将线程间通信特指同一进程中的所有线程通信问题,实质就是解决线程间的竞争问题.
多线程程序的核心问题:竞争 对(共享)资源的竞争关系.解决这种竞争关系的方法就是线程间通信问题,包括互斥和同步.
线程程同步:是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
线程互斥:是指对于共享的操作系统资源(指的是广义的"资源",譬如全局变量就是一种共享资源),在各线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥是一种特殊的线程同步。
实际上,互斥和同步对应着线程间通信发生的两种情况:
(1)当有多个线程访问共享资源而不使资源被破坏时;
(2)当一个线程需要将某个任务已经完成的情况通知另外一个或多个线程时。
在WIN32中,同步机制主要有以下几种:
(1)事件(Event);(内)核心对象,多用于overlapped. 适用于不同进程的线程
(2)信号量(semaphore);核心对象,无拥有者. 适用于不同进程的线程
(3)互斥量(mutex);:内核对象(内)核心对象,排他性对象,甚至适用于不同进程的线程.
(4)临界区(Critical section)。:局部对象,不是(内)核心对象,仅适用于同一个进程的不同线程.
进程通常被定义为一个正在运行的程序的实例,它由两个部分组成:
一个是操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方。另一个是地址空间,它包含所有的可执行模块或DLL模块的代码和数据。它还包含动态分配的空间。如线程堆栈和堆分配空间。每个进程被赋予它自己的虚拟地址空间,当进程中的一个线程正在运行时,该线程可以访问只属于它的进程的内存。属于其它进程的内存则是隐藏的,并不能被正在运行的线程访问。为了能在两个进程之间进行通讯,由以下几种方法可供参考:
0、剪贴板Clipboard: 在16位时代常使用的方式,CWnd中提供支持。剪贴板(Clipped Board)实质是Win32 API中一组用来传输数据的函数和消息,为Windows应用程序之间进行数据共享提供了一个中介,Windows已建立的剪切(复制)-粘贴的机制为不同应用程序之间共享不同格式数据提供了一条捷径。当用户在应用程序中执行剪切或复制操作时,应用程序把选取的数据用一种或多种格式放在剪贴板上。然后任何其它应用程序都可以从剪贴板上拾取数据,从给定格式中选择适合自己的格式。剪贴板是一个非常松散的交换媒介,可以支持任何数据格式,每一格式由一无符号整数标识,对标准(预定义)剪贴板格式,该值是Win32 API定义的常量;对非标准格式可以使用Register Clipboard Format函数注册为新的剪贴板格式。利用剪贴板进行交换的数据只需在数据格式上一致或都可以转化为某种格式就行。但剪贴板只能在基于Windows的程序中使用,不能在网络上使用。
1、窗口消息:标准的Windows消息以及专用的WM_COPYDATA消息 SENDMESSAGE()接收端必须有一个窗口。:WM_COPYDATA是一种非常强大却鲜为人知的消息。当一个应用向另一个应用传送数据时,发送方只需使用调用SendMessage函数,参数是目的窗口的句柄、传递数据的起始地址、WM_COPYDATA消息。接收方只需像处理其它消息那样处理WM_COPY DATA消息,这样收发双方就实现了数据共享。WM_COPYDATA是一种非常简单的方法,它在底层实际上是通过文件映射来实现的。它的缺点是灵活性不高,并且它只能用于Windows平台的单机环境下。
2、使用共享内存方式(Shared Memory):Win32 API*享内存(Shared Memory)实际就是文件映射的一种特殊情况。进程在创建文件映射对象时用0xFFFFFFFF来代替文件句柄(HANDLE),就表示了对应的文件映射对象是从操作系统页面文件访问内存,其它进程打开该文件映射对象就可以访问该内存块。由于共享内存是用文件映射实现的,所以它也有较好的安全性,也只能运行于同一计算机上的进程之间。
a.设定一块共享内存区域
HANDLE CreateFileMapping(HANDLE,LPSECURITY_ATTRIBUTES, DWORD, DWORD, DWORD,  LPCSTR)
产生一个file-mapping核心对象
LPVOID MapViewOfFile(
HANDLE hFileMappingObject,
DWORD  dwDesiredAcess,
DWORD  dwFileOffsetHigh,
DWORD  dwFileOffsetLow,
DWORD  dwNumberOfBytesToMap
);
得到共享内存的指针
b.找出共享内存
决定这块内存要以点对点(peer to peer)的形式呈现
每个进程都必须有相同的能力,产生共享内存并将它初始化。每个进程
都应该调用CreateFileMapping(),然后调用GetLastError().如果传回的
错误代码是ERROR_ALREADY_EXISTS,那么进程就可以假设这一共享内存区域已经被别的进程打开并初始化了,否则该进程就可以合理的认为自己 排在第一位,并接下来将共享内存初始化。
还是要使用client/server架构中
只有server进程才应该产生并初始化共享内存。所有的进程都应该使用
HANDLE OpenFileMapping(DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName);
再调用MapViewOfFile(),取得共享内存的指针
c.同步处理(Mutex)
d.清理(Cleaning up) BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);
CloseHandle()
3、动态数据交换(DDE)通过维护全局分配内存使的应用程序间传递成为可能。其方式是再一块全局内存中手工放置大量的数据,然后使用窗口消息传递内存指针.这是16位WIN时代使用的方式,因为在WIN32下已经没有全局和局部内存了,现在的内存只有一种就是虚存。动态数据交换(DDE)是使用共享内存在应用程序之间进行数据交换的一种进程间通信形式。应用程序可以使用DDE进行一次性数据传输,也可以当出现新数据时,通过发送更新值在应用程序间动态交换数据。DDE和剪贴板一样既支持标准数据格式(如文本、位图等),又可以支持自己定义的数据格式。但它们的数据传输机制却不同,一个明显区别是剪贴板操作几乎总是用作对用户指定操作的一次性应答-如从菜单中选择Paste命令。尽管 DDE也可以由用户启动,但它继续发挥作用一般不必用户进一步干预。DDE有三种数据交换方式:
(1) 冷链:数据交换是一次性数据传输,与剪贴板相同。
(2) 温链:当数据交换时服务器通知客户,然后客户必须请求新的数据。
(3) 热链:当数据交换时服务器自动给客户发送数据。
DDE交换可以发生在单机或网络中不同计算机的应用程序之间。开发者还可以定义定制的DDE数据格式进行应用程序之间特别目的IPC,它们有更紧密耦合的通信要求。大多数基于Windows的应用程序都支持DDE。
4、消息管道(Message Pipe):用于设置应用程序间的一条永久通讯通道,通过该通道可以象自己的应用程序访问一个平面文件一样读写数据。
匿名管道(Anonymous Pipes):单向流动,并且只能够在同一电脑上的各个进程之间流动。管道(Pipe)是一种具有两个端点的通信通道:有一端句柄的进程可以和有另一端句柄的进程通信。管道可以是单向-一端是只读的,另一端点是只写的;也可以是双向的一管道的两端点既可读也可写。匿名管道(Anonymous Pipe)是在父进程和子进程之间,或同一父进程的两个子进程之间传输数据的无名字的单向管道。通常由父进程创建管道,然后由要通信的子进程继承通道的读端点句柄或写端点句柄,然后实现通信。父进程还可以建立两个或更多个继承匿名管道读和写句柄的子进程。这些子进程可以使用管道直接通信,不需要通过父进程。匿名管道是单机上实现子进程标准I/O重定向的有效方法,它不能在网上使用,也不能用于两个不相关的进程之间。
命名管道(Named Pipes):双向,跨网络,任何进程都可以轻易的抓住,放进管道的数据有固定的格式,而使用ReadFile()只能读取该大小的倍数。可以被使用于I/O Completion Ports。命名管道(Named Pipe)是服务器进程和一个或多个客户进程之间通信的单向或双向管道。不同于匿名管道的是命名管道可以在不相关的进程之间和不同计算机之间使用,服务器建立命名管道时给它指定一个名字,任何进程都可以通过该名字打开管道的另一端,根据给定的权限和服务器进程通信。命名管道提供了相对简单的编程接口,使通过网络传输数据并不比同一计算机上两进程之间通信更困难,不过如果要同时和多个进程通信它就力不从心了。
5、邮件槽(Mailslots):广播式通信,在32系统中提供的新方法,可以在不同主机间交换数据,在WIN9X下只支持邮件槽客户。邮件槽(Mailslots)提供进程间单向通信能力,任何进程都能建立邮件槽成为邮件槽服务器。其它进程,称为邮件槽客户,可以通过邮件槽的名字给邮件槽服务器进程发送消息。进来的消息一直放在邮件槽中,直到服务器进程读取它为止。一个进程既可以是邮件槽服务器也可以是邮件槽客户,因此可建立多个邮件槽实现进程间的双向通信。通过邮件槽可以给本地计算机上的邮件槽、其它计算机上的邮件槽或指定网络区域中所有计算机上有同样名字的邮件槽发送消息。广播通信的消息长度不能超过400字节,非广播消息的长度则受邮件槽服务器指定的最大消息长度的限制。邮件槽与命名管道相似,不过它传输数据是通过不可靠的数据报(如TCP/IP 协议中的UDP包)完成的,一旦网络发生错误则无法保证消息正确地接收,而命名管道传输数据则是建立在可靠连接基础上的。不过邮件槽有简化的编程接口和给指定网络区域内的所有计算机广播消息的能力,所以邮件槽不失为应用程序发送和接收消息的另一种选择。
6、Windows套接字(Windows Socket):它具备消息管道所有的功能,但遵守一套通信标准使的不同操作系统之上的应用程序之间可以互相通信。Windows Sockets规范是以U.C.Berkeley大学BSD UNIX中流行的Socket接口为范例定义的一套Windows下的网络编程接口。除了Berkeley Socket原有的库函数以外,还扩展了一组针对Windows的函数,使程序员可以充分利用Windows的消息机制进行编程。现在通过Sockets实现进程通信的网络应用越来越多,这主要的原因是 Sockets的跨平台性要比其它IPC机制好得多,另外WinSock 2.0不仅支持TCP/IP协议,而且还支持其它协议(如IPX)。Sockets的唯一缺点是它支持的是底层通信操作,这使得在单机的进程间进行简单数据传递不太方便,这时使用WM_COPYDATA消息将更合适些。
7、Internet通信:它让应用程序从Internet地址上载或下载文件
8、RPC:远程过程调用,很少使用,因其与UNIX的RPC不兼容。Win32 API提供的远程过程调用(RPC)使应用程序可以使用远程调用函数,这使在网络上用RPC进行进程通信就像函数调用那样简单。RPC既可以在单机不同进程间使用也可以在网络中使用。由于Win32 API提供的RPC服从OSF-DCE(Open Software Foundation Distributed Computing Environment)标准。所以通过Win32 API编写的RPC应用程序能与其它操作系统上支持DEC的RPC应用程序通信。使用RPC开发者可以建立高性能、紧密耦合的分布式应用程序。
9、串行/并行通信(Serial/Parallel Communication):它允许应用程序通过串行或并行端口与其他的应用程序通信
10、COM/DCOM:通过COM系统的代理存根方式进行进程间数据交换,但只能够表现在对接口函数的调用时传送数据,通过DCOM可以在不同主机间传送数据。
11、文件映射:文件映射(Memory-Mapped Files)能使进程把文件内容当作进程地址区间一块内存那样来对待。因此,进程不必使用文件I/O操作,只需简单的指针操作就可读取和修改文件的内容。Win32 API允许多个进程访问同一文件映射对象,各个进程在它自己的地址空间里接收内存的指针。通过使用这些指针,不同进程就可以读或修改文件的内容,实现了对文件中数据的共享。应用程序有三种方法来使多个进程共享一个文件映射对象。
(1)继承:第一个进程建立文件映射对象,它的子进程继承该对象的句柄。
(2)命名文件映射:第一个进程在建立文件映射对象时可以给该对象指定一个名字(可与文件名不同)。第二个进程可通过这个名字打开此文件映射对象。另外,第一个进程也可以通过一些其它IPC机制(有名管道、邮件槽等)把名字传给第二个进程。
(3)句柄复制:第一个进程建立文件映射对象,然后通过其它IPC机制(有名管道、邮件槽等)把对象句柄传递给第二个进程。第二个进程复制该句柄就取得对该文件映射对象的访问权限。文件映射是在多个进程间共享数据的非常有效方法,有较好的安全性。但文件映射只能用于本地机器的进程之间,不能用于网络中,而开发者还必须控制进程间的同步。
12、对象连接与嵌入:应用程序利用对象连接与嵌入(OLE)技术管理复合文档(由多种数据格式组成的文档),OLE提供使某应用程序更容易调用其它应用程序进行数据编辑的服务。例如,OLE支持的字处理器可以嵌套电子表格,当用户要编辑电子表格时 OLE库可自动启动电子表格编辑器。当用户退出电子表格编辑器时,该表格已在原始字处理器文档中得到更新。在这里电子表格编辑器变成了字处理器的扩展,而如果使用DDE,用户要显式地启动电子表格编辑器。同DDE技术相同,大多数基于Windows的应用程序都支持OLE技术。
13、动态连接库:Win32动态连接库(DLL)中的全局数据可以被调用DLL的所有进程共享,这就又给进程间通信开辟了一条新的途径,当然访问时要注意同步问题。虽然可以通过DLL进行进程间数据共享,但从数据安全的角度考虑,我们并不提倡这种方法,使用带有访问权限控制的共享内存的方法更好一些。
14、NetBios函数:Win32 API提供NetBios函数用于处理低级网络控制,这主要是为IBM NetBios系统编写与Windows的接口。除非那些有特殊低级网络功能要求的应用程序,其它应用程序最好不要使用NetBios函数来进行进程间通信。