对进程,线程,协程的认识

本文深入探讨了进程、线程与协程的概念,解释了它们在计算机系统中的角色与区别。进程作为资源分配的基本单位,线程作为轻量级进程提升并发效率,而协程则提供更细粒度的控制。文章还讨论了多线程编程中的资源共享与竞争问题,以及GIL锁在Python中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

进程

进程是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
在当代面向线程设计的计算机结构中,进程是线程的容器。
程序是指令、数据及其组织形式的描述,进程是程序的实体。
进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动。通过程序计数器的值和处理寄存器的内容来表示。

两点概念

1、进程是一个实体,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
2、进程是一个“执行中的程序”,只有操作系统执行程序,程序才能成为一个实体,我们称之为进程。

线程和多线程

也被称为轻量级进程,是程序执行流的最小单元。
线程是进程的一个实体,是被系统独立调度和分派的基本单位。线程自己不拥有系统资源,但它可与同属于一个进程的其他线程共享进程所拥有的全部资源。
在单个程序中同时运行多个线程完成不同的工作,称为多线程

GIL锁

也称为全局解释器锁。
线程的特点是数据资源是共享的,而多个线程又要共享Cpython(python解释器)解释权限,共享意味着竞争,然后竞争的话数据就会不安全,并且要保证同一时刻只有一个线程执行代码,每个线程在执行过程中都要先获取GIL。

下面是线程中的生产者消费者模型

from queue import Queue
import time
import threading

#生产者消费者模式是线程间通信的一种应用
#在使用数据结构的时候确定是否是线程安全,Queue本身是线程安全的
#list([]) Dict({}) 都不是线程安全的
def set_value(q):
    index = 0
    while True:
        q.put(index)
        index += 1
        q.put(index)
        index += 1
        time.sleep(2)

def get_value(q):
    while True:
        print("消费者获取数据",q.get()) #如果队列为空,get()方法会sleep,直到队列有数据

def main():
    q = Queue(8)
    t1 = threading.Thread(target=set_value,args=[q])
    t2 = threading.Thread(target=get_value,args=[q])

    t1.start()
    t2.start()

if __name__ == '__main__':
    main()

协程

协程是一种用户态的轻量级线程,协程的调度完全由用户控制。

下面是协程的生产者消费者模型

from queue import Queue
import time
import threading

#生产者消费者模式是线程间通信的一种应用
#在使用数据结构的时候确定是否是线程安全,Queue本身是线程安全的
#list([]) Dict({}) 都不是线程安全的
def set_value(q):
    index = 0
    while True:
        q.put(index)
        index += 1
        q.put(index)
        index += 1
        time.sleep(2)

def get_value(q):
    while True:
        print("消费者获取数据",q.get()) #如果队列为空,get()方法会sleep,直到队列有数据

def main():
    q = Queue(8)
    t1 = threading.Thread(target=set_value,args=[q])
    t2 = threading.Thread(target=get_value,args=[q])

    t1.start()
    t2.start()

if __name__ == '__main__':
    main()

三者区别

1、先有进程,然后进程可以创建线程,线程是依附在进程里面的,线程里面可以包含多个协程。
2、进程之间不共享全局变量,线程之间共享全局变量,但是要注意资源竞争问题。两者都不共享局部变量。
3、多进程开发比单进程,多线程开发稳定性要强,但是多进程开发比多线程开发资源开销要大。
4、多线程开发,线程之间执行是无序的,协程之间按照一定顺序交替执行。
5、协程主要用在网络爬虫和网络请求,开辟一个协程大概需要5k空间,开辟一个线程需要512k空间,开辟进程占用资源最多。
6、一个线程可以有多个协程,一个进程也可以单独拥有多个协程,这样python中则能够使用多核CPU。

