一. 进程和线程
一.进程(Process)
进程是操作系统中资源分配和调度的基本单位。它是正在运行的程序的实例,具有独立的地址空间和资源集合。每一个进程都是一个具有一定功能的程序在某个数据集合上的一次动态执行过程。
1. 进程的特征:
- 动态性:进程是程序的执行过程,具有生命周期(创建、执行、终止)。
- 并发性:多个进程可并发执行,提高系统资源利用率。
- 独立性:每个进程拥有自己独立的内存空间,互不影响。
- 异步性:由于资源竞争和调度机制,不同进程的执行顺序和执行速度不确定。
2. 进程的组成
一个典型的进程包括以下几个部分:
- 程序代码(Code segment):用于描述进程的逻辑功能;
- 数据段(Data segment):存储变量、常量等数据;
- 堆区与栈区:用于动态内存分配和函数调用管理;
- 进程控制块(PCB):用于存储进程的状态信息,是操作系统管理进程的关键数据结构。
3. 进程的上下文
进程上下文是操作系统保存和恢复进程状态、资源和执行环境的全套信息,是“多任务操作系统”存在的技术基础之一。
3.1 进程上下文用途
1. 进程切换
当 CPU 从一个进程切换到另一个时,系统要:
1. 保存当前进程的上下文;
2. 恢复目标进程的上下文;
3. 切换虚拟地址空间(更昂贵);
4. 可能涉及缓存失效、TLB flush 等操作。
注意:进程切换开销通常大于线程切换,因为涉及更多内存空间、权限、资源隔离的重建。
2. 进程挂起与恢复
挂起进程时,保存完整上下文;
恢复时,重新加载上下文,一切如旧。
3. 系统调用/中断处理
内核在处理进程的系统调用或中断时,也依赖进程上下文确保系统能“断点恢复”。
3.2 进程上下文包括的内容
进程上下文比线程上下文复杂得多,因为进程不仅要记录执行状态,还要管理资源和环境。以下是常见的组成:
类别 | 描述 | 示例内容 |
---|---|---|
CPU状态 | 和线程类似的寄存器、程序计数器、堆栈指针等 | PC、寄存器组 |
内存映射信息 | 虚拟地址空间的结构与映射 | 页表、段表、内存区域描述 |
内核状态信息 | 操作系统管理进程的元数据 | PID、状态(运行、就绪、阻塞等) |
文件资源 | 进程打开的文件、IO设备等 | 文件描述符表、目录句柄 |
信号处理信息 | 信号处理函数、屏蔽字等 | 信号队列、回调指针 |
调度信息 | 优先级、时间片、CPU亲和性等 | 调度策略字段 |
用户空间信息 | 程序代码、数据段、堆、栈等 | ELF结构、malloc堆区等 |
线程组信息 | 多线程进程的主线程、线程列表 | 线程间共享的资源结构 |
这些信息通常封装在 Linux 中的 task_struct 或 Windows 中的 EPROCESS 结构体里。
4. 进程间的通信
进程间通信(IPC)是指不同进程之间交换数据或同步状态的机制。由于每个进程有独立的地址空间,它们不能像线程那样直接共享内存,因此必须借助操作系统提供的通信机制。
常见的进程间通信方式:
通信方式 | 是否共享内存 | 是否阻塞(默认/可配置) | 是否支持同步 | 是否支持跨主机 | 典型适用场景 |
---|---|---|---|---|---|
无名管道(Pipe) | ❌ | ✅ 阻塞(默认),可设非阻塞 | ❌(需额外机制) | ❌ | 父子进程之间简单数据传输 |
命名管道(FIFO) | ❌ | ✅ 阻塞(默认),可设非阻塞 | ❌(需额外机制) | ❌ | 任意本地进程间通信(单向、低频通信) |
消息队列(Message Queue) | ❌ | ✅ 阻塞(默认),支持非阻塞参数 | ✅ | ❌ | 多生产者-多消费者模型,事件驱动系统 |
共享内存(Shared Memory) | ✅ | ❌ 非阻塞(直接读写) | ❌(需同步机制) | ❌ | 高速数据交换、大块数据共享 |
信号量(Semaphore) | ❌ | ✅ 阻塞(默认),支持非阻塞版本 | ✅ | ❌ | 多进程资源同步与互斥控制 |
信号(Signal) | ❌ | ❌ 非阻塞(异步通知) | ✅(仅事件) | ❌ | 异步事件通知、进程终止、控制类通信 |
套接字(Socket) | ❌ | ✅ 阻塞(默认),可设非阻塞 | ✅ | ✅ | 跨主机通信、本地多进程服务、网络应用 |
内存映射文件(mmap) | ✅ | ❌ 非阻塞(直接映射) | ❌(需同步机制) | ❌ | 文件共享、数据持久化缓存、内核与用户空间共享 |
高层IPC机制(如 D-Bus、Binder、Windows Message) | 封装形式 | ✅(一般默认阻塞) | ✅(系统封装) | 部分支持 | 桌面服务通信、Android 系统通信、GUI事件系统 |
二.线程(Thread)
线程是程序执行中的最小单位,是CPU调度和执行的基本单位。一个进程可以包含一个或多个线程,多个线程共享该进程的资源(如内存空间、文件描述符等)。
1. 线程的特征
- 轻量级:线程创建、撤销和切换的开销远小于进程。
- 共享资源:同一进程内的线程共享代码段、数据段等资源,但拥有各自的栈和寄存器上下文。
- 并发性强:多个线程可并发执行,提高程序的响应性与处理能力。
- 依赖性:线程不能独立存在,必须依附于进程。
2. 线程的类型
1.用户级线程(User-Level Thread, ULT):线程管理由用户空间的线程库完成,切换速度快,但调度不由内核控制。
2.内核级线程(Kernel-Level Thread, KLT):线程管理由操作系统内核完成,支持真正的并行,但上下文切换开销较大。
3. 线程的上下文
线程上下文是指线程在任意时刻运行所必须依赖的状态信息集合,
3.1 线程上下文的用途
1. 线程上下文切换
当操作系统从一个线程切换到另一个线程时,需要:
保存当前线程的上下文(压栈)
加载新线程的上下文(出栈)
这是操作系统调度线程的核心机制之一,但它是有性能开销的(尤其是在频繁切换的高并发场景中)。
2. 线程挂起与恢复
挂起线程时操作系统必须记录其上下文;当恢复线程时重新加载上下文信息,让线程“从断点继续”。
3. 调试与异常处理
调试器和操作系统也会借助线程上下文来还原线程执行路径和错误状态。
3.2 线程上下文包括的内容
线程的上下文信息依赖于操作系统和硬件平台,但一般包括以下几大类:
类别 | 描述 | 示例内容 |
---|---|---|
程序计数器(PC) | 表示线程当前执行的代码位置 | 指令地址 |
寄存器状态 | 包含CPU中所有通用寄存器的值 | 如:EAX、EBX、ESP、EBP等(x86架构) |
栈指针与栈内容 | 每个线程有独立的栈,用于函数调用、局部变量 | 栈顶地址、栈帧内容等 |
线程专属状态 | 操作系统为线程维护的信息 | 线程ID、状态(就绪/运行/阻塞)、优先级 |
调度信息 | 系统调度线程时用到的额外信息 | CPU亲和性、时间片等 |
线程局部存储(TLS) | 线程的私有数据存储区域 | 比如线程特有的缓存、数据块等 |
3.3 引起线程上下文切换的场景
1. 主动让出 CPU 的情况
原因 | 说明 | 示例 |
---|---|---|
线程调用 sleep() / yield() / join() 等 | 当前线程明确告知操作系统暂时不需要执行 | Thread.sleep(1000) 、pthread_yield() |
等待同步资源 | 线程试图访问已被其他线程占用的互斥锁、读写锁、信号量等资源时,会被阻塞 | 多线程竞争 mutex |
等待 I/O | 线程发起阻塞型 I/O 调用,如读取磁盘、网络等,会进入阻塞状态 | read() 、数据库访问、文件下载等 |
2.被动抢占(系统调度引发的切换)
原因 | 说明 | 示例 |
---|---|---|
时间片耗尽 | 系统采用时间片轮转(Round-Robin)或抢占式调度,线程运行时间达到上限后被强制挂起 | 操作系统定时器中断触发调度 |
有更高优先级线程就绪 | 新线程或其他高优先级线程进入就绪队列,调度器立即切换过去执行它 | 实时线程唤醒、中断服务程序 |
系统负载均衡 | 多核系统中,调度器可能为了平衡负载将线程迁移到其他核心,触发切换 | CFS 调度器的负载平衡策略(Linux) |
3.用户/内核态切换引发的间接线程切换
情况 | 说明 |
---|---|
系统调用阻塞 | 比如调用 read() 没有数据时线程被挂起 |
中断处理程序唤醒其他线程 | 比如硬盘数据到达,唤醒等 I/O 的线程 |
从用户态切换到内核态时发现调度机会 | 比如一次系统调用结束后,调度器发现另一个线程更合适运行 |
4.信号或异常处理
情况 | 说明 |
---|---|
接收到信号 | 某线程接收到中断或异常信号,挂起自身或唤醒其他线程 |
出现错误/崩溃 | 如线程崩溃、断言失败或被其他线程取消,系统转向调度其他线程 |
5.内核调度策略驱动
情况 | 说明 |
---|---|
调度器周期性评估任务状态 | 操作系统定期触发调度器判断是否要切换线程(如 Linux 的 CFS) |
任务亲和性调整或 NUMA 优化 | 内核为提升性能可能迁移线程到合适的 CPU 或内存节点 |
4.线程间通信
线程间通信是指:同一进程内的多个线程为了协同完成任务,在共享内存的基础上通过某些机制传递数据、同步状态或协调行为的过程。
和进程不同,线程天然就可以共享内存空间,因此线程间通信的关键不在于“如何传数据”,而在于如何正确同步,防止竞争条件、死锁等问题。
常见的线程间通信方式:
通信方式 | 是否共享内存 | 是否支持同步 | 特点与说明 |
---|---|---|---|
共享变量(全局/堆变量) | ✅ | ❌(需手动) | 最简单、最常见,但需要同步机制保护 |
锁机制(互斥锁 Mutex) | ✅ | ✅ | 控制对共享资源的排他访问 |
读写锁(RWLock) | ✅ | ✅ | 多读单写场景更高效 |
条件变量(Condition) | ✅ | ✅ | 用于线程间等待/通知协调 |
信号量(Semaphore) | ✅ | ✅ | 计数型信号量可实现资源池控制 |
事件(Event) | ✅ | ✅ | 适用于线程同步触发机制 |
队列(Queue) | ✅ | ✅(内置锁) | 用于线程安全地传递任务或数据 |
消息通道(Channel) | ✅ | ✅ | 类似于队列,更偏向 CSP 模式 |
内存屏障 / 原子操作 | ✅ | ✅(低层次) | 提供最低级别的同步控制,适合性能敏感场景 |
三. 进程和线程的区别
维度 | 进程(Process) | 线程(Thread) |
---|---|---|
基本定义 | 程序执行的实例,是资源分配的基本单位 | 进程中的执行单元,是CPU调度的基本单位 |
地址空间 | 拥有独立的虚拟地址空间 | 同一进程内的线程共享地址空间(代码段、堆、全局变量) |
资源拥有者 | 是资源的拥有者(如内存、文件句柄、设备) | 不拥有资源,只使用所属进程的资源 |
创建与销毁开销 | 创建、切换和销毁成本较高 | 创建、切换和销毁成本低(轻量级) |
通信方式 | 需通过进程间通信(IPC)机制,如管道、消息队列、共享内存等 | 可直接读写共享内存,通信更高效 |
调度单位 | 由操作系统作为独立调度单位进行管理 | 是进程中的可调度单位,通常由操作系统或线程库调度 |
稳定性和隔离性 | 崩溃通常不会影响其他进程,安全性高 | 崩溃可能导致整个进程出错,安全性低 |
并发性支持 | 支持并发,通过多进程模型实现 | 更高效的并发实现方式,适用于高并发场景 |
栈和寄存器 | 各进程有自己的栈空间和寄存器 | 各线程有独立的栈和寄存器,但共享代码和堆内存 |
系统开销 | 大(包括内存空间切换、上下文切换、缓存失效等) | 小(无需切换地址空间,缓存影响小) |
适用场景 | 稳定性要求高、资源隔离强的场景(如数据库、Web服务中的独立子进程) | 高并发、需要频繁切换和共享数据的场景(如Web服务器的线程池、图像处理、多任务游戏) |
四.go语言协程
Go 语言的协程(Goroutine )是一种轻量级的、由 Go 运行时管理的并发单元。它比线程更加高效,特别是在需要大量并发处理的场景下,Goroutine 的优势更加明显。通过通道(channel),Go 也提供了安全且高效的协程间通信机制,这使得并发编程变得更加简单和可靠。
1. Goroutine 的特点:
-
简单易用:相比线程模型,Goroutine 提供了一种简单的并发编程模型,开发者不需要直接处理锁、信号量等复杂的并发控制结构。
-
轻量级:Goroutine 比传统的线程更加轻量,一个 Go 程序可以轻松启动数万个协程而不会有明显的性能损耗。
-
由 Go 运行时调度:Goroutine 是用户态的,Go 语言有自己的调度器(M:N 调度模型),运行时会动态调整 Goroutine 与 OS 线程的映射关系,可以在多个操作系统线程上调度执行数万个协程。Go 运行时会自动处理协程的调度、执行和阻塞。
-
栈内存动态扩展:Goroutine 具有非常低的内存占用和创建开销,每个 Goroutine 的初始栈非常小(典型为几 KB),可以根据需要动态扩展和收缩。这与操作系统线程(通常栈大小为 MB 级别)相比,能够更高效地利用内存。
2. 创建 Goroutine :
只需要在函数前加上 go 关键字,就能将该函数以 Goroutine 的方式执行:
package main
import (
"fmt"
"time"
)
func hello() {
fmt.Println("Hello, Goroutine!")
}
func main() {
// 启动一个新的 Goroutine
go hello()
// 等待 Goroutine 运行完成
time.Sleep(time.Second)
}
go hello() 会启动一个新的 Goroutine 执行 hello 函数,main 函数不会等待它执行完成,所以需要 time.Sleep(time.Second) 让主协程稍微等待一会儿,否则 main 退出后,整个程序就结束了。
3. 配合通道(Channel)通信
Goroutine 之间通过 channel(通道) 进行通信,而不是共享内存,以避免数据竞争。
package main
import (
"fmt"
)
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 将 sum 发送到通道 c
}
func main() {
s := []int{1, 2, 3, 4, 5}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从通道 c 接收数据
fmt.Println(x, y, x+y)
}
在这个例子中,两个 goroutine 并发地计算数组的一部分和,然后通过通道将结果传递回主协程进行汇总。
c <- sum 发送数据,x, y := <-c, <-c 读取数据,实现 Goroutine 之间的同步和通信。
4. Goroutine 与线程的区别:
特性 | Goroutine | 线程(Thread) |
---|---|---|
管理者 | Go 运行时(用户态调度) | 操作系统(内核态调度) |
创建开销 | 非常小,数千个 goroutine 轻松支持 | 较大,创建销毁开销较高 |
栈大小 | 初始几 KB,动态扩展 | 固定大小(通常几 MB) |
切换开销 | 轻量级,用户态切换,开销小 | 重量级,内核态切换,开销大 |
数量限制 | 数万个以上 | 受限于系统资源,通常几百个 |
内存共享 | 共享同一进程内存空间 | 共享同一进程内存空间 |
调度方式 | M:N 模型,Go 的调度器可以将多个 goroutine 映射到少量的 OS 线程上 | 由操作系统线程调度器直接管理 |
阻塞行为 | 阻塞时自动让出线程,调度器切换其他 goroutine | 阻塞时线程被阻塞,可能导致性能下降 |
通信方式 | 通过 Channel 进行安全通信 | 通常通过锁、信号量、条件变量等同步工具 |
使用场景 | 大规模并发,网络服务器,异步处理 | 多线程应用,系统级别任务,多核并行 |
编程复杂度 | 简单,Go 语言内置支持 | 复杂,需要处理同步、锁等问题 |
五.多线程任务
1. 任务类型
1.1 CPU 密集型任务
CPU 密集型任务是指在运行过程中主要消耗 中央处理器(CPU)计算资源的任务。这类任务在运行过程中进行大量的数学计算、逻辑判断或数据处理,CPU 是其性能的关键瓶颈,因此线程数量应该与 可用的物理核心数相匹配或略少,避免过度线程导致频繁的上下文切换。
在开发时,对于 CPU 密集型任务,使用线程数设计公式为:
线程数 ≈ CPU核心数 或 CPU核心数 + 1
1.2 I/O 密集型任务
I/O 密集型任务是指在运行过程中主要依赖 输入/输出操作(如磁盘读写、网络通信、数据库访问等)的任务。其瓶颈不在
CPU,而在于 I/O 设备的响应速度,因此 CPU 并不总是满负载。我们可以适当 增加线程数来提高系统吞吐量。
在开发时,对于 IO 密集型任务,使用线程数设计公式为:
线程数 ≈ CPU核心数 × (1 + 平均I/O等待时间 / 平均CPU计算时间)
可以简化:
线程数 ≈ 2 × CPU核心数,或甚至 5~10 倍,视具体场景而定
1.3 两者区别
类型 | 资源依赖 | 典型特征 | 优化手段 |
---|---|---|---|
CPU 密集型任务 | CPU | 运算密集,少 I/O | 并行计算、多核优化、减少切换 |
I/O 密集型任务 | I/O 设备 | 等待频繁,CPU闲置 | 异步编程、缓冲优化、并发设计 |
2. python同一个脚本跑多个线程, 和分成多个脚本同时跑, 哪个执行的更快
在 Python 中,同一个脚本跑多个线程和分成多个脚本同时跑,它们的执行效率会受到多个因素的影响,主要包括以下几点:
2.1 Python 的 GIL(Global Interpreter Lock):
GIL 是 CPython(Python 最常用的解释器)内部的一个互斥锁,它确保任何时候只有一个线程在执行 Python 字节码。设计 GIL 的初衷是为了让 CPython 的内存管理变得简单且安全(因为对象引用计数不是线程安全的)。
由于 GIL 的存在,Python 多线程在 CPU 密集型任务(如复杂计算)中无法实现真正的并行,导致性能受限。
如果任务是 I/O 密集型(如文件读写、网络请求),线程可能不会受到 GIL 的显著影响,因为 I/O 操作会释放 GIL,允许其他线程运行。
2.2 多个脚本的并行性:
如果分成多个脚本独立运行,每个脚本都会运行在一个独立的 Python 解释器实例中,因此每个脚本有自己的 GIL。这样,它们不会彼此竞争 GIL,因此 CPU 密集型任务的执行速度会更快。
这种方法有效地利用了多核 CPU,因为多个独立的进程可以同时在不同的 CPU 核心上运行,真正实现并行计算。
2.3 资源开销:
运行多个脚本时,每个脚本都有自己的解释器实例和内存空间,可能会占用更多的系统资源(如内存)。
相比之下,单个脚本中的多线程共享相同的内存空间,开销更小,但受 GIL 影响较大。
2.4 总结:
CPU 密集型任务:多个独立的脚本运行通常会更快,因为每个脚本可以充分利用多核 CPU。
I/O 密集型任务:同一个脚本中的多线程可能和多个脚本同时跑的速度相近,因为 GIL 对 I/O 操作影响较小。
Python 运行 CPU 密集型任务时,建议用 multiprocessing 模块启动多个进程,以规避 GIL 的限制。
3. java的多线程能够有效利用多核cpu执行cpu密集型任务吗?
Java 的多线程可以真正实现多核并行执行 CPU 密集型任务,而 Python 的多线程受 GIL 限制,无法真正并行执行。
Java 的线程是操作系统级别的线程(通常是 POSIX thread 或 Windows thread),多个线程可以同时运行在不同的 CPU 核心上,真正实现并行。
- 没有全局解释器锁(GIL),线程间可以同时执行字节码或原生指令。
- Java 虚拟机(JVM)支持多线程并发执行,并具备先进的线程调度、线程池管理和 JIT 编译优化,执行效率高。
- 对于大量 CPU 运算(如图像处理、加密、模拟计算等),Java 的多线程可以显著提高处理速度,前提是机器具备多核处理器。
java 多线程和python 多线程的区别:
特性 | Java 多线程 | Python 多线程(CPython) |
---|---|---|
是否有 GIL 限制 | ❌ 没有,全线程可并行 | ✅ 有 GIL 限制,线程不能并行执行 |
线程调度 | 由操作系统原生支持,真实并行 | 单线程执行 Python 字节码(GIL) |
CPU 利用率 | 能同时跑满多个核心 | 只能用到一个核心(即使启动多线程) |
适合 CPU 密集型任务 | ✅ 非常适合 | ❌ 不适合,多进程才有效 |
开发复杂度 | 略高,需要注意锁和同步 | 类似,但性能不佳 |