作者简介:大家好,我是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
进群,大家一起学习,一起进步,一起对抗互联网寒冬