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 一张图看懂区别
说了这么多,咱们用一张图把整个知识体系串起来:
3.6 实际项目中的选择
搞清楚了概念,那实际项目里怎么选?我个人的经验是这样的:
- 能用同步,优先用同步——逻辑简单,调试方便,不用考虑并发问题
- 必须异步的场景:硬件中断、长时间I/O操作、定时任务、多任务协作
- 异步回调里:只做最必要的事,把耗时操作扔到主循环或任务队列里处理
个人经验:我曾经在一个项目里,把文件系统的写操作直接放在中断回调里执行。结果呢?文件系统内部用了互斥锁,中断里拿锁直接死锁。从那以后,我定了个规矩——异步回调里只做"标记"和"通知",绝不碰复杂逻辑。
嗯,回调函数的概念和同步/异步的区别,说白了就这些。理解了这个基础,后面咱们才能深入讨论回调的各种高级用法和坑。记住一句话:回调是手段,解耦是目的。
公众号:蓝海资料掘金营,微信deep3321