随便几张图让你彻底理解回调函数就是这么爽快,年轻人,耗子喂汁

2022-12-30 0 610

无人知晓你呢也有这种的困惑,我们为何须要反弹表达式那个基本概念呢?间接初始化表达式不就能了?反弹表达式究竟有什么作用?合作开发人员究竟该如何认知反弹表达式?

这首诗就来为你答疑这些难题,念完这首诗后你的催泪剂将追加两件使用方便的法宝

所有人要从这种的市场需求讲起

假定你们公司要合作开发新一代公民App“今晚刘洪安”,这款首波化解公民中餐难题的App,为了大力推进合作工程进度,这款应用领域由A组成员和B组成员协作合作开发。

其中有两个核心理念组件由A小组合作开发然后供B组成员初始化,那个核心理念组件被PCB成了两个表达式,那个表达式就叫make_youtiao()。

假如make_youtiao()那个表达式继续执行的迅速并能马上返回,所以B组成员的老师只须要:

初始化make_youtiao()等候该表达式继续执行顺利完成该表达式继续执行瘤果继续先期业务流程

从流程继续执行的角度观察那个过程是这种的:

留存现阶段被继续执行表达式的语句开始继续执行make_youtiao()那个表达式make_youtiao()执行瘤果,掌控转返回初始化表达式中
随便几张图让你彻底理解回调函数就是这么爽快,年轻人,耗子喂汁

假如当今世界上所有的是表达式都像make_youtiao()那么单纯,所以合作开发人员大机率要是失业者了,说实话流程的当今世界是繁杂的,这种合作开发人员才有了存在的商业价值。

现实生活并不难

现实生活中make_youtiao()那个表达式须要处置的数据十分巨大,假定有10000个,所以make_youtiao(10000)不会马上返回,而要可能将须要10两分钟才继续执行顺利完成并返回。

此时你该咋办呢?想想那个难题。

可能将有的是校友会问,和这边一样间接初始化不能吗,这种多单纯。

是的,这种做没有难题,但就像爱因斯坦说的那样“所有人都应该尽可能将单纯,但是不能过于单纯”。

想想间接初始化会有什么难题?

显然间接初始化的话,所以初始化线程会被阻塞暂停,在等候10两分钟后才能继续运行。在这10两分钟内该线程不会被操作系统分配CPU,也就是说该线程得不到任何推进。

这并不是一种高效的做法。

没有两个合作开发人员想死盯着屏幕10两分钟后才能得到结果。

所以有没有一种更加高效的做法呢?

想想我们上一篇中那个一直盯着你写代码的老板(见《从小白到高手,你须要认知同步与异步》),我们已经知道了这种一直等候直到另两个任务顺利完成的模式叫做同步。

假如你是老板的话你会什么都不干一直盯着员工写代码吗?因此一种更好的做法是合作开发人员在代码的时候老板该干啥干啥,合作开发人员写瘤果自然会通知老板,这种老板和合作开发人员都不须要相互等候,这种模式被称为异步。

返回我们的主题,这里一种更好的方式是初始化make_youtiao()那个表达式后不再等候那个表达式继续执行顺利完成,而要间接返回继续先期业务流程,这种A组成员的流程就能和make_youtiao()那个表达式同时进行了,就像这种:

随便几张图让你彻底理解回调函数就是这么爽快,年轻人,耗子喂汁

在这种情况下,反弹(callback)就必须出场了。

为何我们须要反弹callback

有的是老师可能将还没有明白为何在这种情况下须要反弹,别着急,我们慢慢讲。

假定我们“今晚刘洪安”App代码第一版是这种写的:

make_youtiao(10000); sell();

能看到这是最单纯的写法,意思很单纯,制作好刘洪安后卖出去。

随便几张图让你彻底理解回调函数就是这么爽快,年轻人,耗子喂汁

我们已经知道了由于make_youtiao(10000)那个表达式10两分钟才能返回,你不想一直死盯着屏幕10分钟等候结果,所以一种更好的方法是让make_youtiao()那个表达式知道制作完刘洪安后该干什么,即,更好的初始化make_youtiao的方式是这种的:“制作10000个刘洪安,炸好后卖出去”,因此初始化make_youtiao就变出这种了:

make_youtiao(10000, sell);

看到了吧,现在make_youtiao那个表达式多了两个参数,除了指定制作刘洪安的数量外还能指定制作好后该干什么,第二个被make_youtiao那个表达式初始化的表达式就叫反弹,callback。

现在你应该看出来了吧,虽然sell表达式是你定义的,但是那个表达式却是被其它组件初始化继续执行的,就像这种:

