浅学线程

一:什么是线程

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

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

(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 等): 一种让异步代码写起来像同步代码的语法糖,底层仍然是基于回调或事件循环,但极大地提高了代码的可读性。
相关推荐
egoist20233 小时前
[linux仓库]线程与进程的较量:资源划分与内核实现的全景解析[线程·贰]
linux·开发语言·线程·进程·资源划分
半梦半醒*4 小时前
ELK2——logstash
linux·运维·elk·elasticsearch·centos·1024程序员节
java_logo4 小时前
Docker 部署 CentOS 全流程指南
linux·运维·人工智能·docker·容器·centos
半梦半醒*4 小时前
ELK3——kibana
linux·运维·elasticsearch·centos·gitlab
额呃呃5 小时前
对信号的理解
linux·运维·算法
weixin_307779135 小时前
Linux 下 Docker 与 ClickHouse 的安装配置及 MySQL 数据同步指南
linux·数据库·mysql·clickhouse·运维开发
qq_297075675 小时前
vmware和kali linux安装和搭建
linux·安全测试
zhaotiannuo_19985 小时前
【Linux kali 更换yum源】
linux·运维·服务器
会灭火的程序员5 小时前
银河麒麟V10 SP3 升级GCC环境
linux·c++·supermap