浅学线程

一:什么是线程

线程: 进程中的一条执行流, 每个进程一开始(启动之后)都会有一个"主线程"

前提: 一个 进程 中 可以有多个线程

(1) 线程直接共享进程的所有资源(如:mm_struct), 创建线程比创建进程要快10~100倍

(2) 线程之间共享相同的地址空间(mm_struct), 这样利于线程之间数据高效的传输

(3) 多CPU操作系统中, 多个线程可以真正的并行执行

比如:下图是进程A中有三个线程 比如:下图是日常分析问题的Log日志, 从Log中我们也能看得出进程中有很多线程 从两个角度来重新认识进程:

二:Linux操作系统中线程的创建

Linux操作系统中依然使用 "task_struct" 数据结构来维护一个线程 下图演示了Linux操作系统中 线程 的创建过程

fork 与 clone 的区别

三:用户级线程 vs 内核级线程

TCB: Thread Control Block(线程控制块)

PCB: Process Control Block(进程控制块)

线程的实现(创建)方式主要有两种,分别是:"用户级线程"、 "内核级线程"

用户级线程 内核级线程
在用户态实现 在内核态实现
用户态直接创建并维护 用户态发起创建--->透过--->系统调用--->内核态真正新建线程
------------------------------------ POSIX Threads (pthreads) 库、 JAVA Thread库

用户级线程:在用户空间实现的线程,操作系统内核看不到的线程(线程的TCB存放在用户空间的,操作系统内核它只能看到进程_PCB, 操作系统只能以进程为单位来调度) ---------》用户级线程是由一些应用程序中的线程库()来实现,应用程序可以调用"线程库"的API来完成线程的创建、线程的结束、等操作

内核级线程:在内核空间实现的线程,由操作系统管理的线程---------》内核级线程管理的所有工作都是由操作系统内核完成的, 比如内核线程的创建,结束,是否占用CPU等。都是由操作系统内核来管理

用户级线程 特点 & 缺点:

内核级线程 特点 & 缺点:

内核级线程的创建,终止、切换都是由内核来完成的,所以应用程序如果想用内核级线程的话,需要通过系统调用来完成内核级线程的创建、终止、切换。这里会涉及到用户态和内核态的转换,因此相对于前面用户级线程,"系统开销大"

如今各类操作系统中线程模型

多对1 模型

1对1 模型

多对多 模型

四:Linux内核程序是怎样创建线程的

内核启动的时候,初始化init_mm, 它全局只有一个,管理内核程序虚拟地址空间 进程 task_struct 中的 active_mm 内核线程的创建,使用 kernal_thread()

五:线程的状态

(A): 线程的生命周期

每种操作系统,每种编程语言,都有自己的线程生命周期控制,大部分满足上面的线程生命周期,但也都各有不同。

a.1 在Java中,线程状态是明确定义在 java.lang.Thread.State 枚举中的,共有以下 6种 状态:

状态名称 说明
NEW 线程被创建,但尚未启动(即未调用 start()方法)
RUNNABLE 线程正在Java虚拟机中运行,或正在等待操作系统资源(如CPU时间片)
BLOCKED 线程被阻塞,等待获取一个监视器锁(如进入 synchronized块)
WAITING 线程无限期等待另一个线程执行特定操作(如 Object.wait()、join())
TIMED_WAITING 线程在指定时间内等待另一个线程执行操作(如 sleep()、wait(timeout))
TERMINATED 线程已执行完毕或被中断,生命周期结束

a.2 C++ 中的线程状态(以 C++11 及以后为标准)

C++11 引入了 标准库,但 C++ 标准本身并不定义线程的内部状态枚举,线程状态由底层操作系统管理。不过,从使用角度可以归纳出以下 常见状态:

状态名称 说明
创建(Created) 线程对象已创建,但尚未调用 join()或 detach(),可能尚未启动
运行(Running) 线程正在执行其入口函数
阻塞(Blocked) 线程因等待互斥锁、条件变量、I/O 等而暂停
等待(Waiting) 线程因调用 std::this_thread::sleep_for、condition_variable::wait等而挂起
完成(Finished) 线程入口函数执行完毕,但资源可能尚未释放(取决于是否 join())
可合并/分离(Joinable / Detached) 线程是否可被 join(),或已调用 detach()成为后台线程

⚠️ 注意:C++ 标准库没有提供查询线程状态的接口(如 getState()),状态信息依赖于操作系统(如 Linux 的 /proc、Windows 的线程句柄状态)。


a.3 C 中的线程状态(以 POSIX threads - pthread 为例)

C 语言本身没有线程概念,线程支持依赖于平台库,最常用的是 POSIX threads(pthread)。pthread 也没有官方枚举状态,但可以从线程生命周期中归纳出以下 常见状态:

