在Windows应用程序开发中,有很多地方使用了回调函数。一般的开发并不关心谁来调用这些函数,但如果开发复杂的多线程协作处理程序,或者研究操作系统中程序的“操作权限”问题,您就需要知道您写的这段代码是由谁来调用的,在哪个线程或进程中执行的。
最常见的就是Windows消息响应函数。一般的书本并不讨论这些函数是如何被调用的,只是说消息产生时,这些函数将会执行。初学者也许会以为,如果两个消息一起发出,那么它们的响应函数会同时执行。事实并不是这样。这些函数是串行执行的,也就是说只有执行完一个之后,另一个才有机会被调用。不仅如此,这些函数也不是由什么系统调用的,而是由一个确定的线程中统一管理,这个线程正是你的程序自己的主线程。
您可能自己并没有写这样的代码,但您使用的基础库中有。无论是MFC还是ATL,您都可以在它们的源代码中找到类似下面这样的内容:
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
这个是消息循环,也称消息泵。它像个抽水机似的,把消息从一个地方抽出来,然后洒给各个消息响应函数处理。因此也可以说,消息响应函数是从线程的消息泵开始调用的。
还有一种作为参数传进去的回调函数。这些回调函数类似于Java中的那些接口,当调用某个函数时,把它当成参数传进去。在适当的时候,这些被函数会被调用。很多窗口枚举函数就是这样的,比如:EnumChildWindows、EnumDesktopWindows、EnumWindows等。这些函数其实并不是异步函数,在函数返回时,相应的功能已经完成,被传进去的回调函数也已经被调用过了。这种情况下,回调函数可看作是被相应的功能函数调用的。
钩子(HOOK)处理函数也是一种回调函数。我曾一度以为它是由操作系统的某个线程调用的,但事实并非如此。我在钩子函数里使用GetCurrentThreadId测试了一下,发现调用钩子函数的线程竟然是安装钩子的那个线程。那么这个调用是从哪发起的呢?原来还是消息泵!当调用GetMessage或PeekMessage函数时,并不只是把消息从队列中取出来这么简单,而是做了更复杂的工作。具体这些API是如何实现的我们不得而知,但有一个我进行了验证:不启动消息泵,钩子啥也钩不到。所以归根结底,钩子处理函数还是由线程的消息泵开始调用的。
Windows使用消息和回调函数来调度程序的执行,同一线程设置的回调函数,无论是消息响应还是钩子,最后仍在这一线程内执行,消息泵负责调度。
了解这一点,我们就可以在这些回调函数中放心地使用任何资源了,不用担心互斥、死锁。但是要担心另外一些问题:比如消息响应的嵌套冲突导致的数据变化、消息的循环发送等。一般这些问题都易于重现,所以还算容易发现和处理。
作者:苏林