编程中的概念解析
1 同步、异步
1.1 同步与异步编程的区别
同步(Synchronous):
任务按代码顺序阻塞执行,必须等待当前操作完成后才能执行下一操作。程序执行流在时间上严格耦合,逻辑简单但效率较低。类比1:排队买奶茶,必须等前一人完成才能轮到自己。
类比2:想象你在厨房做饭,严格按照步骤执行:洗菜 → 切菜 → 炒菜,每一步都必须等前一步完成,整个过程是顺序且阻塞的。
异步(Asynchronous):
任务触发后无需等待完成,程序继续执行后续代码。结果通过回调函数、事件循环或Promise等机制处理,效率高但逻辑复杂,适用于耗时长的操作(如网络请求、文件读写、硬件交互)。类比1:餐厅点餐后继续聊天,厨师后台备餐,完成后通知取餐。
类比2:下单外卖(异步操作)→ 打扫房间(无需等待外卖)→ 外卖送达时收到通知,外卖的准备过程与打扫房间并行进行,外卖完成时通过通知(回调)告知你。
关键选择:任务是否依赖前序结果?是 → 同步;否 → 异步
1.2 代码示例说明
- 同步示例:顺序执行
1 |
|
特点:任务2必须等待任务1完成后才能开始。程序总耗时 = 2秒(task1) + 1秒(task2) = 3秒。
- 异步示例:并发执行(使用线程模拟)
1 |
|
特点:任务1和任务2同时运行,互不等待。程序总耗时 = 2秒(以耗时最长的任务为准)。
2 阻塞、非阻塞
2.1 阻塞与非阻塞的区别
- 阻塞(Blocking)
- 程序在调用某个操作时,必须等待该操作完成才能继续执行后续代码。
- 如果操作未完成(如等待数据读取),程序会卡住(暂停),直到操作完成。
- 非阻塞(Non-blocking)
- 程序在调用某个操作时,立即返回,无需等待操作完成。
- 如果操作未完成(如数据未准备好),程序会立即返回错误码(如
EAGAIN或EWOULDBLOCK),允许程序继续执行其他任务。
2.2. 形象化比喻
- 阻塞:
想象你去餐厅点餐,服务员需要10分钟准备食物。你必须一直等在原地,直到食物上桌才能离开。- 程序行为:调用
read()读取文件时,如果没有数据,程序会卡住,直到数据到达。
- 程序行为:调用
- 非阻塞:
想象你去餐厅点餐,服务员告诉你“食物需要10分钟,你可以先去逛街,做好后我们会通知你”。你立即离开,去干其他事,等服务员通知你后再回来取餐。- 程序行为:调用
read()时,如果没有数据,程序立即返回,并继续执行其他任务(如轮询或事件驱动)。
- 程序行为:调用
2.3 代码示例说明
1 |
|
特点:如果 data.txt 中没有数据,fread() 会无限等待,程序卡住。适用于数据一定存在的场景(如读取本地文件)。
1 |
|
特点:如果 data.txt 中没有数据,read() 会立即返回 EAGAIN 或 EWOULDBLOCK,程序不会卡住。通过轮询或事件驱动(如 select() / epoll)处理后续逻辑,适合高并发场景。
2.4 同步、异步、阻塞、非阻塞四个概念
2.4.1 四种组合模式
- 同步阻塞 (Synchronous Blocking)
想象你去餐厅吃饭,点完菜后必须坐在餐桌前等待,直到服务员把菜端上来,期间不能做其他事情。这就是同步阻塞模式,你需要主动等待结果,并且在等待过程中不能做其他事情。
- 同步非阻塞 (Synchronous Non-blocking)
还是在餐厅,你点完菜后可以在餐厅里自由活动(比如去洗手间、和朋友聊天),但需要时不时回到餐桌前询问服务员菜是否做好了。这就是同步非阻塞模式,你不需要一直等待,但需要主动轮询结果。
- 异步阻塞 (Asynchronous Blocking)
这种模式比较少见,因为异步通常与非阻塞结合使用。想象你去餐厅吃饭,点完菜后坐在餐桌前等待,但服务员会在菜做好后主动通知你。虽然你可以做其他事情,但你选择坐在那里等待,这就是异步阻塞模式。
- 异步非阻塞 (Asynchronous Non-blocking)
最理想的模式。你点完菜后可以离开餐厅去逛街,服务员会在菜做好后打电话通知你。这就是异步非阻塞模式,你不需要等待,也不需要主动询问,系统会在任务完成时通知你。
| 模式 | 解释 |
|---|---|
| 同步阻塞 | 这是最严格的模式,必须顺序执行,且阻塞过程不能干其他事情,必须等上面的任务完成。 |
| 同步非阻塞 | 这种模式也是顺序执行,但是阻塞过程可以干其他的事情,但是必须主动去询问上面的任务是否完成。 |
| 异步阻塞 | 这种模式比较少 |
| 异步非阻塞 | 这种模式最合理也最常见,异步/并发执行,发起任务后可立即返回,也无需主动发起询问任务是否完成,系统会在任务完成时通知你。 |
2.4.2 程序示例
(一)同步非阻塞(Synchronous Non-blocking)
场景:模拟一个“等待文件数据”的任务,但调用方不会被阻塞,而是需要主动轮询检查数据是否准备好。
1 |
|
运行流程
- 主线程启动一个子线程模拟读取文件(耗时3秒)。
- 主线程进入轮询状态,每隔1秒检查
data_ready是否为1。主线程必须不断检查状态(data_ready是否为1),这会消耗一定的CPU资源(如频繁调用printf或检查变量)。 - 子线程3秒后设置
data_ready = 1。 - 主线程检测到数据就绪后继续执行。
关键点
- 同步:主线程必须等待子线程完成任务(
data_ready == 1)才能继续。 - 非阻塞:主线程不会被挂起,而是主动轮询检查状态(
while (!data_ready))。 - 缺点:轮询浪费CPU资源(频繁检查
data_ready)。
(二)异步非阻塞(Asynchronous Non-blocking)
场景:同样模拟“等待文件数据”,但调用方完全不阻塞,而是通过回调函数处理结果。
1 |
|
运行流程
- 主线程启动子线程模拟读取文件(耗时3秒),并传递一个回调函数
on_data_ready。 - 主线程 立即返回,继续执行其他任务(打印
主线程正在做其他事情...)。 - 子线程3秒后完成任务,调用回调函数
on_data_ready输出结果。 - 主线程和子线程并行执行,主线程无需等待。
关键点
- 异步:主线程无需等待任务完成,任务完成后通过回调通知主线程。
- 非阻塞:主线程完全不阻塞,可以继续执行其他任务。
- 优点:无需轮询,资源利用率高。
2.4.3 图解对比
1 | // 同步非阻塞: |
2.5 挂起、阻塞、等待三个概念
在编程中,挂起(Suspend)、阻塞(Block)和等待(Wait)这三个术语经常用于描述任务或线程的状态,它们都与任务不能继续执行有关,但发生的原因和机制有所不同。
2.5.1 挂起(Suspend)
挂起通常指的是任务被外部因素(如操作系统、用户请求)强行暂停执行,并且任务的状态被保存起来,以便后续恢复执行。挂起通常与调度有关,可能因为系统资源不足、用户干预或者调试等原因,属于被动原因。
- 特点:任务被挂起后,它不再参与调度,不占用CPU,直到被恢复(Resume)。挂起操作可以由外部控制。
- 形象化理解:就像你在看视频时按下了暂停键,视频画面停止在当前帧,当你再次按下播放键时,视频会从暂停的地方继续播放。
2.5.2 阻塞(Block)
阻塞指的是任务因为等待某个资源(如I/O操作完成、锁的释放、信号量等)而主动让出CPU,进入等待状态。当资源可用时,任务会被唤醒继续执行。
- 特点:任务主动放弃CPU,进入阻塞状态,直到等待的条件满足。阻塞通常与同步机制(如互斥锁、条件变量)或I/O操作相关。
- 形象化理解:你在等公交车,但车还没来,于是你坐在车站的长椅上休息(阻塞),直到公交车来了(条件满足),你才站起来上车(继续执行)。
2.5.3 等待(Wait)
等待是一个更广义的概念,它通常指任务因为某些条件不满足而暂停执行。等待可以是阻塞的一种表现形式,也可以包括其他形式的暂停(如忙等待)。在具体上下文中,等待往往与特定的同步原语(如条件变量)一起使用。
- 特点:等待通常意味着任务在某个条件上停留,直到条件满足。它可以是阻塞的(让出CPU)也可以是非阻塞的(忙等待,即循环检查条件)。
- 形象化理解:你在餐厅等位,你可以选择坐在椅子上休息(阻塞等待)或者不断去问前台还有多久(忙等待)。
2.5.4 联系与区别
- 联系:三者都表示任务暂时不能执行,但挂起和阻塞通常都是被动或主动地让出CPU,而等待则是一个更通用的术语。
- 区别:
- 挂起通常是由外部发起的,任务自身无法控制,而阻塞和等待往往是任务主动的行为(等待某个条件)。
- 阻塞是等待的一种方式,即让出CPU的等待(非忙等),而等待也可以包括忙等待(不释放CPU,循环检查条件)。
- 挂起后任务的状态被保存,且不参与调度;阻塞的任务则处于等待队列中,当条件满足时会被唤醒;等待则可能处于阻塞状态,也可能处于运行状态(忙等待)。
3 并发、并行
3.1 核心定义与本质区别
- 1. 并发(Concurrency)
- 定义:指多个任务在同一时间段内交替执行,但同一时刻只有一个任务在运行。
- 本质:通过 CPU 时间片轮转(或事件驱动),让用户感觉多个任务同时进行,但实际是单核 CPU “伪同时” 处理。
- 类比场景:
像一个厨师同时处理切菜、炒菜、煮汤三件事:切菜时暂停炒菜,煮汤时暂停切菜,通过快速切换完成所有任务。虽然看起来同时在做,但同一时间只做一件事。
- 2. 并行(Parallelism)
- 定义:多个任务在同一时刻真正同时执行,依赖多核 CPU 或多台计算机同时处理。
- 本质:利用硬件资源(如多核处理器)将任务分解为多个子任务,同时并行计算。
- 类比场景:
多个厨师分工合作,一人切菜、一人炒菜、一人煮汤,各自独立工作,同一时间内不同任务同步进行。
3.2 关键差异对比表
| 对比维度 | 并发(Concurrency) | 并行(Parallelism) |
|---|---|---|
| 执行方式 | 任务交替执行(伪同时) | 任务同时执行(真同时) |
| 硬件依赖 | 单核 CPU 即可(通过时间片切换) | 需要多核 CPU 或多处理器(硬件支持) |
| 解决问题 | 处理任务间的协作、资源竞争等复杂逻辑 | 加速计算密集型任务(如大数据处理) |
| 典型场景 | 服务器处理多客户端请求、GUI 界面响应 | 科学计算、3D 渲染、区块链挖矿 |
| 编程模型 | 多线程、异步回调、事件循环 | 多进程、MPI(消息传递接口)、GPU 并行计算 |
3.3 程序示例
- 并发编程:多线程实现,通过
pthread_create创建线程,多个线程共享 CPU 时间片。
1 |
|
- 并行编程:多核并行,使用 OpenMP 指令让编译器自动将循环分配到多个核心。
1 |
|
4 回调函数callback
4.1 回调函数的基本概念
回调函数是一种函数指针的应用,回调函数作为参数传递给另一个函数(称为 “调用者”),允许调用者在特定事件或条件发生时执行这个函数。简单来说,回调函数是由他人调用的函数,而非直接在代码中调用。
(函数指针,啥语言都一样,只不过各自有各自叫法,本质就是函数指针,指向函数的指针,真因为有函数指针,你才可以将函数作为变量传递给另一个函数)
4.2 回调函数与普通函数的区别
| 特性 | 普通函数 | 回调函数 |
|---|---|---|
| 调用方式 | 由程序员显式调用(如 func()) |
作为参数传递给其他函数,由其他函数调用 |
| 控制权 | 由程序员决定何时调用 | 由调用者(函数)决定何时调用 |
| 灵活性 | 固定逻辑,无法动态改变调用的逻辑 | 动态传递逻辑,灵活性更高 |
| 应用场景 | 普通流程控制(如计算、判断) | 异步操作、事件驱动、解耦逻辑 |
4.3. 为什么要使用回调函数
- (1)解耦与灵活性
- 解耦:调用者不需要知道回调函数的具体实现细节,只需知道它的接口(参数和返回值)。
例如:硬件驱动模块不需要知道用户如何处理传感器数据,只需调用回调函数。 - 灵活性:可以在运行时动态切换不同的回调函数,适应不同需求。
例如:一个排序算法可以接受不同的比较函数作为回调,实现升序或降序排序。
- 解耦:调用者不需要知道回调函数的具体实现细节,只需知道它的接口(参数和返回值)。
- (2)异步处理
- 在嵌入式系统中,许多操作是异步的(如定时器、中断、通信协议)。
回调函数可以在这些异步事件发生时通知主程序,避免阻塞主流程。
- 在嵌入式系统中,许多操作是异步的(如定时器、中断、通信协议)。
- (3)事件驱动
- 回调函数常用于事件驱动的场景(如按键按下、传感器触发)。
例如:当用户按下按钮时,系统会调用预先注册的回调函数处理按键事件。
- 回调函数常用于事件驱动的场景(如按键按下、传感器触发)。
4.4 回调函数的常见使用场景
- (1)硬件中断处理
- 在嵌入式系统中,当硬件(如按键、定时器)触发中断时,会调用回调函数处理中断事件。
例如:定时器每隔1秒触发一次,调用回调函数更新系统时间。
- 在嵌入式系统中,当硬件(如按键、定时器)触发中断时,会调用回调函数处理中断事件。
- (2)通信协议
- 在串口通信或网络通信中,数据接收完成时会调用回调函数处理数据。
例如:当串口接收到一帧数据后,调用用户定义的回调函数解析数据。
- 在串口通信或网络通信中,数据接收完成时会调用回调函数处理数据。
- (3)状态机或事件驱动框架
- 在事件驱动的框架中,回调函数用于处理不同的状态或事件。
例如:GUI系统中,用户点击按钮时调用回调函数执行对应操作。
- 在事件驱动的框架中,回调函数用于处理不同的状态或事件。
- (4)通用库的设计
- 通用库(如排序、过滤算法)通过回调函数允许用户自定义逻辑。
例如:C标准库的qsort函数通过回调函数实现自定义排序规则。
- 通用库(如排序、过滤算法)通过回调函数允许用户自定义逻辑。
4.5 举例嵌入式场景:模拟定时器回调
1 |
|
5 几个常用关键字
5.1 volatile关键字
(一)volatile的主要作用
- 防止编译器优化
编译器在进行优化时,可能会假设某些变量的值在代码块中不会被修改(除非程序显式更改)。volatile 的作用就是打破这种假设,确保每次访问变量时都直接从内存中读取,避免因为优化导致程序行为异常。
- 应用于特殊场景
- 硬件寄存器(Memory-Mapped I/O):硬件设备的状态寄存器可能被外部硬件修改,程序必须每次读取最新值。
- 中断服务程序(ISR)中修改的变量:主程序需要感知到中断中对变量的修改。
- 多线程环境中的共享变量:其他线程可能修改了变量的值。
- 信号处理函数中修改的变量:主程序需要根据信号处理函数的修改做出响应。
- 调试变量:某些调试信息可能由外部调试器修改。
(二)使用 volatile 的示例
示例 :模拟中断服务修改变量
1 |
|
说明:
flag被声明为volatile,表示其值可能在主程序之外被修改(如中断线程)。- 如果未使用
volatile,编译器可能会将flag缓存到寄存器中,主循环可能无法检测到flag的变化,导致程序陷入死循环。 - 由于是线程间通信,更推荐使用线程同步机制(如互斥锁、条件变量),但
volatile是基础保障。
(三)不使用 volatile 的后果
如果不使用 volatile,编译器可能会做出以下优化行为,从而引发程序错误:
| 编译器优化行为 | 后果 |
|---|---|
| 将变量值缓存到寄存器 | 无法检测到变量在程序之外的修改 |
| 删除重复读取操作 | 导致循环无法退出,或逻辑错误 |
| 重新排序指令 | 导致硬件行为异常或数据不一致 |
1 | int flag = 0; |
如果 flag 在中断中被设置为 1,但由于未使用 volatile,编译器可能将循环优化为:
1 | mov r0, #0 |
即,寄存器值始终为 0,导致循环永不退出。
简要解释原因:
(四)何时需要使用 volatile
| 场景 | 是否需要 volatile |
|---|---|
| 硬件寄存器 | ✅ 必须使用 |
| 中断服务程序中修改的变量 | ✅ 必须使用 |
| 多线程共享变量 | ✅ 推荐使用,但更应使用线程同步机制 |
| 信号处理函数中修改的变量 | ✅ 必须使用 |
| 单线程中未被修改的局部变量 | ❌ 不需要 |
(五)小结
volatile的核心作用是禁止编译器对变量进行优化,确保每次访问都从内存中读取。- 它适用于外部因素可能修改变量值的场景,如硬件寄存器、中断、多线程和信号处理。
- 在多线程或并发编程中,
volatile是基础保障,但不能替代线程同步机制(如互斥锁、原子操作)。 - 不使用
volatile时,编译器可能优化掉变量访问,导致程序行为与预期不符,甚至陷入死循环。
6 内存单位解析
6.1 基本定义
- KB(Kilobyte)
- 含义:KB 是 千字节(KiloByte),表示存储容量的单位。
- 换算:1 KB = 1024 字节(B)(基于二进制,即 2¹⁰)。
- 用途:常用于描述文件大小、内存分配、存储容量等。
- 示例:一个文本文件大小为 5 KB,表示占用 5 × 1024 = 5120 字节的空间。
- kb(kilobit)
- 含义:kb 是 千比特(kiloBit),表示数据传输速率的单位。
- 换算:1 kb = 1000 比特(bit)(基于十进制,即 10³)。
- 用途:常见于网络带宽描述(如宽带速率、下载速度)。
- 示例:网络速度标注为 100 Mbps(兆比特每秒),即 100,000 kbps。
6.2 大小写区别
B vs b
B(Byte):字节,是计算机存储的基本单位,1 字节 = 8 位(bit)。
b(bit):位,是二进制数据的最小单位(0 或 1)。
关键区别:
- 1 Byte = 8 bit,即 1 B = 8 b。
KB 与 Kb 的关系:
1 KB = 8 Kb(因为 1 字节 = 8 位)。
1 kB = 8 kb(如果 kB 表示 1000 字节,kb 表示 1000 位)。
K vs k
- K(大写):通常表示 1024(基于二进制,即 2¹⁰),用于存储容量(如 KB、MB、GB)。
- k(小写):通常表示 1000(基于十进制,即 10³),用于数据传输速率(如 kb、MBps)。








