27.1 线程的消息队列
(1)Windows用户对象(User Object)
①类型:图标、光标、窗口类、菜单、加速键表等
②当一个线程创建某个对象时,则该对象归这个线程的进程所有,当进程结束时,如果用户没有明确删除这个对象,则操作系统会自动删除这个对象。
③窗口和钩子(hook)这两种用户对象,它们分别由建立窗口和安装钩子的线程所拥有(注意,不是进程)。如果一个线程建立一个窗口或安装一个钩子。然后线程结束,操作系统会自动删除窗口或卸载钩子。
④窗口及线程的拥有关系使得建立窗口的线程必须为它的窗口处理所有消息。这意味着如果线程建立了一个窗口(或调用一个与图形用户界面有关的函数),系统将对它分配一个消息队列,用来向窗口派送(dispatch)消息。
(2)THREADINFO的内部数据结构(注意它与某一线程相关联)
27.2 将消息投递到线程的消息队列
27.2.1 BOOL PostMessage(hwnd,uMsg,wParam,lParam);
(1)调用该函数时,系统先确定是哪一个线程建立了hwnd窗口,然后分配一块内存,将消息参数存储在这块内存,然后把这块内存增加到对应线程的Posted-Message队列中。
(2)添加到队列后,PostMessage还设置了Wake Flags变量的QS_POSTMESSAGE唤醒位。
(3)消息投递完会函数立即返回。
27.2.2 BOOL PostThreadMessage(dwThreadId,uMsg,wParam,lParam)
(1)dwThreadId为目标线程id,当消息被投递出去后,MSG结构体中的hwnd会被自动设为NULL。
(2)目标线程的主消息循环中,当GetMessage(或PeekMessage)获取一条消息后,先检查hwnd是否为NULL,如果是就对这个消息进行一行特殊处理而不发往某个窗口过程,所以就不调用DispatchMessage将消息分派出去。
(3)与PostMessage一样,PostThreadMessage也是立即返回。调用线程无法知道消息是否被处理。
(4)获取创建窗口的线程ID:DWORD GetWindowThreadProcessID(hwnd,pdwProcessId)的返回值即为线程ID。
27.2.3 void PostQuitMessage(int nExitCode)
(1)终止消息循环。与PostThreadMessage(GetCurrentThreadId(),WM_QUIT,nExitCode,0)类似(但不相同)。PostQuitMessage并不真正投递消息到队列中去(即并不发送WM_QUIT消息,原因见27.4.3《从线程的队列中提取消息的算法》),而只是设置QS_QUIT标志位和nExitCode退出码。
(2)因为该函数只是设置QS_QUIT标志位和nExitCode,调用总是会成功,所以返回值为void。
27.3 向窗口发送消息
27.3.1 LRESULT SendMessage(hwnd,uMsg,wParam,lParam);
(1)只有消息被处理后,SendMessage才会返回。
(2)如果调用SendMessage的线程向该线程所建立的一个窗口发送消息,则SendMessage工作过程很简单,只是直接调用了指定的窗口过程,将其作为一个子函数调用而己。当窗口过程处理完消息后,其返回值直接返回给SendMessage,SendMessage再将其返回给调用线程。
(3)如果调用SendMessage向其他线程(包含其他进程中的线程)时,其过程较复杂,因为Windows要求窗口过程须由创建该窗口的线程来调用(而不能是其他线程),其工作过程如下:
①当调用SendMessage时,函数会将这条消息添加到接收线程的“Send-Message”队列中,这将导致开启接收线程的QS_SENDMESSAGE标志位。
②如果此时接收线程正在执行“其他代码”,那么这条消息不会被立即处理。当接收线程开始等待消息时,系统会先检查QS_SENDMESSAGE标志位是否被设置。如果被设置,系统会从“Send-Message”队列中找到第一条消息(队列中可能有很多条消息,因为可能有多个线程在同一时间调用SendMessage向这个队列添加消息)。(注意,前面说的“其他代码”不包含调用GetMessage、PeekMeesage或WaitMessage等函数,因为这些函数会让接收线程取出Send-Message队列中的消息并开始处理)
③当接收线程从“Send-Message”队列中取出消息并调用相应的窗口过程后,处理一条消息后,GetMessage并不返回,而是再转去处理该队列的消息,直到“send-queue”队列中没有其他消息了,系统就会关闭QS_SENDMESSAGE标志位。在接收线程处理消息期间,调用SendMessage的线程(也被称为“调用线程”或“发送线程”)被设置为“空闲”以便等待接收线程发送一条应答消息到自己的“Reply-Message”队列。当接收线程处理完消息后,会把窗口过程的返回值投递到发送线程的“Reply-Message”队列(注意,这个返回值会作为SendMessage的返回值,返回给发送线程),并唤醒发送线程。然后发送线程继续正常执行。
④当发送线程等待SendMessage返回期间,他通常被设成“空闲”,但他也被允许执行一项任务:假如另一个线程(B)发送一个消息给当前这个线程调用SendMessage的线程(A),A线程必须立即去处理这个消息,而不必调用GetMessage、PeekMessage或WaitMessage等函数。为什么要立即处理?比如A线程SendMessage给B,这时A挂起等待B线程处理完毕,在这期间B也SendMessage给了A,如果A不被立即唤醒去处理B发送过来的消息,两个进程就会进入互相等待的死锁状态)
(4)Windows采用这种方法来处理线程间的SendMessage会导致发送线程挂起,如果这时接收线程出现一个Bug而导致死循环,这时发送线程也就无法被唤醒。这就意味着一个进程的Bug,可能影响到另一个进程。(可以调用以下四个函数来解决这个问题)
27.3.2 SendMessageTimeOut函数
参数 |
含义 |
HWND hwnd |
前四个参数与SendMessage含义一样 |
UINT uMsg |
|
WPARAM wParam |
|
LPARAM lParam |
|
UINT fuFlags |
标志:可以是以下几个的组合 ①SMTO_NORMAL(0):不使用任何其他标志,或取消以下几个标志或其组合。 ②SMTO_ABORTIFHUNG:如果接收线程被挂起时,立即返回 ③SMTO_BLOCK:一个线程在等待SendMessage*返回时可以被中断,以便处理另一个发送来的消息。使用SMTO_BLOCK标志阻止系统允许这种中断,但可能会造成死锁(原因见SendMessage部分的分析)。 ④SMTO_NOTIMEOUTIFNOTHUNG:如果接收线程没有被挂起时,不考虑时间限定值。 |
UINT uTimeOut |
等待其他线程应答我们发送的消息的时间最大值,单位ms |
PDWORD pdwResult |
处理消息的结果。 |
返回值 |
成功或失败,可用GetLastError获取更多的信息 |
备注:如果调用SendMessageTimeOut向调用线程所建立的窗口发送一个消息,系统只是调用这个窗口的过程,并将返回值赋给pdwResult。因为所有的处理都必须发生在一个线程里,调用该函数之后出现的代码必须等消息被处理完之后才会被执行。 |
27.3.3 SendMessageCallBack(hwnd,uMsg,wParam,lParam,pfnResultCallBack,dwData);
(1)调用SendMessageCallBack时,一条消息会被添加到接收线程的“Send-Message”队列,然后调用线程立即返回。当接收线程处理完消息后,会向发送线程的“Reply-Message”队列发送一条应答消息。然后系统合适的时候通知发送线程去执行pfnResultCallBack指定的回调函数。
(2)回调函数的原型:VOID CALLBACK ResultCallBack(hwnd,uMsg,dwData,dwResult); 当调用SendMessageCallBack时,会其将自己的dwData参数直接传给回调函数,dwResult处理消息的窗口过程返回的结果。
(3)在线程间发送消息时,SendMessageCallBack会立即返回,但回调函数并不立即执行。即使接收线程处理完消息后,回调函数也不一定立即执行,因为接收线程只是发送个应答给发送线程,告知他处理完了消息。至于发送线程什么时候调用这个回调函数,由发送线程说了算,一般是当发送线程调用GetMessage、PeekMessage、WaitMessage时,消息从“Reply-Message”队列被取出时,回调函数才会被执行。
(4)SendMessageCallBack还有另一种用法。Windows提供了一种广播消息的方法,可以用系统中所有的重叠(Overlapped)窗口广播消息。虽然可以通过SendMessage,并向hwnd传递HWND_BROADCAST(-1),但这种方法的广播,其返回值只是一个LRESULT值,并不能查看每个重叠窗口的窗口过程处理后的返回结果。但如果调用SendMessageCallBack,对每个窗口过程处理会后,回调函数都会被调用一次,因此就可以通过dwResult查看返回结果。
(5)如果SendMessageCallBack向一个由调用线程所建立的窗口发送一个消息时,系统会立即调用窗口过程,并且在消息被处理之后,系统再调用回调函数。当回调函数返回之后,系统从调用SendMessageCallBack之后的代码开始执行。
27.7.4 BOOL SendNotifyMessage(hwnd,uMsg,wParam,lParam);
(1)SendNotifyMessage将一条消息添加到接收线程的“Send-Message”队列中并立即返回,这有点像PostMessage,但他与PostMessage有两点不同:
①如果向其他线程的窗口发送消息时, SendNotifyMessage是向接收线程的“Send-Message”添加消息的,而PostMessage是向接收线程的“Posted-Message”队列添加消息的。在系统的消息机制中, “Send-Message”队列中的消息总是比“Posted-Message”队列中的消息被优先处理。
②其次,当向调用线程自己创建的窗口发送消息时,SendNotifyMessage的工作就像SendMessage一样,直接调用窗口过程,同时等待窗口过程处理完才返回。
(2)很多消息只是用于通知的目的,用于告知应用程序某个状态发生了改变,所以发送线程没有必要等待接收线程处理完毕才返回。(如系统向窗口发送WM_SIZE、WM_MOVE等,操作系统没有必要停下来等用户处理了这些消息再继续。但有些消息,系统必须等待,如操作系统向一个窗口发送WM_CREATE,必须等待,如果窗口过程返回-1,则系统不建立这个窗口。
27.3.5 BOOL ReplyMessage(LRESULT lResult)
(1)接收线程可以在窗口过程还没处理完消息的情况下,提前向通过SendMessage发送消息的线程的“Reply-Message”添加一个应答消息,这会唤醒发送线程。
(2)接收线程把lResult作为消息处理结果传递给发送线程。当调用ReplyMessage后,发送线程被提前唤醒。当接收线程真正从窗口过程中返回时,系统将忽略这个返回值,即不再向发送线程回复本应在窗口过程正常结束才发送的应答消息。
(3)ReplyMessage必须在接收消息的窗口过程中被调用,而不能由某个调用Send*的线程调用,因为他是用来回复调用SendMessage的线程。前面讨论过的3个SendMessage*函数会立即返回。而SendMessage函数的返回,可以由窗口过程的实现者通过调用ReplyMessage来控制。
(4)如果消息不是通过SendMessage发送的,或者消息由同一个线程发送,ReplyMessage不起作用。这也是返回值指出的,如果在处理线程间的消息发送时调用了ReplyMessage返回TRUE。处理线程内的消息发送时调用ReplyMessage会返回FALSE。
(5)可以调用InSendMessage(Ex)来确定是线程间的消息发送还是线程内的发送消息。如果是线程间发送的会返回TRUE,线程内Send或Post的会返回FALSE。
【Sending a Message示例】
//WM_USER+5是由其他线程发送过来的消息。如果是其他进程发送过来的消息,那个进程里要先调用RegisterWindowMessage注册为全局的消息类型 case WM_USER + 5: ……//处理些其他事情 if (InSendMessage()) //是否是线程间发送的消息,只有线程间的才能Reply ReplyMessage(TRUE); //该消息己经处理了差不多了,SendMessage的线程可以被唤醒了,因为后面我还要做些其他事情,比如//弹出对话框(DialogBox),你就不要傻等下去了。注意:在哪里调用ReplyMessage就可以在那个什么//通知系统去唤醒SendMessage线程 DialogBox(hInst, "MyDialogBox", hwndMain, (DLGPROC) MyDlgProc); break;