漫谈callback

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

相信大部分同学和我一样,长久以来被以下几个概念困扰:

  • callback(回调函数)是啥?
  • 同步/异步/阻塞/非阻塞 又是啥?

今天,让我们一起来聊聊这些玩意。

callback概念解释

这是非常普通的一次方法调用(method call):

一般来说,method()的调用耗时很短,一般也就几毫秒。但如果method()内部涉及磁盘IO或者method()干脆就是网络调用(网络IO),可能就比较耗时了。至于多久才称得上“耗时”,不太好定义。当前案例中,不妨理解为:调用者无法忍受得到最终结果所耗费的时间。

main线程必须等待method返回结果后才能继续往下走,最终给客户端响应

如何解决耗时操作呢?你可能很容易就想到“异步”:

这里的“异步”,指的是狭隘的“开启一个子线程”。但是上面这个图并不完整,原本的代码里method()是有result的:

此时就有一个矛盾:虽然method的执行时异步的,但main主线程却需要获取method的返回结果

你可能很容易就想到了两种方案:

  • main()自己开启一个循环,不停地问method:好了没、好了没...,直到有结果
  • main阻塞等待

这里只是简单地设想两种获取子线程结果的方式,先不要深究如何实现

但实际上,对于调用者(main)来说两种方式是一样的,它都要等待method耗时操作完成才能收到result,从而执行后面的do something later操作。

于是产生了矛盾:

  • do something依赖于method的result
  • 但method很慢
  • 而我希望尽快do something并返回

比较好的处理方式是:

既然do something依赖于method的result,那么do something应该和method共同处理,所以我把do something挪到method内部

左图看似用了异步线程,但由于要获取异步结果,产生了阻塞等待,并没有把异步的功效最大化!

怎么把do something挪到method里面呢?最简单的方式当然是直接把do something整段代码“剪切”到method内部。但由于do something可能是变化的,是不确定的,所以最好不要在method里写死。为了能让method()帮我们执行自定义的操作,method()必须再提供一个入参:

callback()具有特异性,只有调用方才知道要做什么样的处理,所以最好由调用方指定具体的回调处理。

有call,有back,所以叫callback。

callback与设计模式

callback和策略模式很像,但个人觉得还是有细微差别的,主要是侧重点不同。

这是callback:

这是策略模式:

策略模式一般都是被调用方预先定义几种策略供选择,比如线程池拒绝策略。但我们自定义拒绝策略也是可以的

你要说策略模式也能callback,也勉强说得通...但两者出发点是不同的。但如果换一个场景,就会发现两者还是有本质的不同。比如跨系统的callback:

不同于进程内方法调用,系统间调用需要额外的ip+port

此时一般不太会有人称之为“策略模式”。

还有观察者模式也是如此,看起来也和callback很像,但出发点也多少有点区别。观察者模式出发点是当事件源发生改变时收到通知,而callback更像是有妥协的步骤拆分。当然,还是那句话,你要是觉得这几个本质是一样的,那就这么认为也无妨,没必要死扣定义。

callback与IO模型

很多人也学习过BIO、NIO、AIO,其中是AIO也有callback机制,它定义了一个CompletionHandler接口:

当某个I/O操作完成时,操作系统会自动回调completed()。所以,利用这个机制我们可以预先编写好回调函数:

总的来说,callback可大可小,宏观的有JVM内的回调函数、系统间的回调接口,微观的有操作系统的回调机制,甚至脱口秀也有callback。

一点小幽默,送给大家。

反思:callback与同步、异步、阻塞、非阻塞

我们通常说异步能“提升处理速度”,指的是在子线程处理耗时任务的过程中,主线程能继续执行自己的任务,就好比华罗庚教授的“烧水泡茶”理论。

不可否认,在烧水的过程中洗茶杯确实能提高一部分效率(两者没有依赖关系),但泡茶却一定要等到水烧开(有依赖关系)。也就是说,关键问题在于你“是否要获取结果”(有依赖关系),如果不要结果,确实可以直接return了,快得不得了。

由此可以引申出令大部分初学者懵逼的四个概念:

  • 同步阻塞
  • 同步非阻塞
  • IO多路复用
  • 异步非阻塞

搞懂这些概念的核心在于:调用者如何获取result(主动还是被动),以及获取result时调用者的状态(阻塞还是非阻塞)。

同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication) :

  • 所谓同步,就是在发出一个*调用*时,在没有得到结果之前,该*调用*就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由*调用者*主动等待这个*调用*的结果
  • 而异步则是相反,*调用*在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在*调用*发出后,*被调用者*通过状态、通知来通知调用者,或通过回调函数处理这个调用

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态:

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程

回到我们平时觉得“很快”的“异步编程”:

其实IO模型中的同步、异步和我们日常开发所谓的“同步异步”是不同的两个概念,不要搞混了。就以NIO为例,网络上一些博客会说NIO是异步非阻塞,但实际上从IO模型来讲,它是同步非阻塞,只有AIO才是真正的异步非阻塞。由于我们并不是做学术研究,没必要做严格的区分,只要能分清楚几种IO模型即可,默认日常使用多线程的场景也称为“异步”。

最后,日常开发中有没有同学见到过:

第三方client同时提供了异步调用和回调两种方式

结合上面的说法,这两种方式各自适合什么场景呢?

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

猜你喜欢

转载自blog.csdn.net/smart_an/article/details/134918720