59 lines
3.4 KiB
Markdown
59 lines
3.4 KiB
Markdown
“线程阻塞”(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)**:
|
||
|
||
1. **陷入内核**:线程通过系统调用(如 `read`)进入内核态。
|
||
2. **状态变更**:内核将该线程对应的 `task_struct`(任务描述符)状态从 `TASK_RUNNING` 改为 `TASK_INTERRUPTIBLE`(可中断阻塞)或 `TASK_UNINTERRUPTIBLE`(不可中断阻塞)。
|
||
3. **保存现场**:内核将当前线程的寄存器、程序计数器(PC)、栈指针等信息保存到该线程的**内核栈**中。
|
||
4. **重新调度**:调度器(Scheduler)从就绪队列中挑选下一个线程,把它的上下文加载到 CPU 寄存器中开始执行。
|
||
|
||
|
||
|
||
---
|
||
|
||
## 4. 阻塞 vs. 非阻塞 (Non-blocking)
|
||
|
||
这是开发者经常优化的方向:
|
||
|
||
* **阻塞式**:线程死等,代码写起来简单(顺序执行),但浪费线程资源。
|
||
* **非阻塞式**:线程调用后立即返回。如果数据没准备好,返回一个错误码(如 `EAGAIN`)。线程可以先去做别的事,过会儿再来轮询(Polling)。
|
||
* **多路复用 (epoll)**:这是你作为开发者可能会接触到的高级货——一个线程同时监控多个可能阻塞的 I/O,谁好了就处理谁。
|
||
|
||
---
|
||
|
||
### 区分“阻塞”与“挂起”
|
||
* **阻塞 (Block)**:通常是被动的,等待资源,**内存不释放**。
|
||
* **挂起 (Suspend)**:通常是主动的(或系统压力大),**整个线程可能被交换到磁盘(Swap)上**以腾出内存空间。
|
||
|
||
你想看看在 C 语言或者 Rust 中,如何通过具体的代码调用来观察线程从运行态进入阻塞态的过程吗? |