3.4 KiB
3.4 KiB
“线程阻塞”(Thread Blocking)是多线程编程中一个核心的运行状态。简单来说,它是指线程因为等待某个事件或资源,暂时放弃 CPU 使用权,进入“不响应”状态的过程。
1. 什么是线程阻塞?
在操作系统的状态机中,线程通常经历 运行 (Running) -> 阻塞 (Blocked/Waiting) -> 就绪 (Ready) 的循环。
- 直观比喻:就像你在银行排队取钱。如果柜台告诉你“系统断网了,请稍等”,你不能继续取钱(运行),也不能离开队伍(退出),只能在原地坐下等网络恢复(阻塞)。
- 内核视角:内核将该线程从 “就绪队列”(Ready Queue)中移除,放入 “等待队列”(Wait Queue)。此时 CPU 不再给它分配时间片。
2. 阻塞是如何发生的?(典型场景)
阻塞通常由以下几种情况触发:
A. I/O 阻塞 (最常见)
当线程请求磁盘文件、网络数据或键盘输入时,CPU 的速度远高于硬件响应速度。
- 过程:线程发起
read()系统调用 -> 内核发现数据没准备好 -> 将线程设为阻塞态 -> 硬件完成读取后发出中断信号 -> 内核将线程唤醒。
B. 锁竞争 (Synchronization)
当线程尝试获取一个已经被其他线程占用的互斥锁(Mutex)时。
- 过程:线程执行
lock()-> 发现锁被占用 -> 线程挂起(放入该锁的等待队列) -> 占有锁的线程执行unlock()-> 内核挑选一个阻塞线程恢复运行。
C. 休眠与等待 (Sleep/Wait)
线程主动要求“停一会儿”。
- 过程:调用
sleep()或wait()。内核设置一个定时器或监听特定信号,到期后再将其唤醒。
3. 阻塞过程的底层原理
在 Linux(内核级线程)中,阻塞的进行涉及上下文切换 (Context Switch):
- 陷入内核:线程通过系统调用(如
read)进入内核态。 - 状态变更:内核将该线程对应的
task_struct(任务描述符)状态从TASK_RUNNING改为TASK_INTERRUPTIBLE(可中断阻塞)或TASK_UNINTERRUPTIBLE(不可中断阻塞)。 - 保存现场:内核将当前线程的寄存器、程序计数器(PC)、栈指针等信息保存到该线程的内核栈中。
- 重新调度:调度器(Scheduler)从就绪队列中挑选下一个线程,把它的上下文加载到 CPU 寄存器中开始执行。
4. 阻塞 vs. 非阻塞 (Non-blocking)
这是开发者经常优化的方向:
- 阻塞式:线程死等,代码写起来简单(顺序执行),但浪费线程资源。
- 非阻塞式:线程调用后立即返回。如果数据没准备好,返回一个错误码(如
EAGAIN)。线程可以先去做别的事,过会儿再来轮询(Polling)。 - 多路复用 (epoll):这是你作为开发者可能会接触到的高级货——一个线程同时监控多个可能阻塞的 I/O,谁好了就处理谁。
区分“阻塞”与“挂起”
- 阻塞 (Block):通常是被动的,等待资源,内存不释放。
- 挂起 (Suspend):通常是主动的(或系统压力大),整个线程可能被交换到磁盘(Swap)上以腾出内存空间。
你想看看在 C 语言或者 Rust 中,如何通过具体的代码调用来观察线程从运行态进入阻塞态的过程吗?