状态名称 说明
创建(Created) 线程已调用 pthread_create(),但尚未开始执行或尚未被调度
运行(Running) 线程正在执行其入口函数
阻塞(Blocked) 线程因等待互斥锁(pthread_mutex_lock)、条件变量(pthread_cond_wait)等而阻塞
等待(Waiting) 线程因调用 sleep()、pthread_cond_timedwait等而挂起
终止(Terminated) 线程入口函数返回或调用 pthread_exit(),但资源尚未释放(未 join)
已分离(Detached) 线程已调用 pthread_detach(),资源在线程结束时自动回收

⚠️ 同样,pthread 没有提供获取线程状态的函数(如 pthread_getstate()),状态信息需通过调试工具(如 gdb、top、htop、ps -L)查看。


总结对比表

语言/库 是否明确定义线程状态 状态数量 是否可查询状态 备注
Java ✅(Thread.State) 6种 ✅ getState() 标准明确,跨平台一致
C++ 11+ ❌(标准未定义) 约5~6种 ❌ 无标准接口 状态依赖于操作系统
C (pthread) ❌(库未定义) 约5~6种 ❌ 无标准接口 状态依赖于操作系统

如需进一步查看底层线程状态(如 Linux 的 task_state、Windows 的 THREAD_STATE),可结合系统工具或调试器分析。

a.4 Linux 操作系统内核中,线程和进程使用相同的状态描述

📊 Linux 线程(进程)状态一览

下面的表格汇总了 Linux 中主要的线程(进程)状态 "下面总结的不是全部状态,还有很多哈,AI了解一下"、 它们在 ps 或 top 命令中对应的显示字母以及简单的说明:

状态宏定义 在 ps/top 中的显示 说明
TASK_RUNNING R 线程正在运行或就绪(在运行队列中等待调度)。
TASK_INTERRUPTIBLE S 可中断的睡眠阻塞。线程在等待某个事件(如I/O完成、信号量),可被信号或中断唤醒。
TASK_UNINTERRUPTIBLE D 不可中断的睡眠阻塞。线程在等待某些特定条件(通常是I/O),不会响应信号(即使是 kill -9)。此状态通常很短暂。
TASK_STOPPED T 线程被暂停,例如通过 SIGSTOP信号。 可以通过 SIGCONT信号恢复运行。
TASK_TRACED T 线程正在被跟踪(例如由调试器 gdb 在断点处暂停)。与 TASK_STOPPED类似,但多了一层保护,不能通过 SIGCONT信号恢复。
EXIT_ZOMBIE Z 僵尸状态。线程已终止,但其退出状态信息还未被父进程获取(例如父进程未调用 wait())。此时线程占用的绝大多数资源已释放,仅保留 task_struct空壳。
EXIT_DEAD X 死亡状态。线程的最终状态,接下来会彻底被系统销毁回收。此状态非常短暂,通常无法通过 ps命令捕捉到。

🔍 如何查看线程状态

你可以使用以下命令来查看线程的状态:

css 复制代码
-   ps 命令:例如 ps aux 或 ps -eLF,在 STAT 或 S 列查看状态字母。
-   top 命令:运行 top 后,在 S 列查看状态字母。按下 H 键可以切换至线程视图。

💎 简单总结

理解这些状态对看BUG问题很有帮助。比如,偶尔看到 D 状态通常是正常的,但如果多个线程长时间处于 D 状态,可能意味着某些硬件(如磁盘)或驱动出现了问题。而大量的 Z 状态线程则会消耗系统进程号资源,是需要避免的"僵尸线程"。

(B): Linux操作系统中线程的状态变化

linux的线程状态存放在 该线程的 task_struct

注意:linux中的线程,没有就绪状态(在linux中,就绪状态和运行状态的线程都是 "TASK_RUNNING", 但是linux搞了一个专门用来指向当前运行任务的指针current 指向它,以表示它是一个正在运行的线程)

下图展示了部分状态转换过程:

(1) TASK_INTERRUPTIBLE 可中断的阻塞(睡眠)状态: 阻塞(睡眠)的时候,会响应其他信号(如: kill)

(2) TASK_UNINTERRUPTIBLE 不可中断的阻塞(睡眠)状态: 阻塞(睡眠)的时候,忽略其他信号

跑个题:了解一些 I/O 知识

I/O 操作的核心是"输入/输出",它的本质是 数据在 CPU/内存 与 外部设备之间 的流动。 磁盘读写只是其中最常见、最典型的一种,但远不是全部。

可以把 CPU 和内存想象成公司的"总部办公室",而所有其他设备都是"外部办事处或合作伙伴"。任何需要与这些"外部实体"进行的数据交换,都属于 I/O 操作。

以下是操作系统中主要的 I/O 操作类型:

1. 存储设备 I/O(你最熟悉的)

这是与持久化存储设备的数据交换。

  • 磁盘 I/O:读写硬盘(HDD)、固态硬盘(SSD)。例如:保存文件、加载程序、数据库查询。
  • 光盘 I/O:从 CD、DVD、蓝光光盘读取数据。
  • U盘/移动硬盘 I/O。

2. 网络 I/O

这是与网络设备(网卡)的数据交换,是网络编程和互联网应用的基石。

  • 发送数据:将数据包通过网卡发送到网络上。
  • 接收数据:从网卡读取到来的数据包。
  • 例子:浏览网页、发送邮件、在线视频、网络游戏。这通常不被用户直观地感知为"I/O",但它在系统资源占用和性能上极其重要。

3. 外设 I/O

这是与用户交互或专用功能设备的通信。

  • 键盘输入:你按下按键,对于 OS 来说就是一个输入操作。
  • 鼠标输入:移动鼠标、点击按键,也是一系列输入操作。
  • 显示器输出:GPU 将渲染好的帧数据输出到显示器。
  • 打印机输出:将文档数据发送给打印机。
  • 扫描仪输入:从扫描仪获取图像数据。
  • 音响/耳机输出:声卡将数字音频信号输出到音响设备。
  • 麦克风输入:声卡从麦克风录制音频信号。

4. 进程间通信(IPC - Inter-Process Communication)

这是在同一台机器上,不同进程之间交换数据。虽然数据可能没有离开主机,但它跨越了进程的"边界",所以也是一种 I/O。

  • 管道:一个进程的输出作为另一个进程的输入(如 shell 中的 | 操作符)。
  • 消息队列
  • 共享内存

一个重要的概念:为什么 I/O 很"慢"?

理解 I/O 的关键在于认识到它与 CPU 处理速度的巨大差异。

  • CPU 和 内存(纳秒级):处理速度极快,在纳秒级别。

  • I/O 设备(毫秒级甚至秒级):

    • 机械硬盘寻道需要毫秒级。
    • 网络请求可能需要几十到几百毫秒。
    • 等待用户键盘输入可能需要几秒甚至几分钟。

这个速度差距是几个数量级的! 因此,如何高效地管理 I/O,避免让高速的 CPU 白白等待低速的 I/O 设备,就成了操作系统设计中的一个核心问题。这就引出了以下几种 I/O 模型:

操作系统中的五种主要 I/O 模型

这些模型定义了应用程序如何与操作系统协作来完成一个 I/O 操作。

  1. 阻塞 I/O :

    • 应用发起 I/O 调用后,线程被挂起,一直等待数据准备好并从内核缓冲区拷贝到用户空间后,才继续执行。
    • 最简单,但性能最差,一个线程只能处理一个 I/O 流。
  2. 非阻塞 I/O :

    • 应用发起 I/O 调用后,立即返回一个错误码,不会阻塞线程。
    • 应用程序需要不断地轮询内核,询问数据是否准备好。这会消耗大量 CPU。
  3. I/O 多路复用 :

    • 这是 select、poll、epoll 等系统调用的核心思想。
    • 应用将一个或多个 I/O 请求(如多个网络连接)注册到一个"代理"上(如 epoll)。
    • 然后这个"代理"会阻塞,等待任何一个被注册的 I/O 准备就绪,然后通知应用程序哪些 I/O 可以读了/写了。
    • 这是构建高性能网络服务器的关键模型(如 Nginx、Redis),可以用单个线程管理成千上万的连接。
  4. 信号驱动 I/O :

    • 应用发起一个 I/O 请求,并注册一个信号处理函数。请求发出后,线程继续执行。
    • 当内核数据准备好时,会向应用发送一个 SIGIO 信号。
    • 应用在信号处理函数中进行实际的 I/O 操作。
  5. 异步 I/O :

    • 应用发起一个 I/O 请求后,立即返回,继续做其他事情。
    • 内核会完成整个 I/O 操作(包括等待数据和将数据拷贝到用户空间)。
    • 操作完成后,内核通过某种机制(如回调函数)通知应用。
    • 与信号驱动 I/O 的区别在于:AIO 是内核完成所有工作后通知;信号驱动是内核通知你"可以开始工作了",你自己还得去拷贝数据。

总结

I/O 类型 常见例子 特点
存储 I/O 读写硬盘文件 持久化存储,速度慢(相对于内存)
网络 I/O 网页请求、数据库远程连接 高延迟,不稳定,是现代后端开发的重点
外设 I/O 键盘、鼠标、显示器 与用户交互,实时性要求高
进程间 I/O 管道、共享内存 进程间数据交换,速度较快

所以,I/O 是一个极其广泛的概念,涵盖了计算机与外界(包括用户、网络、其他设备)的所有数据交换。 理解不同类型的 I/O 及其背后的模型,对于开发高效、健壮的软件至关重要。

(C): 导致线程阻塞(睡眠)的多种场景

I/O 操作 是导致线程阻塞最常见和典型的场景之一,无论是磁盘 I/O 还是网络 I/O。 除了 I/O,线程阻塞(或进入非运行状态)的场景还有很多。我们可以从操作系统调度和线程生命周期的角度来系统地理解它们。

本质上,当一个线程因为某些原因无法继续执行时,操作系统就会剥夺它的 CPU 时间片,并将其置于一种"等待"或"阻塞"状态,直到某个条件被满足。

以下是导致线程阻塞的主要场景,可以归为几大类:


1. 同步原语 和 锁

这是多线程编程中最常见的阻塞场景,目的是为了协调对共享资源的访问,防止数据竞争。

  • 获取锁失败

    • Synchronized(Java) / Mutex(C++): 当一个线程试图获取一个已经被其他线程持有的互斥锁时,它会被阻塞,直到锁被释放。
    • ReentrantLock : 与 synchronized 类似,但更灵活。调用 lock() 方法时如果锁不可用,线程会阻塞。
  • 等待条件成立

    • Object.wait() / Condition.await() : 线程在持有锁的情况下,主动调用这些方法会释放锁 并进入等待状态,直到其他线程调用 notify()/notifyAll()signal()/signalAll()。这是"等待-通知"机制的核心。
  • 计数器未就绪

    • CountDownLatch : 线程调用 await() 方法会被阻塞,直到计数器减到 0。
    • CyclicBarrier: 一组线程必须全部到达屏障点才会继续执行。先到达的线程会被阻塞,直到最后一个线程到达。
  • 信号量不足

    • Semaphore : 线程调用 acquire() 方法时,如果许可证数量不足,线程会被阻塞,直到有其他线程释放许可证。

2. I/O 操作

这是经典阻塞场景。

  • 网络 I/O : 当线程执行 Socket.read()Socket.write() 时,如果对端数据尚未到达、网络缓冲区已满等,线程会阻塞。
  • 磁盘 I/O: 当线程读写文件时,如果数据尚未从磁盘加载到内核缓冲区,线程会等待磁盘 IO 完成。虽然现代操作系统有大量的缓存和预读机制,但在高负载或大量随机读写的场景下,阻塞仍然明显。

3. 线程协作 与 生命周期管理

  • Thread.join() : 一个线程等待另一个线程执行完毕。线程 A 执行 threadB.join(),那么线程 A 会阻塞,直到线程 B 终止。

4. 超时 与 定时

  • Thread.sleep(millis) : 这是线程主动让出 CPU,进入计时等待状态。在指定的毫秒数内,线程不会参与 CPU 调度。
  • Object.wait(timeout) / Condition.await(timeout): 带有超时参数的等待,避免了无限期等待。

5. 用户输入

  • System.in.read(): 等待用户在控制台输入数据。在用户按下回车键之前,线程会一直阻塞。

总结与对比

为了更清晰地理解,我们可以将阻塞场景分为:

场景类别 具体例子 触发条件 唤醒条件
同步与锁 synchronized, ReentrantLock.lock() 请求的资源被占用 资源被释放
等待通知 Object.wait(), Condition.await() 主动等待某个条件 被其他线程通知
线程协作 Thread.join() 等待另一个线程结束 目标线程运行结束
I/O 操作 Socket.read(), File.read() 数据未就绪 数据就绪
主动暂停 Thread.sleep() 代码主动调用 指定的时间过去
用户交互 System.in.read() 等待用户输入 用户输入并回车

如何避免阻塞?------ 异步编程

在现代高并发操作系统中,为了避免大量线程阻塞耗尽系统资源,广泛采用了异步非阻塞的编程模型。

  • NIO (Non-blocking I/O) : 线程发起一个 I/O 操作后立即返回,不会阻塞。当数据就绪时,通过类似事件通知的机制(如 Selector)再来处理(比如: 在 Linux 中,NIO 的核心就是 I/O 多路复用 + 非阻塞 I/O ,而 epoll 是目前最高效的实现。它让单个线程能够处理成千上万的网络连接,这正是现代高并发服务器(如 Nginx、Redis)能够支撑海量连接的技术基础。)。
  • CompletableFuture (Java) / Promise (JavaScript): 代表一个异步计算的结果。你可以在其上附加回调函数,当计算完成时自动调用,而不需要阻塞等待。
  • Async/Await (C#, Python, JavaScript 等): 一种让异步代码写起来像同步代码的语法糖,底层仍然是基于回调或事件循环,但极大地提高了代码的可读性。
相关推荐
HLC++8 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室9 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
groundhappy12 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕13 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫14 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生14 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1115 小时前
linux基础IO
linux·运维·服务器
BelongPanda17 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯17 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_7779983418 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器