当前位置 博文首页 > 码农的荒岛求生:【计算机内功心法】六:10张图让你彻底理解回调

    码农的荒岛求生:【计算机内功心法】六:10张图让你彻底理解回调

    作者:码农的荒岛求生 时间:2021-02-01 22:22

    不知你是不是也有这样的疑惑,我们为什么需要回调函数这个概念呢?直接调用函数不就可以了?回调函数到底有什么作用?程序员到底该如何理解回调函数?

    这篇文章就来为你解答这些问题,读完这篇文章后你的武器库将新增一件功能强大的利器

    一切要从这样的需求说起

    假设你们公司要开发下一代国民App“明日油条”,一款主打解决国民早餐问题的App,为了加快开发进度,这款应用由A小组和B小组协同开发。

    其中有一个核心模块由A小组开发然后供B小组调用,这个核心模块被封装成了一个函数,这个函数就叫make_youtiao()。

    如果make_youtiao()这个函数执行的很快并可以立即返回,那么B小组的同学只需要:

    1. 调用make_youtiao()
    2. 等待该函数执行完成
    3. 该函数执行完后继续后续流程

    从程序执行的角度看这个过程是这样的:

    1. 保存当前被执行函数的上下文
    2. 开始执行make_youtiao()这个函数
    3. make_youtiao()执行完后,控制转回到调用函数中
    1602596924819
    1602596924819

    如果世界上所有的函数都像make_youtiao()这么简单,那么程序员大概率就要失业了,还好程序的世界是复杂的,这样程序员才有了存在的价值。

    现实情况并不容易

    现实中make_youtiao()这个函数需要处理的数据非常庞大,假设有10000个,那么make_youtiao(10000)不会立刻返回,而是可能需要10分钟才执行完成并返回。

    这时你该怎么办呢?想一想这个问题。

    可能有的同学就像把头埋在沙子里的鸵鸟一样:和刚才一样直接调用不可以吗,这样多简单。

    是的,这样做没有问题,但就像爱因斯坦说的那样“一切都应该尽可能简单,但是不能过于简单”。

    想一想直接调用会有什么问题?

    显然直接调用的话,那么调用线程会被阻塞暂停,在等待10分钟后才能继续运行。在这10分钟内该线程不会被操作系统分配CPU,也就是说该线程得不到任何推进。

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

    没有一个程序员想死盯着屏幕10分钟后才能得到结果。

    那么有没有一种更加高效的做法呢?

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

    如果你是老板的话你会什么都不干一直盯着员工写代码吗?因此一种更好的做法是程序员在代码的时候老板该干啥干啥,程序员写完后自然会通知老板,这样老板和程序员都不需要相互等待,这种模式被称为异步。

    回到我们的主题,这里一种更好的方式是调用make_youtiao()这个函数后不再等待这个函数执行完成,而是直接返回继续后续流程,这样A小组的程序就可以和make_youtiao()这个函数同时进行了,就像这样:

    1602597258523
    1602597258523

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

    为什么我们需要回调callback

    有的同学可能还没有明白为什么在这种情况下需要回调,别着急,我们慢慢讲。

    假设我们“明日油条”App代码第一版是这样写的:

    make_youtiao(10000);
    sell();

    可以看到这是最简单的写法,意思很简单,制作好油条后卖出去。

    1602597572027
    1602597572027

    我们已经知道了由于make_youtiao(10000)这个函数10分钟才能返回,你不想一直死盯着屏幕10分钟等待结果,那么一种更好的方法是让make_youtiao()这个函数知道制作完油条后该干什么,即,更好的调用make_youtiao的方式是这样的:“制作10000个油条,炸好后卖出去”,因此调用make_youtiao就变出这样了:

    make_youtiao(10000, sell);

    看到了吧,现在make_youtiao这个函数多了一个参数,除了指定制作油条的数量外还可以指定制作好后该干什么,第二个被make_youtiao这个函数调用的函数就叫回调,callback。

    现在你应该看出来了吧,虽然sell函数是你定义的,但是这个函数却是被其它模块调用执行的,就像这样:

    1602598090503
    1602598090503

    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吗?这个问题该怎么解决呢?关于这个问题的答案,你可以参考这里。

    异步回调

    故事到这里还没有结束。

    在上面的示例中,虽然我们使用了回调这一概念,也就是调用方实现回调函数然后再将该函数当做参数传递给其它模块调用。

    但是,这里依然有一个问题,那就是make_youtiao函数的调用方式依然是同步的,关于同步异步请参考《从小白到高手,你需要理解同步与异步》,也就是说调用方是这样实现的:

    make_youtiao(10000, sell);
    // make_youtiao函数返回前什么都做不了
    1602598090503
    1602598090503

    我们可以看到,调用方必须等待make_youtiao函数返回后才可以继续后续流程,我们再来看下make_youtiao函数的实现:

    void make_youtiao(int num, func call_back) {
        real_make_youtiao(num);
        call_back(); //执行回调 
    }

    看到了吧,由于我们要制作10000个油条,make_youtiao函数执行完需要10分钟,也就是说即便我们使用了回调,调用方完全不需要关心制作完油条后的后续流程,但是调用方依然会被阻塞10分钟,这就是同步调用的问题所在。

    如果你真的理解了上一节的话应该能想到一种更好的方法了。

    没错,那就是异步调用。

    关于异步回调,你可以参考这里。

    新的编程思维模式

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

    程序员最熟悉的思维模式是这样的:

    1. 调用某个函数,获取结果
    2. 处理获取到的结果
    res = request();
    handle(res);

    这就是函数的同步调用,只有request()函数返回拿到结果后,才能调用handle函数进行处理,request函数返回前我们必须等待,这就是同步调用,其控制流是这样的:

    1602683285172
    1602683285172

    但是如果我们想更加高效的话,那么就需要异步调用了,我们不去直接调用handle函数,而是作为参数传递给request:

    request(handle);

    我们根本就不关心request什么时候真正的获取的结果,这是request该关心的事情,我们只需要把获取到结果后该怎么处理告诉request就可以了,因此request函数可以立刻返回,真的获取结果的处理可能是在另一个线程、进程、甚至另一台机器上完成。

    这就是异步调用,其控制流是这样的:

    1602684355493
    1602684355493

    从编程思维上看,异步调用和同步有很大的差别,如果我们把处理流程当做一个任务来的话,那么同步下整个任务都是我们来实现的,但是异步情况下任务的处理流程被分为了两部分: