协程

 一、一些基本概念:

协程(Coroutine),又称微线程,纤程,一种用户级的轻量级线程。

栈(Stack)是一个数据集合,可以理解为只能在一端进行插入或删除操作的列表。

上下文context: 上下文简单说来就是一个环境,相对于进程而言,就是进程执行时的环境。具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存信息等。
    一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。
    用户级上下文: 正文、数据、用户堆栈以及共享存储区;
    寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
    系统级上下文: 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈。
    当发生进程调度时,进行进程切换就是上下文切换(context switch).操作系统必须对上面提到的全部信息进行切换,新调度的进程才能运行。而系统调用进行的模式切换(mode switch)。模式切换与进程切换比较起来,容易很多,而且节省时间,因为模式切换最主要的任务只是切换进程寄存器上下文的切换。

寄存器上下文则有程序寄存器PC,处理机状态寄存器PS,栈指针和通用寄存器的值组成,其中PC给出了CPU将要执行的下一条指令的虚地址;PS给出了机器与该进程相关联的硬件状态;栈指针指向下一项的当前地址,而通用寄存器则用于不同执行模式间的参数传递。

协程拥有自己的寄存器上下文和栈,协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态

协程的优缺点:

优点:

  1. 无需线程上下文切换的开销

  2. 无需原子操作锁定及同步的开销(更改一个变量)

  3. 方便切换控制流,简化编程模型

  4. 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

缺点:

    1. 无法利用多核资源:协程的本质是个单线程,它不能多核,协程需要和进程配合才能运行在多CPU上,当然我们日常所编写的绝大部分应用都没有这个必要,除非是CPU密集型应用。

    2. 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

 

⽬前的协程框架⼀般都是设计成 1:N 模式。 所谓 1:N 就是⼀个线程作为⼀个容器⾥⾯放置多个协程。 那么谁来适时的切换这些协程? 答案是有协程⾃⼰主动让出CPU, 也就是每个协程池⾥⾯有⼀个调度器, 这个调度器是被动调度的。 意思就是他不会主动调度。 ⽽且当⼀个协程发现⾃⼰执⾏不下去了(如异步等待⽹络的数据回来, 但是当前还没有数据到), 这个时候就可以由这个协程通知调度器, 这个时候执⾏到调度器的代码, 调度器根据事先设计好的调度算法找到当前最需要CPU的协程。 切换这个协程的CPU上下⽂把CPU的运⾏权交个这个协程, 直到这个协程出现执⾏不下去需要等等的情况, 或者它调⽤主动让出CPUAPI之类, 触发下⼀次调度。

那么这个实现有没有问题?

其实是有问题的, 假设这个线程中有⼀个协程是CPU密集型的他没有IO作, 也就是⾃⼰不会主动触发调度器调度的过程, 那么就会出现其他协程得不到执⾏的情况, 所以这种情况下需要程序员⾃⼰避免。 这是⼀个问题, 假设业务开发的⼈员并不懂这个原理的话就可能会出现问题。

协程的好处IO密集型的程序中由于IO操作远远慢于CPU的操作, 所以往往需要CPUIO操作。 同步IO下系统需要切换线程, 让操作系统可以在IO过程中执⾏其他的东⻄。 这样虽然代码是符合⼈类的思维习惯但是由于⼤量的线程切换带来了⼤量的性能的浪费, 尤其是IO密集型的程序。所以⼈们发明了异步IO。 就是当数据到达的时候触发我的回调。 来减少线程切换带来性能损失。 但是这样的坏处也是很⼤的, 主要的坏处就是操作被分⽚” 了, 代码写的不是 ⼀⽓呵成” 这种。 ⽽是每次来段数据就要判断 数据够不够处理哇, 够处理就处理吧, 不够处理就在等等吧。 这样代码的可读性很低, 其实也不符合⼈类的习惯。但是协程可以很好解决这个问题。 ⽐如 把⼀个IO操作 写成⼀个协程。 当触发IO操作的时候就⾃动让出CPU给其他协程。 要知道协程的切换很轻的。 协程通过这种对异步IO的封装 既保留了性能也保证了代码的容易编写和可读性。在⾼IO密集型的程序下很好。 但是⾼CPU密集型的程序下没啥好处。 

