进程线程和协程

程序是静态的代码集合,进程是程序的运行实例,而线程是进程中的执行单元。

进程

进程,可执行程序运行中形成一个独立的内存体,这个内存体有自己独立的地址空间(Linux会给每个进程分配一个虚拟内存空间32位操作系统为4G, 64位为很多T),有自己的堆,上级挂靠单位是操作系统。操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。 进程

在 Linux 中,进程的状态有以下几种,通常在进程状态表中用单个字母表示:

  • 运行中 (Running) - R 进程正在运行或在运行队列中等待。

  • 可中断睡眠 (Interruptible Sleep) - S 进程正在等待某个事件完成,可以被信号中断

  • 不可中断睡眠 (Uninterruptible Sleep) - D 程正在等待无法中断的事件,如I/O操作

  • 停止 (Stopped) - T 进程被停止,可以通过信号控制(如 SIGSTOP)暂停。

  • 僵尸 (Zombie) - Z 进程已经终止,但尚未被父进程收集其退出状态。

  • 挂起 (Traced or Tracked) - t 进程被调试器跟踪

  • 等待中 (Idle) 有时也用I表示,特定内核版本中用于标识空闲进程。

ps命令中的进程状态

ps命令状态

  • 字符’<’表示 nice 值小于 0,nice 值最小为 - 20。因此字符’<’表示此进程可能在调度过程中获得优势。

  • 字符’N’表示 nice 值大于 0,nice 值最大为 19。因此字符’N’表示此进程可能在调度过程中不能获得优势。

  • 字符’L’表示进程 vm_lock 值为真,即此进程有内存页被锁在内存中,这些内存页不能通过换页换出。

  • 字符’s’表示进程的 tgid(pid) 值等于进程的 session(sid) 值,这说明当前进程是会话的 leader

  • 字符’l’表示进程中的线程数量大于 1,这说明当前进程是一个多线程程序

  • 字符’+’表示进程的 pgrp(pgid) 值等于进程的tpgi 前台进程组ID,这表示当前进程在前台进程组中。

进程状态转换:

从运行到阻塞:当进程请求I/O或等待某个条件时。 从阻塞到就绪:当等待的条件满足时。 从就绪到运行:当调度程序将CPU分配给进程时。 从运行到停止:当进程接收到停止信号。 从停止到就绪:当进程接收到继续信号。 从运行到僵尸:当进程执行完毕但父进程尚未收集其状态。

ps -e|wc -l && ps -A|wc -l && ls /proc|grep ^[0-9]|wc -l

线程

线程,有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)执行的最小单位。线程拥有独立的栈,但是共享进程全部资源。多个线程共同寄生在一个进程上,除了拥有各自的栈空间,其他的内存空间都是一起共享。由于这个特性,使得线程间的内存关联性很大,互相通信就很简单(堆区、全局区等数据都共享,需要加锁机制即可完成同步通信),但是同时也让线程之间生命体联系较大,比如一个线程出问题,导致进程出问题,也就导致了其他线程问题。 线程

pthreads 提供了一种跨平台的方式来创建和管理线程,主要在 Unix 和类 Unix 操作系统(如 Linux、macOS)上广泛使用。 pthreads 的核心概念:

  • 线程(Thread):线程是一个轻量级进程,它可以与其他线程共享同一进程的内存空间,但每个线程有自己独立的执行流。pthreads 定义了创建和管理这些线程的接口。

  • 互斥锁(Mutex):pthreads 提供了互斥锁来防止多个线程同时访问共享资源,确保只有一个线程能在某一时刻访问关键区。

  • 条件变量(Condition Variables):条件变量允许线程等待某个条件变为真时再继续执行,可以与互斥锁一起使用,来控制线程的执行顺序。

  • 线程属性(Thread Attributes):pthreads 提供了设置和获取线程属性的接口,如线程的栈大小、调度策略等。

执行单元

对于Linux来讲,不区分进程还是线程,他们都是一个单独的执行单位,CPU一视同仁,均分配时间片。 所以,一个进程想更大程度的与其他进程抢占CPU的资源,那么多开线程是一个好的办法。如上图,进程A没有开线程,那么默认就是1个线程,对于内核来讲,它只有1个执行单元,进程B开了3个线程,那么在内核中,该进程就占有3个执行单元。CPU的视野是只能看见内核的,它不知晓谁是进程谁是线程,时间片轮询平均调度分配,那么进程B拥有的3个单元就有了资源供给的优势。 执行单元

切换问题与协程

我们通过上述的描述,可以知道,线程越多,进程利用(或者)抢占的cpu资源就越高。 进程线程

那么是不是线程可以无限制的多呢?答案当然不是的,我们知道,当我们cpu在内核态切换一个执行单元的时候,会有一个时间成本和性能开销。 CPU成本

线程切换的成本主要体现在以下几个方面:

  • 上下文切换:涉及寄存器状态、内存管理等的保存和恢复。

  • 缓存失效:包括数据缓存和指令缓存的失效。

  • TLB 刷新:虚拟地址到物理地址的缓存失效。

  • 调度开销:调度算法的计算和管理开销。

  • 同步开销:涉及锁和同步机制的开销。

  • TLS 管理:线程局部存储的状态更新开销。 页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个cache就是TLB.当进程切换后页表也要进行切换,**页表切换后TLB就失效了,cache失效导致命中率降低,**那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢。 综上,我们不能够大量的开辟,因为线程执行流程越多,cpu在切换的时间成本越大。很多编程语言就想了办法,既然我们不能左右和优化cpu切换线程的开销,那么,我们能否让cpu内核态不切换执行单元, 而是在用户态切换执行流程呢?很显然,我们是没权限修改操作系统内核机制的,那么只能在用户态再来一个伪执行单元,那么就是协程了。 协程

协程的切换成本

协程切换比线程切换快主要有两点:

  • 协程切换完全在用户空间进行。线程切换涉及特权模式切换,需要在内核空间完成;

  • 协程切换相比线程切换做的事情更少,线程需要有内核和用户态的切换,系统调用过程 协程切换成本: 协程切换非常简单,就是把当前协程的CPU寄存器状态保存起来,然后将需要切换进来的协程的CPU寄存器状态加载到CPU寄存器上就ok了。而且完全在用户态进行,一般来说一次协程上下文切换最多就是几十ns这个量级。 线程切换成本: 系统内核调度的对象是线程,因为线程是调度的基本单元(进程是资源拥有的基本单元,进程的切换需要做的事情更多,这里占时不讨论进程切换),而线程的调度只有拥有最高权限的内核空间才可以完成,所以线程的切换涉及到用户空间和内核空间的切换,也就是特权模式切换,然后需要操作系统调度模块完成线程调度(task_struct)而且除了和协程相同基本的CPU上下文,还有线程私有的栈和寄存器等,说白了就是上下文比协程多一些,其实简单比较下task_strcut和任何一个协程库的coroutine的struct结构体大小就能明显区分出来。而且特权模式切换的开销确实不小,随便搜一组测试数据[3],随便算算都比协程切换开销大很多。 线程基本都是维持Mb的量级单位,一般是4~64Mb不等,多数维持约10M上下

协程的核心优势:协程通过在用户态进行高效的调度,使得线程的CPU时间片得到了充分利用,特别是在遇到I/O阻塞时,协程可以非阻塞地执行其他任务,提高了整体执行效率。协程在原有线程的基础上,通过非阻塞的方式进一步提高了线程在CPU上运行时间的效率。

适合协程的程序:主要是I/O密集型、高并发、事件驱动的应用场景。这些程序可以通过协程实现更高效的资源利用和并发处理,从而获得加速效果。