3. 回调函数概念:什么是回调函数,同步回调与异步回调的区别

好,咱们今天聊一个嵌入式开发里绕不开的话题——回调函数。

我记得刚入行那会儿,第一次看到别人代码里把函数名当参数传进去,整个人是懵的。心想:函数还能当参数?这不是把代码当数据使吗?后来真正搞懂了,才发现这玩意儿简直是嵌入式开发的瑞士军刀。

3.1 什么是回调函数?

说白了,回调函数就是:你写一个函数,但不直接调用它,而是把它当作参数传给另一个函数。由那个函数在合适的时机回头调用你写的这个函数。

嗯,这里要注意——"回调"这个名字很形象。你把自己的函数"交"出去,别人在需要的时候"回"过来调用你。就这么简单。

核心定义:回调函数是通过函数指针传递的、由他人调用的函数。

来看个最朴素的例子:

// 回调函数:打印结果
void print_result(int x) {
    printf("结果是: %d\n", x);
}

// 高阶函数:接收回调
void calculate(int a, int b, void (*callback)(int)) {
    int sum = a + b;
    callback(sum);  // 回头调用你传进来的函数
}

int main() {
    calculate(3, 5, print_result);  // 把print_result当参数传进去
    return 0;
}

你看,calculate 函数根本不知道 print_result 内部怎么实现的。它只管算完结果,然后调用你给它的那个函数指针。这就是回调的精髓——解耦

3.2 同步回调 vs 异步回调

搞清楚了回调是什么,接下来这个问题更关键:回调什么时候发生?

我在项目中遇到过不少新手,把同步回调和异步回调混为一谈,结果调试的时候死活找不到 bug 在哪。其实这两者区别非常清晰。

对比维度 同步回调 异步回调
调用时机 函数返回之前立即调用 函数返回之后,将来某个时刻调用
执行流程 阻塞式,调用者等待回调完成 非阻塞式,调用者不等待
典型场景 排序比较、遍历处理 中断处理、定时器、网络I/O
线程关系 同一线程内 可能不同线程或中断上下文
可重入要求 通常不需要 必须考虑可重入性

3.3 同步回调:你等我办完事

同步回调是最直观的形式。你调用一个函数,这个函数在返回之前就会调用你的回调。整个过程是阻塞的。

举个例子,标准C库里的 qsort 就是典型的同步回调:

// 同步回调:比较函数
int compare_int(const void *a, const void *b) {
    return *(int*)a - *(int*)b;
}

int main() {
    int arr[] = {5, 3, 8, 1, 9};
    qsort(arr, 5, sizeof(int), compare_int);
    // 执行到这里时,compare_int已经被反复调用了多次
    // qsort返回之前,所有回调都已经执行完毕
    return 0;
}

我个人习惯把同步回调叫做"插队式调用"。你想想看,qsort 在执行过程中,时不时"插队"调用一下你的 compare_int,等比较完再继续排序。整个过程都在同一个线程里,顺序是确定的。

避坑指南:我曾经在同步回调里写了一个死循环,结果整个系统卡死了。因为同步回调是在调用者的线程里执行的,回调里一旦阻塞,调用者也别想跑。所以同步回调里千万别做耗时操作。

3.4 异步回调:你先走,我回头找你

异步回调就完全不一样了。你调用一个函数启动某个操作,这个函数立刻返回,不等待结果。等操作完成了,系统在某个时刻(可能是中断里、另一个线程里)调用你的回调函数。

为什么会这样?因为有些操作太慢了——比如从串口读数据、等待按键按下、网络发包。如果同步等待,CPU 就啥也别干了。

看个嵌入式里常见的例子:

// 异步回调:定时器超时处理
void timer_timeout(void *arg) {
    // 这个函数在定时器中断里被调用
    // 注意:这里不能做复杂操作!
    uint32_t *count = (uint32_t*)arg;
    (*count)++;
    printf("定时器触发了 %d 次\n", *count);
}

int main() {
    uint32_t counter = 0;
    
    // 启动一个100ms的定时器,超时后调用timer_timeout
    // start_timer 立即返回,不等待
    start_timer(100, timer_timeout, &counter);
    
    // 主循环继续做其他事情
    while(1) {
        // 处理其他任务...
        // 定时器超时的时候,timer_timeout会在中断里被调用
    }
}

这里的关键区别:start_timer 调用完就返回了,但 timer_timeout 可能在 100ms 之后才被执行。调用者和回调函数不在同一个时间线上。

重要警告:异步回调通常运行在中断上下文或独立线程中。这意味着:

  • 不能调用阻塞函数(如 printf、malloc)
  • 必须保证可重入性
  • 注意临界区保护(关中断或使用互斥锁)
  • 回调执行时间要尽可能短

3.5 一张图看懂区别

说了这么多,咱们用一张图把整个知识体系串起来:

回调函数体系 回调函数 同步回调 异步回调 阻塞等待 同一线程 顺序执行 非阻塞 可能不同线程 延迟执行 典型应用场景 qsort 比较函数 遍历处理回调 定时器超时 中断处理 核心区别:回调在函数返回前还是返回后执行

3.6 实际项目中的选择

搞清楚了概念,那实际项目里怎么选?我个人的经验是这样的:

  • 能用同步,优先用同步——逻辑简单,调试方便,不用考虑并发问题
  • 必须异步的场景:硬件中断、长时间I/O操作、定时任务、多任务协作
  • 异步回调里:只做最必要的事,把耗时操作扔到主循环或任务队列里处理

个人经验:我曾经在一个项目里,把文件系统的写操作直接放在中断回调里执行。结果呢?文件系统内部用了互斥锁,中断里拿锁直接死锁。从那以后,我定了个规矩——异步回调里只做"标记"和"通知",绝不碰复杂逻辑。

嗯,回调函数的概念和同步/异步的区别,说白了就这些。理解了这个基础,后面咱们才能深入讨论回调的各种高级用法和坑。记住一句话:回调是手段,解耦是目的


公众号:蓝海资料掘金营,微信deep3321