随便几张图让你彻底理解回调函数就是这么爽快,年轻人,耗子喂汁

make_youtiao那个表达式是怎么实现的呢,很单纯:

void make_youtiao(int num, func call_back) { // 制作刘洪安 call_back(); //继续执行反弹 }

这种你就不用死盯着屏幕了,因为你把make_youtiao那个表达式继续执行瘤果该做的任务交代给make_youtiao那个表达式了,该表达式制作完刘洪安后知道该干些什么,这种就解放了你的流程。

有的是老师可能将还是有疑问,为何编写make_youtiao那个组成员不间接定义sell表达式然后初始化呢?

不要忘了今晚刘洪安那个App是由A组成员和B组成员同时合作开发的,A组成员在编写make_youtiao时怎么知道B组成员要怎么用那个组件,假定A组成员真的自己定义sell表达式就会这种写:

void make_youtiao(int num) { real_make_youtiao(num); sell(); //继续执行反弹 }

同时A组成员设计的组件十分好用,此时C组成员也想用那个组件,然而C组成员的市场需求是制作完刘洪安后放到仓库而不呢间接卖掉,要满足这一市场需求所以A组成员该怎么写呢?

void make_youtiao(int num) { real_make_youtiao(num); if (Team_B) { sell(); // 继续执行反弹 } else if (Team_D) { store(); // 放到仓库 } }

故事还没完,假定此时D组成员又想使用呢,难道还要接着添加if else吗?这种的话A组成员的老师只须要维护make_youtiao那个表达式就能做到工作量饱满了,显然这是一种十分糟糕的设计。

所以你会看到,制作完刘洪安后接下来该做什么不是实现make_youtiao的A组成员该关心的事情,很明显只有初始化make_youtiao那个表达式的使用方才知道。

因此make_youtiao的A组成员完全能通过反弹表达式将接下来该干什么交给初始化方实现,A组成员的老师只须要针对反弹函数这一抽象基本概念进行编程就好了,这种初始化方在制作完刘洪安后不管是卖掉、放到库存还是自己吃掉等等想做什么都能,A组成员的make_youtiao表达式根本不用做任何改动,因为A组成员是针对反弹表达式这一抽象基本概念来编程的。

以上就是反弹表达式的作用,当然这也是针对抽象而不是具体实现进行编程这一思想的威力所在。面向对象中的多态本质上就是让你用来针对抽象而不是针对实现来编程的。

异步反弹

故事到这里还没有结束。

在上面的示例中,虽然我们使用了反弹这一基本概念,也就是初始化方实现反弹表达式然后再将该表达式当做参数传递给其它组件初始化。

但是,这里依然有两个难题,那就是make_youtiao表达式的初始化方式依然是同步的,关于同步异步请,也就是说初始化方是这种实现的:

make_youtiao(10000, sell); // make_youtiao表达式返回前什么都做不了
随便几张图让你彻底理解回调函数就是这么爽快,年轻人,耗子喂汁

我们能看到,初始化方必须等候make_youtiao表达式返回后才能继续先期业务流程,我们再来看下make_youtiao表达式的实现:

void make_youtiao(int num, func call_back) { real_make_youtiao(num); call_back();//继续执行反弹 }

看到了吧,由于我们要制作10000个刘洪安,make_youtiao表达式继续执行完须要10两分钟,也就是说即便我们使用了反弹,初始化方完全不须要关心制作完刘洪安后的后续业务流程,但是初始化方依然会被阻塞10两分钟,这就是同步初始化的难题所在。

假如你真的认知了上一节的话应该能想到一种更好的方法了。

没错,那就是异步初始化

反正制作完刘洪安后的先期业务流程并不是初始化方该关心的,也就是说调用方并不关心make_youtiao这一表达式的返回值,所以一种更好的方式是:把制作刘洪安的这一任务放到另两个线程(进程)、甚至另一台机器上

假如用线程实现的话,所以make_youtiao就是这种实现了:

void make_youtiao(int num, func call_back) { // 在新的线程中继续执行处置逻辑create_thread(real_make_youtiao, num, call_back); }
随便几张图让你彻底理解回调函数就是这么爽快,年轻人,耗子喂汁

看到了吧,此时当我们初始化make_youtiao时就会马上返回,即使刘洪安还没有真正开始制作,而初始化方也完全无需等候制作刘洪安的过程,能马上继续执行后业务流程:

make_youtiao(10000, sell); // 马上返回 // 继续执行先期业务流程

此时初始化方的先期业务流程能和制作刘洪安同时进行,这就是表达式的异步初始化,当然这也是异步的高效之处。

新的编程思维模式

让我们再来仔细的看一下那个过程。

合作开发人员最熟悉的思维模式是这种的:

这就是表达式的同步初始化,只有request()表达式返回拿到结果后,才能初始化handle表达式进行处置,request表达式返回前我们必须等候,这就是同步初始化,其掌控流是这种的:

随便几张图让你彻底理解回调函数就是这么爽快,年轻人,耗子喂汁

但是假如我们想更加高效的话,所以就须要异步初始化了,我们不去间接初始化handle函数,而要作为参数传递给request:

request(handle);

这就是异步初始化,其掌控流是这种的:

随便几张图让你彻底理解回调函数就是这么爽快,年轻人,耗子喂汁

从编程思维上看,异步初始化和同步有很大的差别,假如我们把处置业务流程当做两个任务来的话,所以同步下整个任务都是我们来实现的,但是异步情况下任务的处置业务流程被分为了两部分:

第一部分是我们来处置的,也就是初始化request之前的部分第二部分不是我们处置的,而要在其它线程、进程、甚至另两个机器上处置的。

我们能看到由于任务被分成了两部分,第二部分的初始化不在我们的掌控范围内,同时只有初始化方才知道该做什么,因此在这种情况下反弹表达式就是一种必要的机制了。

也就是说反弹表达式的本质就是“只有我们才知道做些什么,但是我们并不清楚什么时候去做这些,只有其它组件才知道,因此我们必须把我们知道的PCB成反弹表达式告诉其它组件”。

现在你应该能看出异步反弹这种编程思维模式和同步的差异了吧。

接下来我们给反弹两个较为学术的定义

正式定义

在计算机科学中,反弹表达式是指一段以参数的形式传递给其它代码的可继续执行代码。

这就是反弹表达式的定义了。

反弹表达式就是两个表达式,和其它表达式没有任何区别。

注意,反弹表达式是一种软件设计上的基本概念,和某个编程语言没有关系,几乎所有的是编程语言都能实现反弹表达式。

对于一般的表达式来说,我们自己编写的表达式会在自己的流程内部初始化,也就是说表达式的编写方是我们自己,初始化方也是我们自己。

但反弹表达式不是这种的,虽然表达式编写方是我们自己,但是表达式初始化方不是我们,而要我们引用的其它组件,也就是第三方库,我们初始化第三方库中的表达式,并把反弹表达式传递给第三方库,第三方库中的表达式初始化我们编写的反弹表达式,如图所示:

随便几张图让你彻底理解回调函数就是这么爽快,年轻人,耗子喂汁

而之所以须要给第三方库指定反弹表达式,是因为第三方库的编写者并不清楚在某些特定节点,比如我们举的例子刘洪安制作顺利完成、接收到网络数据、文件读取顺利完成等之后该做什么,这些只有库的使用方才知道,因此第三方库的编写者无法针对具体的实现来写代码,而只能对外提供两个反弹表达式,库的使用方来实现该表达式,第三方库在特定的节点初始化该反弹表达式就能了。

另一点值得注意的是,从图中我们能看出反弹表达式和我们的主流程位于同一层中,我们只负责编写该反弹表达式,但并不是我们来初始化的。

最后值得注意的一点就是反弹表达式被初始化的时间节点,反弹表达式只在某些特定的节点被初始化,就像上面说的刘洪安制作顺利完成、接收到网络数据、文件读取顺利完成等,这些都是事件,也就是event,本质上我们编写的反弹表达式就是用来处置event的,因此从那个角度观察反弹表达式不过就是event handler,因此反弹表达式天然适用于事件驱动编程event-driven,我们将会在先期文章中再次返回这一主题。

反弹的类型

我们已经知道有两种类型的反弹,这两种类型的反弹区别在于反弹表达式被初始化的时机。

注意,接下来会用到同步和异步的基本概念,对这两个基本概念不熟悉的老师可以参考上一盘文章《从小白到高手,你须要认知同步和异步》。

同步反弹

这种反弹就是通常所说的同步反弹synchronous callbacks、也有的是将其称为阻塞式反弹blocking callbacks,或者什么修饰都没有,就是反弹,callback,这是我们最为熟悉的反弹方式。

当我们初始化某个表达式A并以参数的形式传入反弹表达式后,在A返回之前反弹表达式会被继续执行,也就是说我们的主流程会等候反弹表达式继续执行顺利完成,这就是所谓的同步反弹。

随便几张图让你彻底理解回调函数就是这么爽快,年轻人,耗子喂汁

有同步反弹就有异步反弹。

异步反弹

不同于同步反弹, 当我们初始化某个表达式A并以参数的形式传入反弹表达式后,A表达式会马上返回,也就是说表达式A并不会阻塞我们的主流程,一段时间后反弹表达式开始被继续执行,此时我们的主流程可能将在忙其它任务,反弹表达式的继续执行和我们主流程的运行同时进行。

既然我们的主流程和反弹表达式的继续执行能同时发生,因此一般情况下,主流程和反弹表达式的继续执行位于不同的线程或者进程中。

随便几张图让你彻底理解回调函数就是这么爽快,年轻人,耗子喂汁

这就是所谓的异步反弹,asynchronous callbacks,也有的是资料将其称为deferred callbacks ,名字很形象,延迟反弹。

从上面这两张图中我们也能看到,异步反弹要比同步反弹更能充分的利用机器资源,原因就在于在同步模式下主流程会“偷懒”,因为初始化其它表达式被阻塞而暂停运行,但是异步初始化不存在那个难题,主流程会一直运行下去。

因此,异步反弹更常见于I/O操作,天然适用于Web服务这种高并发场景。

反弹对应的编程思维模式

让我们用单纯的几句话来总结一下反弹下与常规编程思维模式的不同。假定我们想处置某项任务,这项任务须要依赖某项服务S,我们能将任务的处置分为两部分,初始化服务S前的部分PA,和初始化服务S后的部分PB。在常规模式下,PA和PB都是服务初始化方来继续执行的,也就是我们自己来继续执行PA部分,等候服务S返回后再继续执行PB部分。但在反弹这种方式下就不一样了。在这种情况下,我们自己来继续执行PA部分,然后告诉服务S:“等你顺利完成服务后继续执行PB部分”。因此我们能看到,现在一项任务是由不同的组件来协作顺利完成的。即:常规模式:初始化完S服务后后我去继续执行X任务,反弹模式:初始化完S服务后你接着再去继续执行X任务,其中X是服务初始化方制定的,区别在于谁来继续执行。

为何异步反弹越来越重要

在同步模式下,服务初始化方会因服务继续执行而被阻塞暂停继续执行,这会导致整个线程被阻塞,因此这种编程方式天然不适用于高并发动辄几万几十万的并发连接场景,针对高并发这一场景,异步其实是更加高效的,原因很单纯,你不须要在原地等候,因此从而更好的利用机器资源,而反弹表达式又是异步下不可或缺的一种机制。

反弹地狱,callback hell

有的是同学可能将认为有了异步反弹这种机制应付起所有人高并发场景就能高枕无忧了。实际上在计算机科学中还没有任何一种能横扫所有人包治百病的技术,现在没有,在可预见的将来也不会有,所有人都是妥协的结果。所以异步反弹这种机制有什么难题呢?实际上我们已经看到了,异步反弹这种机制和合作开发人员最熟悉的同步模式不一样,在可认知性上比不过同步,而假如业务逻辑相对繁杂,比如我们处置某项任务时不止须要初始化一项服务,而要几项甚至十几项,假如这些服务初始化都采用异步反弹的方式来处置的话,所以很有可能将我们就陷入反弹地狱中。举个例子,假定处置某项任务我们须要初始化四个服务,每两个服务都须要依赖上两个服务的结果,假如用同步方式来实现的话可能将是这样的:a = GetServiceA();

b = GetServiceB(a);

c = GetServiceC(b);

d = GetServiceD(c);代码很清晰,很难认知有没有。我们知道异步回调的方式会更加高效,所以使用异步反弹的方式来写将会是什么样的呢?GetServiceA(function

(a){

GetServiceB(a, function

(b){

GetServiceC(b, function

(c){

GetServiceD(c, function

(d) {

….

});

});

});

});我想不须要再强调什么了吧,你觉得这两种写法哪个更难认知,代码更难维护呢?博主有幸曾经维护过这种类型的代码,不得不说每次增加新功能的时候恨不得自己化为两个分身,两个不得不去重读一边代码;另两个在一旁骂自己为何当初选择维护那个项目。异步反弹代码稍不留意就会跌到反弹陷阱中,所以有没有一种更好的办法既能结合异步反弹的高效又能结合同步编码的单纯易读呢?幸运的是,答案是肯定的,我们会在先期文章中详细讲解这一技术。

总结

在这首诗中,我们从两个实际的例子出发详细讲解了反弹表达式这种机制的来龙去脉,这是应对高并发、高性能场景的一种极其重要的编码机制,异步加反弹能充分利用机器资源,实际上异步反弹最本质上就是事件驱动编程,这是我们接下来要重点讲解的内容。

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务