协程 —— 底层实现原理

一、相关概念

1. 进程
进程是一个实体,一个用户程序就是一个进程。
2. 线程
  • 每个线程都有各自的调用栈,寄存器环境,线程本地存储。
  • 线程是进程的执行体,拥有一个执行入口,以及从进程虚拟地址空间分配的栈信息,包括用户栈和内核栈。
3. 子程序
    子程序又被称为执行体、函数、方法等。子程序在主程序中被调用执行。

二、协程

1. 协程的产生
  • 如果线程各自创建几个执行体,给他们各自指定执行入口,申请一些内存分配给他们做执行栈,那么线程就可以按需调度这几个执行体了。
  • 为了实现这几个执行体的切换,线程也需要记录执行体的信息,包括ID、栈的位置、执行入口地址、执行现场等。
  • 线程可以选择一个执行体来执行,此时CPU中指令指针就会指向这个执行体的执行入口,栈基和栈指针寄存器也会指向线程给他分配的执行栈。
  • 要切换执行体时,需要先保存当前执行体的执行现场,然后切换到另一个执行体,通过同样的方式可以恢复到之前的执行体,这样就可以从上次执行中断的地方继续执行。
  • 这些由线程创建的执行体就叫做“协程”,因为用户程序不能操作内核空间,所以只能给协程分配用户栈,而操作系统对协程一无所知,所以协程又被称为“用户态线程”。
2. 协程的定义
  • 协程(Coroutine),是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。

  • 协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

     引用Donald Knuth的一句话总结协程的特点:“子程序就是协程的一种特例。”
     
     注意:在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。
    

子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

比如子程序A、B:

def A():
    print '1'
    print '2'
    print '3'

def B():
    print 'x'
    print 'y'
    print 'z'

假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:

1
2
x
y
3
z

但是在A中是没有调用B的,所以协程的调用比函数调用理解起来要难一些。

看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

来看例子:

传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。

如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:

import time

def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        time.sleep(1)
        r = '200 OK'

def produce(c):
    c.next()
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()

if __name__=='__main__':
    c = consumer()
    produce(c)

执行结果:

[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK

注意到consumer函数是一个generator(生成器),把一个consumer传入produce后:

首先调用c.next()启动生成器;

然后,一旦生产了东西,通过c.send(n)切换到consumer执行;

consumer通过yield拿到消息,处理,又通过yield把结果传回;

produce拿到consumer处理的结果,继续生产下一条消息;

produce决定不生产了,通过c.close()关闭consumer,整个过程结束。

整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

三、线程同步与异步

1. 线程同步

A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求不到,只能阻塞等待。(一般在线程需要修改共享的系统资源时)

2. 线程异步

A线程要请求某个资源,但是此资源正在被B线程使用中,因为没有同步机制存在,A线程仍然请求的到,A线程无需等待。(一般在线程只读的共享系统资源时)

3. 同步与异步的区别
同步异步
Sockfd 管理单个线程管理,方便多个线程共同管理
代码逻辑程序整体逻辑清晰子模块逻辑清晰
程序性能响应时间长,性能差响应时间短,性能好

总结起来就是:

    同步:编程简单,性能差
    异步:编程复杂,性能高

同步编程简单是因为其数据的处理是在同一个过程中进行处理的。但是异步的处理可能在不同的线程中进行处理,例如在异步的情况下客户端发送2次请求,这2个请求可能在服务端的2个线程中进行处理,2个线程共用一个客户端的数据,可能造成数据混乱,最常见的解决方法就是进行加锁。

四、设计协程的核心

  • 通过上面的介绍,我们知道了同步与异步之间的差异,有没有一种方式,有异步性能,同步的代码逻辑。来方便编程人员对 IO 操作的组件呢? 有,采用一种轻量级的协程来实现。在每次 send 或者 recv 之前进行切换,再由调度器来处理 epoll_wait 的流程

  • 而协程的设计核心目标就是:为了拥有同步的简单编程方式,同时又想要拥有异步响应时间短的性能。

五、实现协程的核心:跳转(协程切换)

  • 无论协程怎么被创建,底层都要分配执行栈和控制信息。

  • 让出执行权时候,都要保存执行现场,以便后续恢复。

  • 每个协程都有自己的执行栈,可以保存自己的执行现场。

  • 可以由用户程序按需创建协程。

  • 协程“主动让出”执行权时候,会保存执行现场,然后切换到其他协程。

  • 协程恢复执行时候会根据之前保存的执行现场恢复到中断前的状态,继续执行,这样就通过协程实现了既轻量又灵活的由用户态调度的多任务模型。

总结:

        因为协程由用户程序在用户栈分配存储空间,同一线程中的多个协程间的切换只在用户态下,而不涉及核心态,因此这样的协程不存在用户态与核心态的转换,提高了CPU效率。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Whitemeen太白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值