# -*- coding: utf-8 -*-
# 2017/11/26 14:19
import time
def A():
    while True:
        print("----A---")
        yield
        time.sleep(0.5)

def B(c):
    while True:
        print("----B---")
        next(c)
        time.sleep(0.5)

if __name__=='__main__':
    a = A()
    B(a)
    
# output
# ----B---
# ----A---
# ----B---
# ----A---
# ----B---
#.........

gevent
greenlet已经实现了协程, 但是这个还的⼈⼯切换, 是不是觉得太麻烦了, 不要捉急, python还有⼀个⽐greenlet更强⼤的并且能够⾃动切换任务的模块 gevent
其原理是当⼀个greenlet遇到IO(指的是input output 输⼊输出, ⽐如⽹络、 ⽂件操作等)操作时, ⽐如访问⽹络, 就⾃动切换到其他的greenlet, 等到IO操作完成, 再在适当的时候切换回来继续执⾏。由于IO操作⾮常耗时, 经常使程序处于等待状态, 有了gevent为我们⾃动切换协程, 就保证总有greenlet在运⾏, 而不是等待IO

切换执行

# -*- coding: utf-8 -*-
# 2017/11/26 14:27
import gevent
def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
#⽤来模拟⼀个耗时操作, 注意不是time模块中的sleep
gevent.sleep(1)
g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

"""
<Greenlet at 0x28ed48acb90: f(5)> 0
<Greenlet at 0x28ed48acb90: f(5)> 1
<Greenlet at 0x28ed48acb90: f(5)> 2
<Greenlet at 0x28ed48acb90: f(5)> 3
<Greenlet at 0x28ed48acb90: f(5)> 4
<Greenlet at 0x28ed48acc28: f(5)> 0
<Greenlet at 0x28ed48acc28: f(5)> 1
<Greenlet at 0x28ed48acc28: f(5)> 2
<Greenlet at 0x28ed48acc28: f(5)> 3
<Greenlet at 0x28ed48acc28: f(5)> 4
<Greenlet at 0x28ed48accc0: f(5)> 0
<Greenlet at 0x28ed48accc0: f(5)> 1
<Greenlet at 0x28ed48accc0: f(5)> 2
<Greenlet at 0x28ed48accc0: f(5)> 3
<Greenlet at 0x28ed48accc0: f(5)> 4
"""
View Code

并发 下载器

# -*- coding: utf-8 -*-
# 2017/11/26 14:29
from gevent import monkey
import gevent
import urllib.request
#有IO才做时需要这⼀句
monkey.patch_all()
def myDownLoad(url):
    print('GET: %s' % url)
    resp = urllib.request.urlopen(url)
    data = resp.read()
    print('%d bytes received from %s.' % (len(data), url))

gevent.joinall([
    gevent.spawn(myDownLoad, 'http://www.baidu.com/'),
    gevent.spawn(myDownLoad, 'http://www.itcast.cn/'),
    gevent.spawn(myDownLoad, 'http://www.itheima.com/'),
    ])


"""
GET: http://www.baidu.com/
GET: http://www.itcast.cn/
GET: http://www.itheima.com/
112106 bytes received from http://www.baidu.com/.
183887 bytes received from http://www.itheima.com/.
216716 bytes received from http://www.itcast.cn/.
"""

上能够看到是先发送的获取baidu的相关信息, 然后依次是itcast
itheima, 但是收到数据的先后顺序不⼀定与发送顺序相同, 这也就体现出了
异步, 即不确定什么时候会收到数据, 顺序不⼀定

 

posted @ 2017-11-25 11:13  ninxin18  阅读(165)  评论(0编辑  收藏  举报