什么是回调函数?
用一句话来形容:回调函数还真有点像您随身带的BP机,告诉别人号码,在它有事情时Call您。换句话说,模块A将一个函数以函数指针的形式注册到模块B,模块B满足一定条件时来调用模块A注册过来的函数,来完成一定的功能,这个过程就是函数的回调。
什么时候调用回调函数?
我们可以把C中完成这个功能的主函数以指针的形式注册到S中,并且保存在S中,当然这个函数的参数可以是C完成这个功能所需要的数据,这样就可以在S中直接调用C注册过来的函数指针,来完成这个功能。下面举个例子:
BNEP的文件中要包含下层(L2CAP)的头文件,但是L2CAP包含BNEP头文件是没有道理的。这样我们就会用到回调函数。在BNEP实现一个函数,然后注册到L2CAP层。参数可以是L2CAP才有的,BNEP需要的数据,这样L2CAP可以根据时机调用BNEP的函数,达到回调的目的。
回调函数经典示例
1. 主线程创建辅助线程—— AssistThread
2. 创建AssistThread的时候会把主线程要执行的一个函数CBFunc以函数指针的形式传递到AssistThread中
3. 在AssistThread结束的时候会用创建AssistThread时传递过来的函数指针,以回调函数的形式调用CBFunc
#include <windows.h>
#include <stdio.h>
typedef int (WINAPI *PFCALLBACK)(int Param1,int Param2) ; /* 定义了一个函数指针类型 */
int WINAPI CBFunc(int Param1,int Param2);
ULONG WINAPI AssistThread(LPVOID Param)
{
PFCALLBACK gCallBack;
/* 定义回调函数指针的变量 */
TCHAR Buffer[256];
MSG Msg;
DWORD StartTick;
int Step=1;
HDC hDC = GetDC(HWND_DESKTOP);
gCallBack = (PFCALLBACK)Param;
/* 给回掉函数变量赋值 */
for(;Step<200;Step++)
{
StartTick = GetTickCount();
/* 这一段为线程交出部分运行时间以让系统处理其他事务 */
for(;GetTickCount()-StartTick<10;)
{
if(PeekMessage(&Msg,NULL,0,0,PM_NOREMOVE) )
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
}
/* 把运行情况打印到桌面 */
sprintf(Buffer,"Running %04d",Step);
if(NULL!=hDC)
TextOut(hDC,30,50,Buffer,strlen(Buffer));
}
(*gCallBack)(Step,1);
/* 延时一段时间后调用回调函数 */
ReleaseDC (HWND_DESKTOP,hDC);
/* 结束 */
return 0;
}
void WINAPI TestCallBack(PFCALLBACK Func)
{
HANDLE hThread;
DWORD ThreadID=0;
if(NULL==Func)return;
hThread = CreateThread(
NULL,
NULL,
AssistThread,
(LPVOID)Func,
/* 将回调函数指针Func传递到线程AssistThread的主函数中 */
0,
&ThreadID
);
WaitForSingleObject( hThread, INFINITE );
return;
}
int WINAPI CBFunc(int Param1,int Param2)
{
int res= Param1+Param2;
TCHAR Buffer[256]="";
sprintf(Buffer,"callback result = %d",res);
MessageBox(NULL,Buffer,"Testing",MB_OK); /* 演示回调函数被调用 */
return res;
}
void main()
{
TestCallBack(CBFunc); /* 函数的参数CBFunc为回调函数的地址 */
return;
}
注意事项
1. 避免死锁
2. 充分考虑服务器性能
如图2所表示的那样,对于客户端和服务器端的功能来说这样的形式感觉是合情合理的,服务器端触发了客户端的动作,客户端要到服务器端取得数据。
但是我们从图2这个很简单的时序图关系来说,这样是很危险的,在服务器的线程中调用了客户端的回调函数,然后还接着占用服务器端的线程来有调用服务器的接口取得数据,这样就很可能造成系统的死锁。
所以正确的方法是如果客户端完成这个功能需要从服务器端取得数据的话,最好有个线程切换的过程,这样便能避免出现死锁了,如图3所示。
还有一种情况的回调会产生死锁,具体情况下图表示得很清楚。
如上图所示,这种情况下很容易出现死锁,在S端首先Lock住自己的信号量C,然后将要调用C端的回调函数(在回调函数中C端首先要Lock自己的信号量A),这时C端的另外一个线程正好要调用S端的接口取得数据,C首先Lock住自己的信号量A,然后将要调用S端的接口(S的接口中要Lock信号量C)。
很明显这样就出现了相互等待的情况而出现死锁。
上面出现的死锁,问题实际上是出在客户端,如果客户端在回调函数中,和在调用服务器端接口的时候使用不同的信号量就可以避免死锁了。
使用回调函数还要注意性能问题,在回调函数中不能有很浪费时间的处理,道理和上面一样,要知道回调函数本身被调用是占用服务器端的线程的,服务器端还要处理自己要做的事情,并且不是只为一个客户端来服务,如果这个服务器的资源长时间被这个回调函数占用的话,对整个系统来说都是不好的。
和上面问题一样,在客户端来个线程切换就可以解决这个问题。