<think>嗯,用户这次想深入理解线程协程的对应关系及操作系统实现机制。从用户之前的提问我的回答来看,用户已经掌握了线程协程的基本概念,现在需要更底层的实现原理分析。 用户可能正在学习操作系统或并发编程,需要理解这两种并发单元在系统层面的关联。从引用材料看,用户特别关注三点:1)线程协程的绑定关系 2)操作系统对两者的感知差异 3)切换机制的本质区别。 需要重点解释M:N线程模型(引用[3][4]提到的关键概念),这是理解对应关系的核心。操作系统只认识内核线程,而协程是用户态实体,必须通过调度器映射到线程。引用[5]提到协程切换只需保存寄存器上下文,这点要结合CPU指令详细说明。 用户似乎对性能比较很敏感,上次回答的切换耗时数据可以复用,但这次要补充机制原因。比如为什么Go的GMP模型里,同一个线程上的协程切换只需修改三个寄存器,而跨线程迁移就需要调度器介入。 考虑到用户连续追问技术细节,回答需要包含:1)内存管理差异(线程栈固定vs协程栈动态)2)调度触发机制(抢占式vs协作式)3)CPU特权级的影响。最后用Go的runtimeLinux的pthread实现作为实例会更直观。 要避免过度深入内核源码,用寄存器操作内存布局图就能说明白关键点。用户提到的“指定一块区间作为协程栈”基本正确,但需要补充动态扩容的细节(引用[3]提到2KB初始栈)。</think>以下是线程协程的对应关系及操作系统实现机制的详细分析: --- ### 一、线程协程的对应关系 | **特性** | **线程** | **协程** | **对应关系** | |------------------|--------------------------------------|--------------------------------------|-----------------------------------------------------------------------------| | **调度主体** | 操作系统内核 | 用户态运行时(如Go调度器) | 一个线程可承载多个协程(1:N),协程依赖线程执行[^3][^4] | | **资源归属** | 拥有独立内核栈、寄存器上下文 | 共享线程的内核资源,自有用户态栈 | 协程栈在**线程栈空间内分配**(动态区间),共享线程的文件描述符等资源[^2][^5] | | **执行单元数量** | 一个进程可含多个线程 | 一个线程可含多个协程 | 进程 $\supset$ 线程 $\supset$ 协程,构成**三级执行单元结构**[^3][^4] | | **阻塞影响范围** | 线程阻塞导致所属进程所有线程暂停 | 协程阻塞仅让出当前线程,其他协程仍运行 | 协程通过**非对称绑定**规避线程级阻塞(如Go的GMP模型)[^4][^5] | > **关键结论**: > 协程是**寄生在用户态线程上的轻量级执行单元**,其生命周期由用户程序控制,操作系统仅感知底层线程[^4][^5]。 > 对应关系公式: > $$ \text{协程} \subseteq \text{线程} \subseteq \text{进程} $$ --- ### 二、操作系统实现机制对比 #### 1. **线程的内核实现(OS感知)** - **调度机制**: 内核通过**任务状态段(TSS)** 保存线程上下文(寄存器+栈指针),切换时通过**系统调用**(如`swapcontext`)完成以下操作[^2][^5]: ```c // 伪代码:线程切换核心步骤 save_callee_registers(); // 保存寄存器 save_stack_pointer(); // 保存栈指针 load_new_thread_context(); // 加载新线程上下文 flush_tlb(); // 刷新TLB ``` - **资源隔离**: 每个线程拥有独立的内核栈(默认8MB)虚拟地址空间,切换时需**刷新CPU缓存TLB**[^5]。 #### 2. **协程的用户态实现(OS不可见)** - **调度机制**: 由运行时(如Go的`runtime`)维护协程队列,切换时**仅修改3个寄存器**(无需系统调用)[^3][^5]: ```asm ; x86汇编示例:协程切换 mov [current_routine.sp], sp ; 保存当前栈指针 mov sp, [next_routine.sp] ; 加载新协程栈指针 jmp next_routine.pc ; 跳转至新协程程序计数器 ``` - **栈管理**: 动态分配小栈(初始2KB),通过**连续栈(Contiguous Stack)** 机制自动扩容(复制旧栈到新内存区)[^3]。 --- ### 三、关键机制图解 #### 1. 线程协程的栈空间关系 ```plaintext ┌───────────────────┐ 线程栈(内核分配,固定大小) │ │ │ ┌─────────────┐ │ ◄── 线程栈基址 │ │ 协程A栈 │ │ │ ├─────────────┤ │ │ │ 协程B栈 │ │ ◄── 协程栈在线程栈内动态分配 │ ├─────────────┤ │ │ │ ... │ │ │ └─────────────┘ │ │ │ └───────────────────┘ ◄── 线程栈顶 ``` #### 2. 协程切换流程(以Go为例) ```go func coroutineA() { // 步骤1:协程A运行 runtime.Gosched() // 主动让出(保存PC/SP/BP到调度器) // 步骤3:恢复执行 } func coroutineB() { // 步骤2:调度器切换到协程B } ``` **寄存器操作序列**: $$ \text{保存} \\{ \text{PC}_A, \text{SP}_A, \text{BP}_A \\} \rightarrow \text{加载} \\{ \text{PC}_B, \text{SP}_B, \text{BP}_B \\} $$ --- ### 四、性能关键点 1. **切换成本** - 线程切换:$$ \text{耗时} \approx 1\mu s \text{ (系统调用+上下文复制)} $$ - 协程切换:$$ \text{耗时} \approx 0.1\mu s \text{ (寄存器操作)} $$[^5] 2. **阻塞处理差异** - 线程阻塞(如I/O)→ 触发内核调度 → 整个线程挂起 - 协程阻塞 → 运行时挂起该协程 → **线程继续运行其他就绪协程**[^4] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值