一、回调函数
什么是回调函数?顾名思意,回调函数就是调用方被(被调用方)调用,有点绕口啊。一般的函数调用,都是一方向另一方发起调用,然后得到调用的结果。一般情况下,回调函数通过参数传递到指定回调的场景下。例如下面的代码:
int A(){return 5;}
int B(){return A();}
int main()
{int ret = B();std::cout<<"call result is:"<<ret<<std::endl;
}
可在某些情况下,B调用A函数时,A函数需要一个较长的时间来处理或者A函数中有一种行为需要在某种情况下触发通知A函数,但B函数又不能在此等待。这象不象是异步处理的情况?但异步只是其中的一种情况。这时候儿该如何处理呢?最简单的就是让A函数执行完成后再通知B函数不就可以了。OK,非常好。但如何通知呢?这方法可就多了,回调函数就是一种方式。
可以在调用A时,把需要处理的通知定义为一个函数,假设为与A同一模块内的C函数,跟随调用A时注册到B内,让B在允许的情况下调用这个函数C,而在C函数内处理相关的事宜,如下代码:
typedef void(*CB)(int) ;//函数指针
void C(int d){std::cout<<"call back value:"<<d<<std::endl;}
int A(CB b){b(5+5);return 5;
}
int B(){int r = A(C);return r;
}
int main()
{int ret = B();std::cout<<"call result is:"<<ret<<std::endl;
}
如果把B、C两个函数划分为一个模块1,把A函数划分成另外一个模块2,那么模块1调用模块2后,模块2又调用模块1(这个在线程里更容易理解).这就是调用方被调用。这样容易理解,如果较真非要是B函数调用A函数再返回调用B函数,这就需要处理一下内部的逻辑。有兴趣可以自己搞一搞,有点类似于递归,一定要有一个终结的点。
所以说一般情况下,回调函数是一个函数指针。当然,在c++中提供了一些函数对象的封装,可以把它们从宏观上都划到函数指针一类。
在C这类面向过程开发的语言中,回调函数是非常重要的一个环节。有过嵌入式开发经验的知道,一些官方提供的代码例程中会有一个特殊的指针数组,用来处理各种中断或者异常的回调。在Linux的内核中,也可以看到类似的代码。
回调函数和普通函数从本质上没有区别,这一点大家一定要明白,它也有类似的调用约定方式和相关属性等。可以这样理解,回调函数只是功能层次上的应用的不同,而与本身的定义无关,它就是一种函数。
二、回调函数的优势
在上面基本弄明白了,什么是回调函数,回调函数初步的应用。那么,为什么要使用回调函数呢?它有什么好处?最重要的一点,就是使用回调函数可以安全可靠的进行异步编程。由于其使用函数参数传递,那么这种回调的场景应用就变得非常灵活。同样,由于回调可以进行触发性管理,而不用将两个模块紧密的耦合在一起。达到了设计上的解耦。比如在常见的场景中,如果想实现A与B两个类间的互动,一般是互相包含头文件,然后在指定的情况下进行互相调用。可这样,就会让双方互相包含各自的头文件,紧密的耦合在一起。如下:
//a.h
class A{void Display();void WorkerA();
};//a.cpp
#include "b.h"
B b;
void A::Display(){std::cout<<"A Display!"<<std::endl;}
void A::WorkerA(){b.Display();}//b.h
class B{void Display();void WorkerB();
};
//b.cpp
#inlcude "a.h"
A a;
void B::Display(){std::cout<<"B Display!"<<std::endl;}
void B::WorkerB(){a.Display();}
而如果使用使用回调函数,只需要注册一下B或A的相关函数到对方即可。
三、回调函数的应用缺点
事件往往是有多面性的,回调函数有优势,则必有劣势。最常见的就是人们常说的“回调地狱”。大家可能听说过DLL地狱,那么回调地狱也类似。可能国外把一些容易搞成大问题的事件都叫地狱。这和中国人动不动就叫老天爷估计没啥区别。
回调地狱一般是指在异步编程中,回调函数的深层嵌套调用。即,A->B->C->D->E,但返回来可能是E->D->F->C->B->A,中间莫名多了一个工序,即使没有多一道工序,直接返回的话,也是很难阅读代码和定位问题的。大家可能有这种经验,特别是在读Linux内核的代码时,阅读工具往往无法跟踪函数指针的流程,而一般来说,函数指针往往多是回调函数。
回调函数的嵌套带来的复杂性,往往是后续维护者和学习者头大。那种线性的单纯的前进和回退的还好说一些,往往一些回调会拐个弯进行通知一下,然后再搞回来,这会让很多人在不懂得业务逻辑的情况下,不明所以。这也是异步回调难于调度的原因。如果中间再增加一些基于线程同步的通信,则更是复杂的难于理解。
那么问题已经出现,如何解决这种回调地狱的问题呢?仍然是编程的解决思想之一,引入中间层。但这个中间层,目的不是进一步回调,而是把回调的深度打平,既然深度大,就把一些不必要的深度去除即可。这种方式最简单的方法就是使用设计模式中的链式调用。这和c++11后的std::promis的机制类似。另外一种是继续抽象,增加一种异步机制,将复杂的异步回调隐藏的框架内部。
四、回调函数与异步编程
一般来说,异步编程和回调基本是一种难兄难弟。焦不离孟,孟不离焦。异步是相对于同步而言,而同步大家都明白,就是共同步进协调完成工作。而异步而不是,它是你干你的我干我的,大家谁干完就通知一声。等最后大家都完成了,这事儿也就完了。
同步更适合一些批量相同类似的工作的完成。比如工厂打螺丝,就可以流水线同步作业。因为每个熟练的工人,其打螺丝的速度基本是同步的。而农民的庄稼的收割则是异步的,庄稼不可能同时成熟,也不可能每块地大小一样。那么收割机就不能同步的安排作业,很可能割完A家的村南的一块地,又去B家割村东的,然后才可能回来割A家的村面另外一块地。
异步更有点人情社会的味道而同步更接近工厂生产的流程。
回调函数在设计模式中应用也非常广泛,如观察者模式、职责链模式等都可以在底层应用回调函数来实现。需要特别注意的是,在使用Lambda表达式作为回调函数时,由于Lambda表达式和闭包概念的密切相关性,特别是在处理对闭包外部变量的处理时,要千万谨慎。
五、例程
回调函数,不单纯限于函数,只要是具有函数性质的对象、表达式等都可以归为回调函数一类。说这些是因为在c++中为了安全,将函数进行了封装,既有普通的函数,也有Lambda,更有传统的仿函数(functor),还有std::mem_fn和std::function。至于还会发展出什么来,估计没人知道。
下面看相关的例程:
#include <functional>
class SerialCom
{
public:void initSerialCom(std::function<void(byte*,int)> func){this->m_onPackaged = func;}
private:int packageDoWith(byte *buf,int len){...if (isEnd){this->m_onPackaged(buf_,len_);}...}
private:std::function<void(byte*,int)> m_onPackaged = nullptr;
};void OnPackaged(byte*buf,int len){......
}
int main(){SerialCom scom;scom.initSerialCom(OnPackaged);return 0;
}
这个例程很简单,是一个串口通信的处理过程。在完成组包后将完整包回调给上层应用。
六、总结
之所以把回调函数放到高级篇,不是说回调函数本身有多么高级而是回调函数的应用是非常灵活的,用得精妙之处,完全可以放到高级的应用中。一个简单的回调函数,最大的优势是可控和安全。但回调函数不是没病的山梨儿,大家还是要斟酌考虑,不要滥用。