370 likes | 447 Views
线程间通信. 第四章 线程间通信. 4.1 进程/线程通信的方式 4.2 使用事件进行通信 4.3 Windows 消息 4.4 Shared Memory. 4.1 进程 / 线程通信的方式. 某些应用程序中,进程 / 进程和线程 / 线程之间不可避免地进行通信,进行消息传递,数据共享等 同一进程的线程之间通信方式包括 Windows 中常用 Event, Message 等。
E N D
第四章 线程间通信 • 4.1 进程/线程通信的方式 • 4.2 使用事件进行通信 • 4.3 Windows消息 • 4.4 Shared Memory
4.1 进程/线程通信的方式 • 某些应用程序中,进程/进程和线程/线程之间不可避免地进行通信,进行消息传递,数据共享等 • 同一进程的线程之间通信方式包括Windows中常用Event, Message等。 • 不同进程之间的通信可以利用Event, FileMapping(内存共享), WM_COPYDATA消息以及ClipBoard(剪贴板),DDE(动态数据交换), MessagePipe, MailSlot(邮件槽)等
4.2 使用事件进行通信 • 事件除了用来保证两个线程之间同步之外,借由通知功能,也可以作为线程之间的简单通信的手段 • 线程使用WaitForSingleObject等待一件事情的发生,该事件可以由另外一个线程通过SetEvent进行触发。 • 使用WaitForMultipleObjects等待多个由其他线程触发的事件(某些情况下,在线程的函数中也给自己发送消息) • 事件只能通知一件事情的发生,不能传递其他附属数据。
通知事件 • 调用SetEvent, 可以将事件的内核对象的状态变成已通知 • 调用ResetEvent, 可以将事件的内核对象的状态变成未通知 • 调用PulseEvent, 将事件对象置为有信号状态,然后立即置为无信号状态,在实际开发中这个函数很少使用 • 不同进程之间也可以通过Event通知事件,这可以通过命名对象来实现 • CreateEvent时指定了名称,不同进程之间通过创建同名的Event就可以获得指向同一个内核对象的句柄,这样不同进程间就可以互相通知和等待Event
使用事件通知并传送数据 • Event可以通知一件事情, 但是不能同时传送数据, 可以采用几种方式异步地获得数据. • 先设置给接收方数据, 后通知Event. • 由接收Event方提供设置(保存)数据的函数, 在接收方接受到通知后, 从自己保存的数据中取得数据. • 先通知Event后从发送方取得数据 • 使用Event做成消息队列 • 在接收方做成一个存储消息和数据的队列, 每次登录一个消息(数据)后,通知接收方, 接受方从该队列中顺序取得数据.
4.3 Windows消息 • Windows是一个消息(Message)驱动系统 • Windows的消息提供了应用程序之间、应用程序与Windows系统之间进行通信的手段。 • 应用程序想要实现的功能由消息来触发,并且靠对消息的响应和处理来完成。 • Windows系统中有两种消息队列:系统消息队列和应用程序消息队列。 • 计算机的所有输入设备由Windows监控。当一个事件发生时,Windows先将输入的消息放入系统消息队列中,再将消息拷贝到相应的应用程序消息队列中。应用程序的消息处理程序将反复检测消息队列,并把检测到的每个消息发送到相应的窗口函数中。这便是一个事件从发生至到达窗口函数必须经历的过程。 • 注意: • 消息并非是抢占性的,无论事件的缓急,总是按照到达的先后派对,依次处理(一些系统消息除外),这样可能使一些实时外部事件得不到及时处理
用户界面线程和工作线程 • Windows中存在两种线程:用户界面线程、工作线程。 • 工作线程 • 用来执行某些辅助处理的线程,它不需要进行任何系统事件或者窗口事件的处理。 • 用户界面线程 • 指拥有自己的消息循环并能对用户界面对象进行创建、交互和撤销的线程 • Windows的消息机制与用户界面线程息息相关。 • 用户界面线程一般是继承CWinThread类实现。 • 一个线程被创建后,系统假定线程不会被用于任何与用户相关的操作 • 一旦一个线程调用一个与图形用户界面有关的函数 (如创建窗口或者检查消息队列的函数),Windows会分配给这个线程一个THREADINFO结构。
线程的消息队列 • 每个线程利用THREADINFO来认为自己是在一个独占的环境中运行。在这个结构里保存了一系列的消息队列(登记消息队列、发送消息队列、应答消息队列)、唤醒标志、以及用来描述线程局部输入状态的若干变量。 • THREADINFO结构是窗口消息系统的基础 • 当线程有了与之相联系的THREADINFO的结构时,线程就有了自己的消息队列集合
Windows消息结构介绍 • MSG • The MSG structure contains message information from a thread's message queue. • typedef struct tagMSG { HWND hwnd; //接收消息的窗口句柄 UINT message; WPARAM wParam; //一个32位的参数,其含义和数值根据消息的不同而不同。 LPARAM lParam; //一个32位的消息参数,其值与消息有关。 DWORD time; //消息放入消息队列中的时间 POINT pt; //消息放入队列时的鼠标座标 } MSG, *PMSG; • 用户使用自定义的消息的时候, 可充分利用lParam,和wParam传递数据 message消息类别。 这是一个数值,用以标识消息。对於每个消息息,均有一个对应的识别字,这些识别字定义在Windows头文件中(其中大多数在WINUSER.H中),以字首WM开头。 例如,使用者将鼠标窗口内,并按下左按钮,Windows就在消息队列中放入一个消息,该消息的message成员的值是WM_LBUTTONDOWN。这是一个常数,其值为0x0201。
将消息发送到线程的消息队列中 • 通过PostMessage把消息发送给窗口所在的线程 • The PostMessage function places (posts) a message in the message queue associated with the thread that created the specified window and returns without waiting for the thread to process the message. • BOOL PostMessage( HWNDhWnd, // handle to destination window UINTMsg, // message WPARAMwParam, // first message parameter LPARAMlParam// second message parameter ); • 当一个线程调用了这个函数的时候,系统要确定是哪一个线程建立了用hWnd参数标志的窗口 • 系统分配内存,存储消息参数,将这块内存增加到相应线程的消息队列中 • 系统设置QS_POSTMESSAGE唤醒位标志
将消息发送到线程的消息队列中 • 通过PostThreadMessage将消息放置在线程的消息队列 • The PostThreadMessage function posts a message to the message queue of the specified thread. It returns without waiting for the thread to process the message. • BOOL PostThreadMessage( DWORDidThread, // thread identifier UINTMsg, // message WPARAMwParam, // first message parameter LPARAMlParam// second message parameter ); • 可以通过GetWindowThreadProcessID来确认是哪个线程创建了窗口 • ThreadID是在线程创建的时候获得的,在全系统范围内是唯一的 • 要对线程编写消息循环(GetMessage,PeekMessage,DispatchMessage) • PostQuitMessage(intnExitCode)向线程发送退出消息,等于PostThreadMessage(ThreadID,WM_QUIT,nExitCode,0);
PostMessage和SendMessage • PostMessage • 当我们使用PostMessage时,是将一个Message复制到“登记消息队列”中,然后并立即返回。之后由GetMessage取回并响应之。 • SendMessage • 当我们使用SendMessage时,我们都知道,这个消息发送函数必须等到消息响应执行完毕才能返回。而它如何做到这一点的呢? • 当调用这个SendMessage的线程向这个线程自己创建的窗口发送消息的时候,它只是调用指定窗口的窗口过程,将其作为一个子例程,当窗口过程完成对消息的处理时,返回给SendMessage一个值。 • 当一个线程向其他线程创建的窗口发送消息的时候,SendMessage首先将消息加入接收线程的“发送消息队列”,并为这个线程设置标志。同时发送线程将自己挂起,并在自己的“应答消息队列”中加入一个等待消息。当消息被接收线程处理完毕后,窗口的返回值被登记到发送线程的应答消息队列中。这时发送线程被唤醒,取出包含在应答消息队列中的返回值。
取得窗口消息 • 使用GetMessage或者PeekMessage取得窗口的消息 • 一个典型而普通的消息循环处理 while(GetMessage(&msg,NULL,0,0)) { TranslateMessage(&msg); DispatchMessage(&msg); } • 这段处理必然是在用户界面线程的主循环中完成的 • GetMessage的特点是当线程消息队列中有消息的时候则能立刻返回,否则进行等待 • PeedMessage也能从线程消息队列中取得消息,它的特点是不管消息队列是否存在消息,都不等待。
CWinApp处理消息 • MFC应用程序都有一个CWinApp对象,继承于CWinThread,在CWinApp的Run函数中进行了和GetMessage和DispatchMessage的处理 • 可以在MFC源代码THRDCORE.cpp找到CWinThread::Run()的代码,以及Run函数的运行时机。
同时等待消息和内核对象MsgWaitForMultipleObjects • dwWakeMask指出了想要观察的用户输入Message • QS_ALLINPUT • QS_HOTKEY • QS_INPUT • QS_KEY • QS_MOUSE • QS_MOUSEBUTTON • QS_MOUSEMOVE • QS_PAINT • QS_POSTMESSAGE • QS_SENDMESSAGE • QS_TIMER • 某些情况下,你的线程不得不等待一个或多个事件,并且同时等待某些Message。 • MsgWaitForMultipleObjects函数非常类似WaitForMultipleObjects,但它会在“对象被激发”或者”消息到达队列”时被唤醒并且返回 • DWORD MsgWaitForMultipleObjects( DWORD nCount, //等待内核事件的个数 LPHANDLE pHandles, // 内核事件对象数组指针 BOOLfWaitAll, // 是否等待全部事件才返回 DWORD dwMilliseconds, // 超时值 DWORD dwWakeMask // 唤醒Mask ); • 为了表达”消息到达队列“,返回值将是WAIT_OBJECT_0+nCount
MsgWaitForMultipleObjects代码实例 DWORD WINAPI ThreadFunc(LPVOIDlpParam) { boolbRun= true; while(bRun) { dwRet= MsgWaitForMultipleObjects(nEventCount, HandleArray,// event handle array FALSE,// 只要有一个事件触发,就可以返回 dwMilliseconds,// 超时值 QS_ALLINPUT );// 等全部消息 if (WAIT_FAILED == dwRet) {// 错误处理} if (WAIT_TIMEOUT==dwRet){// 超时处理} if (WAIT_OBJECT_0+nEventCount==dwRet) { while(PeekMessage(&Msg,NULL,0,0,PM_REMOVE) {// 做消息处理} } else{ switch(dwRet-WAIT_OBJECT_0) { case EVENT_EVT1: // 处理事件1 break; //….. 其它事件处理 } } } }
Event和Message • Event会覆盖(丢失), Message不会 • Event只能通知一件事情的发生,不能传送数据,Message可以传递简单的数据(通过LParam和WParam) • 在同一个进程内, Message可以通过LParam或者WParam指向一个堆内存而达到数据通信的功能. • 发Event需要指定目标Handle, 发Message需要指定目标Window(窗口句柄) • Event可以跨越进程, 通过名字达到共享. • Message也可以跨越进程, 通过注册消息(RegisterWindowMessage), 在进程间共享消息.
进程间通信-自定义消息 • 在某些情况下我们可能需要向其他的应用程序发送消息,这时候我们可以采用SendMessage()函数向目标应用程序的某个窗口的句柄发送消息。 • 其中的技巧在于获取该窗口的句柄。 • 同时使用RegisterWindowMessage()函数创建一个唯一的消息,并且两个应用程序相互都了解这条消息的含义。 • 同时还会用到BrodcastSystemMessage()函数,它可以向系统中的每个应用程序的主窗口发送消息。这样便可以避免出现获取另一个应用程序窗口句柄的问题。 • BroadcastSystemMessage()函数提供了附加的标志BSF_LPARAMPOINTER,可以将写入参数lParam的指针转化为可以被目标程序用来访问程序空间的指针,但是这个标志可能尚未进行文档标准化。
首先注册自己的窗口消息。 • 不过我们这次不用WM_USER+1的技术,注册窗口消息的好处是不必费心考虑WM_USER加上某个数之后,所表示的消息标识符是否超出工程的允许范围。本例在两个工程中都使用文本字符串来注册消息。由于这个文本字符串在整个系统中应当是唯一的,因此将使用一种称为GUID的COM技术来命名消息。GUID名字生成器程序可以在MFC的\BIN目录下找到,其可执行文件名为GUIDGEN.EXE。该程序将生成在应用程序已知范围内认为是唯一的文本字符串,这对应用程序来说当然是最好不过的。
1) 注册一个唯一的窗口消息 • 使用GUIDGEN.EXE生成一个GUID。 • 在应用程序中把GUID定义为窗口消息文本字符串 • #define HELLO_MSG “{6047CCB1-E4E7-11d1-9B7E-00AA003D8695}” • 使用::RegisterWindowsMessage()注册该窗口消息文本字符串 • idHelloMsg= ::RegisterWindowMessage( HELLO_MSG ); • 保存消息标识符idHelloMsg,便于以后使用。
2) 向其他应用程序发送消息 • 使用::RegisterWindowsMessage()返回的消息标识符发送消息 • ::SendMessage(hWnd, idHelloMsg,wParam,lParam); • 以上代码假定事先可以通过某种方式获取目标应用程序的某个窗口的句柄。一个指向CWnd类的指针不能在程序范围之外而发挥作用。但是可以在CWnd 类中封装已获取的窗口句柄,并如下所示来发送消息: CWnd wnd; wnd.Attach( hWnd ); wnd.SendMessage( idHelloMsg,wParam,lParam );
3) 接收已注册的窗口消息 • 为接收已注册的窗口消息,需要在接收窗口类,一般为CMainFrame中手工添加ON_REGISTERED_MESSAGE消息宏到消息映射中: BEGIN_MESSAGE_MAP( CMainFrame, CMDIFrameWnd) // {{AFX_MSG_MAP( CMainFrame) // }}AFX_MSG_MAP ON_REGISTERED_MESSAGE( idHelloMsg,OnHelloMsg) END_MESSAGE_MAP() • 有关已注册消息的消息处理函数的代码如下: LRESULT CMainFrame::OnHelloMsg( WPARAM wParam,LPARAMlParam) { // process message return 0; }
该实例到目前为止,一直假定事先可以通过某种方式取得目标应用程序的某个窗口的句柄。但这是一个困难的任务。简单的方法是向每个应用程序广播一条消息,并且希望目标程序正在监听。由于在系统中注册了一条唯一的消息,因此只有目标程序会响应这条消息。应用程序广播的消息可能是它自己的窗口句柄,于是接收程序可以使用::SendMessage()来发送应答,也可能是用窗口句柄来结束循环。该实例到目前为止,一直假定事先可以通过某种方式取得目标应用程序的某个窗口的句柄。但这是一个困难的任务。简单的方法是向每个应用程序广播一条消息,并且希望目标程序正在监听。由于在系统中注册了一条唯一的消息,因此只有目标程序会响应这条消息。应用程序广播的消息可能是它自己的窗口句柄,于是接收程序可以使用::SendMessage()来发送应答,也可能是用窗口句柄来结束循环。
4) 广播窗口消息 WPARAM wParam= xxx; LPARAM lParam= xxx; DWORD dwRecipients= BSM_APPLICATIONS; ::BroadcastSystemMessage( BSF_IGNORECURRENTTASK,&dwRecipients,idHelloMsg, wParam,lParam);
进程间通信-WM_COPYDATA • 通过SendMessage可以把一个消息发送到另外一个进程,但是在另外一个进程中试图访问LPARAM所指向的数据,也许会发生错误 • 进程地址空间是受到保护和相互隔离的,试图访问另外一个进程的地址空间是不正确的。 • Windows定义了WM_COPYDATA消息专门用来在线程之间传递数据,不管两个线程是否属于一个进程 • 处理WM_COPYDATA的线程必须有消息队列和窗口(带有消息队列的工作线程或者UI主线程)
必须使用SendMessage而不是PostMessage发送WM_COPYDATA消息必须使用SendMessage而不是PostMessage发送WM_COPYDATA消息 WM_COPYDATA所传送的数据可以在Heap上也可以在Stack上申请,因为SendMessage保证接收方在返回前完成对数据的操作。 接受方必须在WM_COPYDATA消息处理函数中完成对lpData的处理(读取和保存其中的内容),而不是保存lpData指针,对接收方来说,在这次消息处理后,lpData所指向的内存将不再是可用的。 • SendMessage( hWndReceiver, WM_COPYDATA, (WPARAM)hWndSender, (LPARAM)&cds //指向一个特定的Windows数据结构COPYDATASTRUCT ); • Typedef struct tagCOPYDATASTRUCT{ DWORD dwData; //通常用户自定义为行动代码 DWORD cbData; // lpData所指的数据块大小 PVOID lpData; // 一块数据,可以被传送到接受端 }COPYDATASTRUCT,*PCOPYDATASTRUCT;
4.4 Shared Memory • Win32进程之间有严密的保护,一个进程要看到另外一个进程的地址空间中的任何一部分,都是不可能的。 • 程序只能见到逻辑的地址,所有进程的逻辑地址空间都是相同的(4GB的理论地址空间) • WM_COPYDATA技术简单但是效率不高 • 使用Win32进程通信技术的最低层:共享内存(SharedMemory)来进行高效的进程间数据共享
使用共享内存的方法 • 1.设定一块儿共享内存 • (1) 产生一个FileMapping内核对象,指定共享区域大小(CreateFileMapping) • (2)将共享区域映射到你的进程的地址空间(MapViewOfFile) • 2.使用共享内存 • 找到共享内存块 • 有些情况下,由Server进程创建一个共享内存,其他进程只需要找到这块共享内存并使用就可以了。 • 1. 找到一个FileMapping内核对象,使用OpenFileMapping • 系统找到同一个FileMapping内核对象是根据对象的命名 • 2. 将共享区域映射到你的进程的地址空间(MapViewOfFile) • 3.同步处理共享内存 • 多个进程使用共享内存的时候,必须保证同步安全地使用共享的内存,通常使用等待被命名的Mutex来保证。
创建共享内存CreateFileMapping • HANDLE CreateFileMapping( • HANDLE hFile, //物理文件句柄 • LPSECURITY_ATTRIBUTES lpAttributes, //安全设置 • DWORD flProtect, //保护设置 • DWORD dwMaximumSizeHigh, //高位文件大小 • DWORD dwMaximumSizeLow, //低位文件大小 • LPCTSTR lpName//共享内存名称 • );
1) 物理文件句柄 • 任何可以获得的物理文件句柄, 如果你需要创建一个物理文件无关的内存映射也无妨, 将它设置成为0xFFFFFFFF(INVALID_HANDLE_VALUE)就可以了. • 如果需要和物理文件关联, 要确保你的物理文件创建的时候的访问模式和“保护设置”匹配, 比如: 物理文件只读, 内存映射需要读写就会发生错误. 推荐你的物理文件使用独占方式创建. • 如果使用INVALID_HANDLE_VALUE, 也需要设置需要申请的内存空间的大小, 无论物理文件句柄参数是否有效, 这样CreateFileMapping就可以创建一个和物理文件大小无关的内存空间给你, 甚至超过实际文件大小, 如果你的物理文件有效, 而大小参数为0, 则返回给你的是一个和物理文件大小一样的内存空间地址范围.返回给你的文件映射地址空间是可以通过复制, 集成或者命名得到, 初始内容为0.
2) 保护设置 • 就是安全设置, 不过一般设置NULL就可以了, 使用默认的安全配置. 在win2k下如果需要进行限制, 这是针对那些将内存文件映射共享给整个网络上面的应用进程使用是, 可以考虑进行限制. • 3) 高位文件大小 • 目前我们的机器都是32位的, 不可能得到超过32位进程所能寻址的私有32位地址空间, 一般还是设置0. • 4) 低位文件大小 • 实际共享内存的大小, 不过为了让其他共享用户知道你申请的文件映射的相关信息, 使用的时候是在获得的地址空间头部添加一个结构化描述信息, 记录内存映射的大小, 名称等。 • 5) 共享内存名称 • 在其他进程创建同名的共享内存的时候,系统会返回存在的共享内存.
hFileMappingObject为CreateFileMapping()返回的文件映像对象句柄。hFileMappingObject为CreateFileMapping()返回的文件映像对象句柄。 dwDesiredAccess则再次指定了对文件数据的访问方式,而且同样要与CreateFileMapping()函数所设置的保护属性相匹配。 • 映射共享内存-MapViewOfFile • The MapViewOfFile function maps a view of a file into the address space of the calling process. • LPVOID MapViewOfFile( HANDLEhFileMappingObject, // handle to file-mapping object DWORDdwDesiredAccess, // access mode DWORDdwFileOffsetHigh, // high-order DWORD of offset DWORDdwFileOffsetLow, // low-order DWORD of offset SIZE_TdwNumberOfBytesToMap// number of bytes to map );
MapViewOfFile()函数允许全部或部分映射文件,在映射时,需要指定数据文件的偏移地址以及待映射的长度。其中,MapViewOfFile()函数允许全部或部分映射文件,在映射时,需要指定数据文件的偏移地址以及待映射的长度。其中, • 文件的偏移地址由DWORD型的参数dwFileOffsetHigh和dwFileOffsetLow组成的64位值来指定,而且必须是操作系统的分配粒度的整数倍 • 对于Windows操作系统,分配粒度固定为64KB。当然,也可以通过如下代码来动态获取当前操作系统的分配粒度: SYSTEM_INFO sinf; GetSystemInfo(&sinf); DWORD dwAllocationGranularity= sinf.dwAllocationGranularity; • MapViewOfFile的返回值就是被映射在进程地址空间中的共享内存块指针,用户操作这个指针可以达到共享内存了。
使用共享内存-Create/OpenFileMapping • 共享内存以点对点(peer to peer)的形式呈现 • 则每个进程都必须有相同的能力,产生共享内存并将它初始化。每个进程都应该调用CreateFileMapping(),然后调用GetLastError().如果传回的错误代码是ERROR_ALREADY_EXISTS,那么进程就可以假设这一共享内存区域已经被别的进程打开并初始化了,否则该进程就可以合理的认为自己排在第一位,并接下来将共享内存初始化。 • 共享内存以client/server架构的形式呈现 • 只有server进程才应该产生并初始化共享内存。 • 所有的进程都应该使用由OpenFileMapping得到的共享内存映射文件。 • HANDLE OpenFileMapping( • DWORD dwDesiredAccess, // access mode • BOOL bInheritHandle, // inherit flag • LPCTSTR lpName // object name,是其他进程用CreateFileMapping函数创建共享内存时指定的对象名称。 • ); • 再调用MapViewOfFile(),取得共享内存的指针