python 协程

day13学python 协程+事件驱动

协程+事件驱动

协程 (微线程)--用处多,重点

   当调度切换时 靠寄存器上下文和栈保存 要使用时再调用(即可不会因io传输数据卡壳 从而耗时无法继续进行)实现并行

 

优缺点:

优点:

1 无需同线程上下文切换 消耗cpu

2 修改数据无需加锁(协程都是单线程串行 无需加锁)

3 cpu支持大量协程

缺点:

1无法调用多核资源(本身是单线程)  --指的是cpu只用单核运作 

 

============================================================================================================

 

接下来介绍两种协程切换的 模板与方法

1.greenlet协程切换(手动挡)

2.gevent协程切换(自动挡)

 

 

greenlet 模板(手动切换)

 
import greenlet
def test1():
    print(1)
    gr2.switch()
    print(3)
    gr2.switch()
def test2():
    print(2)
    gr1.switch()
    print(4)
if __name__=='__main__':
    gr1=greenlet.greenlet(test1)     #启动协程 函数名作为参数
    gr2=greenlet.greenlet(test2)
    gr1.switch()
 

  注意

1 需要调用到另一函数时 用函数协程的switch函数  

 gr1=greenlet.greenlet(test1)     #调用greenlet函数 启动协程 函数名作为参数

3 调用顺序

  ①main函数实例化并启动线程 务必随后进入协程 上例为进入test1()-->gr1.switch()

  ②当遇到gr2.switch() 语句 将从test1函数转为执行test2函数 

  ③最后按顺序执行输出1 2 3 4

 

 

gevent 模板(自动切换)

 
import gevent
from gevent import monkey
monkey.patch_all()  #把当前程序所有的io操作都单独做上标记
def f1():
    print(1)
    gevent.sleep(5)     #gevent中的休眠函数 模拟IO操作长耗时 当协程遇到IO即向下切换
    print(5)
def f2():
    print(2)
    gevent.sleep(3)     #遇到IO继续向下切换 如若都有IO 则反复切换 直到时间结束
    print(4)
def f3():
    print(3)
    gevent.sleep(0)     #即使0秒 也代表切换协程信号
    print(3.1)
gevent.joinall([
    gevent.spawn(f1),   #启动协程,以列表形式储存
    gevent.spawn(f2),
    gevent.spawn(f3)
])
 

 注意:

1 gevent为第三方库 已对实现并发,同步、异步编程进行封装 (推荐使用)

2 原理:当遇到I/O操作将自动切换 进行下一协程的运行 若还有i/o则再向下 随后再从头循环   如若都有I/O 则反复切换 直到时间结束 

目的:①节省时间 最终花费最长协程消耗的时间

     ②节约内存,内存消耗远小于进程交替

3   from gevent import monkey
     monkey.patch_all()      //这两句把当前程序所有的i/o操作都单独做上标记--运用与基本网络爬虫中(或那些不会被系统直接认出I/O操作的语句)

gevent的实例化:

   gevent.joinall([
        gevent.spawn(f1),         加入协程,以列表形式储存
      gevent.spawn(f2),      #调用gevent.joinall(列表) 
      gevent.spawn(f3)      #gevent.spawn(f2), 启动协程  亦可再其后补充参数---spawn(函数名,‘参数’)
   ])

5     gevent.sleep(5)     #gevent中的休眠函数 模拟I/O操作长耗时 (当协程遇到I/O即向下切换)

  gevent.sleep(0)     #即使0秒 也代表切换协程切换信号  因此向下切换协程

============================================================================================================

 事件驱动:

定义:通常 在收到一个请求后,放入一个事件列表,让主进程通过非阻塞I/O的方式来处理信息

举个例子:

模拟鼠标键盘输入时,常常要对鼠标点击进行相应,首先如何获得鼠标点击呢?

运用事件驱动的思想:

1. 有一个事件(消息)队列;
2. 鼠标按下时,往这个队列中增加一个点击事件(消息)--随后继续等待接收鼠标下一次点击(避免卡顿)
3. 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
4. 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;

注意:

1 左侧事件源 只管理自己项目 (例:鼠标,键盘输入) 接收到后丢给队列 自己继续接收

2 右侧处理线程一步步去取得 不同任务并调用对应函数解决

 

好处: 避免cpu资源浪费 若一个线程堵塞 无法进行其他操作(简称卡机)因此使用协程

 

 

Python协程

最近对Python中的协程挺感兴趣,这里记录对协程的个人理解。

要理解协程,首先需要知道生成器是什么。生成器其实就是不断产出值的函数,只不过在函数中需要使用yield这一个关键词将值产出。下面来看一个例子:

def gen():
    n = 0
    while True:
        yield n
        n += 1
        
        
g = gen()
print(g)  # <generator object gen at 0x00000246E165A7C8>
print(next(g))  # 输出结果为0
print(next(g))  # 输出结果为1

我们调用gen()函数并不会直接执行该函数,而是会得到一个生成器对象。对这个生成器对象调用next()函数,这个生成器对象会开始执行到第一个yield处,于是产出一个值0,注意:这时候gen()就暂停在yield处,直到第二次调用next()函数。

到这里我们可以发现,生成器函数是可以暂停的函数,它在调用方的驱使下(调用方使用next()函数),每次执行到yield处将yield后方的值产出给调用方后就暂停自己的执行,直到调用方下一次驱动它执行。

send

我们知道,生成器函数可以不断的产出值给调用方,那如果想要调用方传递值给生成器函数呢?这就自然而然的引入了send()函数。来看send()函数的使用:

def gen():
    s = yield "hello"
    print("用户传递进来的值为:%s" % s)
    yield s


g = gen()
print(next(g))
print(g.send("world")) 

执行上面的代码,我们可以看到结果如下:


 
 

来看看上面代码的执行:首先调用gen()得到一个生成器对象,这时候生成器函数还没有开始执行,接着调用next()函数,生成器函数执行到第一个yield处,产出字符串hello后暂停执行,调用方得到产出的值打印输出。然后调用方通过send()发送了一个字符串world给生成器函数,这时候,生成器函数将world赋值给s,继续它的执行,直到第二个yield处,将调用方传递进来的world返回给调用方。

到这里我们可以发现,此时的生成器函数既可以暂停时产出值,又可以接收调用方传递进来的值恢复执行,这就和协程的思想差不多了。

yield from

python3.3中提出了这样一个表达式yield from,我所知道的这个表达式有两个用法:

第一个用法是简化for循环:

def func():
    for x in "ABC":
        yield x

for x in func():
    print(x)

上面的写法等同于:

def func():
    yield from "ABC"

for x in func():
    print(x)

这是yield from 的第一种用法,即后面跟一个可迭代的对象,yield from可以在调用方的驱使下将可迭代对象一个一个的输出。

第二个用法是作为委派生成器使用:

def func():
    """生成器函数"""
    n = 0
    while True:
        s = yield n
        if s is None:
            break
        n += 1
    return n


def deligate():
    """委派生成器"""
    result = yield from func()
    print("the result is : %s" % result)


def main():
    """调用方"""
    g = deligate()
    print(next(g))
    for i in range(3):
        print(g.send(i))
    # 在这里发送None给生成器,生成器不会产出值而抛出StopIteration异常
    try:
        g.send(None)
    except StopIteration:
        pass


if __name__ == '__main__':
    main()

在上面的代码中,委派生成器使用了yield from,这就使得调用方在得到生成器对象时,可以通过send()方法和真正的生成器(这里为func)直接通信。于是我们在调用方中使用for循环了3次,每次传递进去的值都会传递给func函数中的s,当我们最后传递进一个None时,真正的生成器跳出for循环并将n的值返回,这时候委派生成器得到func生成器的返回值并将它赋给result。这样就完成了调用方和真正的生成器函数之间的通信,并且真正的生成器在执行结束之后会将结果返回给委派生成器。

我们可以看到,使用了yield from的委派生成器其实就是为调用方和真正的生成器提供了一个通道,这个通道可以让它们直接通信。

Event Loop

在真正的理解协程之前,还有个东西时我觉得必须要理解的,那就是事件循环(Event Loop)。

协程是单线程的,单线程就意味着所有的任务需要在单线程上排队执行,也就是前一个任务没有执行完成,后一个任务就没有办法执行。在CPU密集型的任务之中,这样其实还行,但是如果我们的任务都是IO密集型的呢?也就是我们大部分的任务都是在等待网络的数据返回,等待磁盘文件的数据,这就会造成CPU一直在等待这些任务的完成再去执行下一个任务。

有没有什么办法能够让单线程的任务执行不这么笨呢?其实我们可以将这些需要等待IO设备的任务挂在一边嘛!这时候,如果我们的任务都是需要等待的任务,那么单线程在执行时遇到一个就把它挂起来,这里可以通过一个数据结构(例如队列)将这些处于执行等待状态的任务放进去,为什么是执行等待状态呢?因为它们正在执行但是又不得不等待例如网络数据的返回等等。直到将所有的任务都放进去之后,单线程就可以开始它的接连不断的表演了:有没有任务完成的小伙伴呀!快来我这里执行!

此时如果有某个任务完成了,它会得到结果,于是发出一个信号:我完成了。那边还在循环追问的单线程终于得到了答复,就会去看看这个任务有没有绑定什么回调函数呀?如果绑定了回调函数就进去把回调函数给执行了,如果没有,就将它所在的任务恢复执行,并将结果返回。

到这里事件循环的大致作用已经说完了,我们可以看到,仅仅有协程是不够的,我们还需要事件循环和它配合使用,这样才能让多个协程可以并发的执行。

Python3.4中,引入了asyncio包,这个包提供了关于事件循环的实现,这就使得在Python中使用协程实现高并发成为可能。我们来模拟一个爬虫:

import asyncio


@asyncio.coroutine
def get_html(url, name):
    print("%s get %s html start" % (name, url))
    yield from asyncio.sleep(2)
    print("%s get %s html end" % (name, url))


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    # 创建两个协程
    tasks = [
        get_html("http://www.baidu.com", "A"),
        get_html("http://www.souhu.com", "B"),
    ]
    # 启动事件循环并将协程放进去执行
    loop.run_until_complete(asyncio.wait(tasks))

在上面的模拟爬虫的代码中,我们使用了装饰器@asyncio.coroutine来将这个get_html()函数定义为协程,在协程中使用了asyncio.sleep()函数模拟从网络请求数据。在执行的过程中,我们首先使用asyncio提供的get_event_loop()创建一个事件循环,这里我们不需要自己实现事件循环,接着创建两个协程,并将这两个协程扔到事件循环中执行。

运行上面代码,可以看到以下结果:


 
 

仔细观察,我们会发现在协程中并没有使用time.sleep()函数,而是使用了asyncio.sleep()函数,是因为time.sleep()函数会将整个线程休眠几秒,而asyncio.sleep()其实也是一个协程,这个协程将和事件循环直接通信并将一个Future对象交给事件循环,事件循环会一直监视着它直到它的任务完成(在这里就是休眠两秒),并不会将整个线程都停止执行。

到现在,我们可以使用基于生成器的协程和事件循环来做到高并发了。但是问题来了,这里是基于生成器的协程,生成器其实有自己的用法,为什么还要给它强加一个协程的用法呢?

async/await

Python3.5中引入了async/await这一组关键词,这就使得python可以定义原生协程了。await的用法和yield from用法类似,但是await后面只能跟Awaitable的对象(实现了__await__魔法方法),而yield from后面可以跟生成器、协程等等。

使用async/await修改上面的代码:

import asyncio


async def get_html(url, name):
    print("%s get %s html start" % (name, url))
    await asyncio.sleep(2)
    print("%s get %s html end" % (name, url))


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    # 创建两个协程
    tasks = [
        get_html("http://www.baidu.com", "A"),
        get_html("http://www.souhu.com", "B"),
    ]
    # 启动事件循环并将协程放进去执行
    loop.run_until_complete(asyncio.wait(tasks))

在Tornado的官方文档中,其实是建议用户使用async/await来定义原生协程,原因有以下几点:

1.原生协程要快于基于生成器的协程

2.原生协程可以使用async forasync with语法

其它更多的差异可以见Tornado的官方文档

参考文章

1.How the heck does async/await work in Python3.5

2.Python:Generator,Coroutine,Native Coroutine and Async/Await

3.Event Loop

 

协程的优点(Python)


协程的优点: 
1.协程是进程和线程的升级版,进程和线程都面临着内核态和用户态的切换问题而耗费许多切换时间,
 而协程就是用户自己控制切换的时机,不再需要陷入系统的内核态。
2.协程的执行效率非常高。因为子程序切换不是线程切换,而是由程序自身控制。因此,没有线程切换的开销,和多线程相比,线程数量越多,相同数量的协程体现出的优势越明显
3.不需要多线程的锁机制。由于只有一个线程,也不存在同时写变量的冲突,在协程中控制共享资源不需要加锁,只需要判断数据的状态,所以执行效率远高于线程 ,对于多核CPU可以使用多进程+协程来尽可能高效率地利用CPU。
 生产者消费者模型通过协程的yield思想实现:
 

import time
 
def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
 
        print('[消费者] <--- Consuming %s...' % n)
        time.sleep(1)
        r = 'ok'
 
def producer(c):
    next(c)
    n = 0
    while n < 5:
        n = n + 1
        print('[生产者] ---> Producing %s...' % n)
        # 和next有同样的同能,可以触发函数到下一个yield,
        # 区别于next的是可以像yield的左边的变量传值,例如上面yield左边的n
        c_ret = c.send(n)  
 
        print('[生产者] Consumer return %s' % c_ret)
    c.close()
 
 
if __name__ == '__main__':
    c = consumer()
    producer(c)         


结果如下:
    
[生产者] ---> Producing 1...
[消费者] <--- Consuming 1...
[生产者] Consumer return ok
[生产者] ---> Producing 2...
[消费者] <--- Consuming 2...
[生产者] Consumer return ok
[生产者] ---> Producing 3...
[消费者] <--- Consuming 3...
[生产者] Consumer return ok
[生产者] ---> Producing 4...
[消费者] <--- Consuming 4...
[生产者] Consumer return ok
[生产者] ---> Producing 5...
[消费者] <--- Consuming 5...
[生产者] Consumer return ok

可以看出通过协程协程实现的生产者消费者模型是一种可控的生产消费模型,在消费者producer调用send之后启动生产者,实现可控的生产者消费者之间的通信。 
使用yield手动实现协程是比较麻烦的,Python提供了greenlet和gevent模块用来实现协程。

gevent实现生产者消费者

from gevent import monkey;monkey.patch_all()
from gevent.queue import Queue #队列 gevent中的队列
import gevent
import random
#这个猴子补丁,all是所有能切换协程的地方都切换,包含了socket,所以一般都用all

qq = Queue(3)

def produceer():
while True:
item = random.randint(0,99)
qq.put(item)
print("生产了:", item)

def consumer():
while True:
item = qq.get()
print("消费了:",item)

p = gevent.spawn(produceer)
c = gevent.spawn(consumer)
gevent.joinall([p,c])

python协程进阶,原来实现一个事件循环可以如此简单!

引言
目前很多公司选择将python项目使用golang重构,很大一方面原因是因为golang的并发能力,golang自带的语法糖支持使并发编程变的相对简单,也更能充分的使用多核CPU的计算资源。

相应的,python长期受制于GIL,无法在多线程时使用多核CPU,所以一直以来在谈及python的缺陷时,性能总是无法回避的一个问题。当然,一些python著名的第三方组织也一直通过各种手段来改善python的并发性能,如twisted的异步模型使用事件驱动机制来提升python性能,著名的爬虫框架scrapy便是以twisted作为底层网络库来开发的,还有gevent,它使用greenlet在用户态完成栈和上下文切换来减少切换带来的性能损耗,同样还有著名的web协程框架tornado,他使用生成器来保存协程上下文及状态,使用原生的python语法实现了协程。但从python3.4开始python引入asyncio标准库,随后又在3.5引入async/await关键字,从根本上规范了python异步编程标准,使python异步编程逐渐流行起来。

关于什么是python协程,相信网上已经有了不少资料,但是只描述抽象的上层建筑难免会让人接受困难,本文希望可以通过从最简单的代码和逻辑,使用最基础的数据结构,从实现出发,带领大家理解什么是python协程。

首先需要补充一些基础知识

 

什么是生成器
我们都应该听说过迭代器,这在很多语言中都有类似的概念,简单的说,迭代器就是可以被迭代的对象,对其使用next操作可以返回一个元素,通常多次迭代后迭代器会中止,此时迭代器无法再使用。比如python中可以通过iter方法来将一个列表转换成迭代器:

I
n [1]: lst = [1, 2, 3]
 
In [2]: iterator = iter(lst)
 
In [3]: next(iterator)
Out[3]: 1
 
In [4]: next(iterator)
Out[4]: 2
 
In [5]: next(iterator)
Out[5]: 3
 
In [6]: next(iterator)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-7-4ce711c44abc> in <module>()
----> 1 next(iterator)
 
StopIteration:
进python群:835017344,获取python学习资料
生成器可以看作是迭代器的子类,同时提供了比迭代器更强大的功能,python中,可以使用yield关键字使函数返回生成器对象。

In [8]: def fun():
   ...:     yield 1
   ...:     yield 2
   ...:     yield 3
   ...:

In [9]: iterator = fun()

In [10]: next(iterator)
Out[10]: 1

In [11]: next(iterator)
Out[11]: 2

In [12]: next(iterator)
Out[12]: 3

In [13]: next(iterator)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-13-4ce711c44abc> in <module>()
----> 1 next(iterator)

StopIteration:
每次next调用, fun函数只执行四分之一,如果我们拥有多个生成器对象, 按照一定规则 可控的对他分别调用next,生成器每次的暂停都保存了执行进度和内部状态。如果将这三个生成器理解成协程,那不正是我们熟悉的协程间的切换?

事件循环
所以,我们可以想象,现在有一个循环和一个生成器列表,每次循环,我们都将所有的生成器进行一次调用,所有生成器交替执行。如下:

In [16]: gen_list = [fun(), fun(), fun()]

In [17]: while True:
    ...:     for gen in gen_list:
    ...:         print(next(gen))
    ...:
1
1
1
2
2
2
3
3
3
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-17-f2c1d557da29> in <module>()
      1 while True:
      2     for gen in gen_list:
----> 3         print(next(gen))
      4

StopIteration:
当然,我们还可以换一种写法,将生成器的每一步都当成是一次调用,把生成器包装成一个Handle对象,每次调用handle对象的call来完成生成器的调用,同时,我们还可以在调用完成后做一些准备来控制下一次调用的时间,将Handle对应放到一个scheduled_list里面:

def fun():
    print("step1")
    yield
    print("step2")
    yield
    print("step3")
    yield 


scheduled_list = []


class Handle(object):
    def __init__(self, gen):
        self.gen = gen

    def call(self):
        next(self.gen)
        scheduled_list.append(self)


def loop(*coroutines):
    scheduled_list.extend(Handle(c) for c in coroutines)
    while True:
        while scheduled_list:
            handle = scheduled_list.pop(0)
            handle.call()


if __name__ == "__main__":
    loop(fun(), fun(), fun())
协程中的阻塞
在有了以上的基础后,我们来分析上面提到的切换规则,什么时候应该切换协程(生成器)?显而易见,当遇到阻塞时,我们才需要切换协程,以避免CPU的浪费。我将阻塞分为了以下三种:

IO调用,如socket,file,pipe等。
人为制造的阻塞,如sleep。
异步调用。
假设,在我们的生成器内有一次socket调用,我们不知道它多久会ready,我们希望不等待它的返回,切换到其它协程运行,等其准备好之后再切换回来,该怎么办?

有同学可能会想到了,将socket注册到epoll上。如下:

import time
import socket
from functools import partial
from select import epoll

poll = epoll()
handlers = dict()
scheduled_list = []


def fun():
    print("step1")
    sock = socket.socket()
    future = Future()

    def handler():
        future.set_done(sock.recv(1024))

    add_handler(sock.fileno(), handler, READ)
    yield future
    print("step2")
    yield 
    print("step3")
    yield 


def add_handler(fd, handler, events):
    handlers[fd] = handler
    poll.register(fd, events)


class Future(object):

    def __init__(self):
        self.callbacks = []

    def add_callback(self, callback):
        self.callbacks.append(callback)

    def set_done(self, value):
        self.value = value
        for callback in self.callbacks:
            callback()

    def get_result(self):
        return self.value


class Handle(object):
    def __init__(self, gen):
        self.gen = gen

    def call(self):
        yielded = next(self.gen)
        if isinstance(yielded, Future):
            yielded.add_callback(partial(scheduled_list.append, self))
        else:
            scheduled_list.append(self)


def loop(*coroutines):
    scheduled_list.extend(Handle(c) for c in coroutines)
    while True:

        default_timeout = 10000

        while scheduled_list:
            handle = scheduled_list.pop(0)
            handle.call()

        # 等待描述符可操作
        events = poll.poll(default_timeout)
        while events:
            fd, event = events.popitem()
            handlers[fd]()
            poll.unregister(fd)
            del handlers[fd]


if __name__ == "__main__":
    loop(fun(), fun(), fun())
这一步引入一个新的对象Future,他用来代指未来即将发生的调用,通过epoll上注册的事件,触发了它的完成,完成之后执行了将handle对象放回scheduled_list, 可从而切回了协程。

那么,人为制造的阻塞我们怎么切换协程呢?这里,我们又引入了一个新的对象Timeout

import time
import socket
from functools import partial
from select import epoll

poll = epoll()
handlers = dict()

scheduled_list = []
# 创建一个timeout_list
timeout_list = []


def fun():
    print("step1")
    sock = socket.socket()
    future = Future()

    def handler():
        future.set_done()

    add_handler(sock.fileno(), handler, READ)
    yield future
    print("step2")
    yield sleep(3)
    print("step3")
    yield 


def add_handler(fd, handler, events):
    handlers[fd] = handler
    poll.register(fd, events)


def sleep(sec):
    future = Future()
    timeout = Timeout(sec, future.set_done)
    timeout_list.append(timeout)
    return future


class Timeout(object):

    def __init__(self, timeout, callback):
        self.deadline = time.time() + timeout
        self.callback = callback

    def call(self):
        self.callback(None)


class Future(object):

    def __init__(self):
        self.callbacks = []
        self.value = None

    def add_callback(self, callback):
        self.callbacks.append(callback)

    def set_done(self, value):
        self.value = value
        for callback in self.callbacks:
            callback()

    def get_result(self):
        return self.value


class Handle(object):
    def __init__(self, gen):
        self.gen = gen

    def call(self):
        yielded = next(self.gen)
        if isinstance(yielded, Future):
            yielded.add_callback(partial(scheduled_list.append, self))
        else:
            scheduled_list.append(self)


def loop(*coroutines):
    scheduled_list.extend(Handle(c) for c in coroutines)
    while True:

        default_timeout = 10000
        deadline = time.time()

        for timeout in timeout_list[:]:
            if timeout.deadline <= deadline:
                timeout.call()
                timeout_list.remove(timeout)

        while scheduled_list:
            handle = scheduled_list.pop(0)
            handle.call()

        for timeout in timeout_list:
            wait_time = timeout.deadline - deadline
            if wait_time <= 0:
                wait_time = 0
            default_timeout = min(default_timeout, wait_time)

        if not scheduled_list and not timeout_list and not handlers:
            break

        # 等待描述符可操作
        events = poll.poll(default_timeout)
        while events:
            fd, event = events.popitem()
            handlers[fd]()
            poll.unregister(fd)
            del handlers[fd]


if __name__ == "__main__":
    loop(fun(), fun(), fun())
通过创建一个Timeout对象,我们在deadline时触发了其回调,使Future完成,从而完成了协程的切换。

由以上两点,我们可以大致观察出一个规律,创建Future对象,切出协程,在合适的时机(如socket ready或到达deadline/timeout)让他完成,切入协程,这才是协程切换的关键所在,由此,我们可以使用Future来管理各种异步调用。

如,我们在python编码时遇到了一个计算密集型的函数,由于python单进程无法利用多核,我们可以创建一个子进程来处理计算,同时关联到一个Future中:

def fun():
    print("step1")
    sock = socket.socket()
    future = Future()

    def handler():
        future.set_done()

    add_handler(sock.fileno(), handler, READ)
    yield future
    print("step2")
    yield sleep(3)
    print("step3")
    future = Future() 
    from multiprocessing import Process
    Process(target=long_time_call, args=(future, )).start()
    yield future


def long_time_call(future):
    #...
    future.set_done()
当协程执行到第三步时,遇到了长时间运行的函数调用,我们创建了一个Future,关联到一个子进程中,并在子进程完成时设置future完成,在子进程完成之前,父进程已完成协程的切出,将执行权交给其它协程执行。

这个地方遗漏了一个细节,当没有其它协程可以执行时,epoll会被设置成超时时间=10000,因而陷入到长时间的睡眠中,而子进程完成后需要切入协程,但父进程已经被epoll阻塞掉,如何唤醒主进程继续执行该协程呢?业界通用的做法是,创建一个管道,在切出协程时让epoll监听读fd,子进程完成后,往管道中写入一个字符,epoll监听的读fd 马上变成ready,因此epoll解除阻塞,事件循环得以继续执行。

当然,异步调用不仅仅可以使用子进程,子线程、远程计算框架都可以通过这种方式执行。

讲到这里,大家应该基本明白了一个协程函数是如何工作的了。下表可帮助我们从线程的角度理解协程



上面的表格表述线程和协程的一一对应关系,最后一栏可能还需要举例解释一下:

我们知道一个线程执行过程中,会嵌套调用多个函数,如:

def foo():
    print("in foo")
    
    
def bar():
    print("in bar")


def fun():
    bar()
    foo()
    

if __name__ == "__main__":
    fun()
那么生成器如何嵌套调用呢?python3.4之前,嵌套的生成器只能这么使用:

def foo():
    print("in foo")
    yield


def bar():
    print("in bar")
    yield


def fun():
    for i in bar():
        yield i
    for i in foo():
        yield i


if __name__ == "__main__":
    for i in fun():
        pass
python3.4之后引入了新的语法糖yield from,简化了调用方式:

def foo():
    print("in foo")
    yield


def bar():
    print("in bar")
    yield


def fun():
    yield from bar()
    yield from foo()
    

if __name__ == "__main__":
    for i in fun():
        pass
yield from可以驱动子生成器,来逐一返回子生成器中的值,将嵌套的生成器打平。值得一提的是,yield from才是await的真实身份。

让我们用最初的例子来编写一个嵌套了的子生成器函数的协程demo。我们将fun生成器抽离成2种阻塞操作,并封装好:

def read(sock):
    future = Future()

    def handler():
        buf = sock.recv(1024)
        future.set_done(buf)

    add_handler(sock.fileno(), handler, 0x001)
    yield future
    return future.get_result()


def sleep(sec):
    future = Future()
    timeout = Timeout(sec, future.set_done)
    timeout_list.append(timeout)
    yield future
有了这两个基础函数之后,我们就可以自由的编写我们协程了

def coroutine(num):
    client = socket.socket()
    client.connect(("", 1234))
    print(f"coroutine_{num} start")
    buffer = yield from read(client)
    print(f"coroutine_{num} recv: ", buffer)
    yield from sleep(3)
    print(f"coroutine_{num} wake up ")
    client.close()


if __name__ == "__main__":
    loop(coroutine(1), coroutine(2))
我们创建了两个协程,其中调用了一次socket读和一个睡眠,让我们看一下执行效果:

coroutine_1 start
coroutine_2 start
coroutine_2 recv:  b'test'
coroutine_1 recv:  b'test'
coroutine_2 wake up 
coroutine_1 wake up
两个协程异步交替执行。

asyncio的使用
相信看完上面的例子之后,大家应该对python协程的实现有了初步的认识,那标准的python协程如何使用呢?

import socket
import asyncio


async def read(sock):
    loop = asyncio.get_event_loop()
    future = loop.create_future()

    def handler():
        buf = sock.recv(1024)
        future.set_result(buf)
        loop.remove_reader(sock.fileno())

    loop.add_reader(sock.fileno(), handler)
    await future
    return future.result()


async def coroutine(num):
    client = socket.socket()
    client.connect(("", 1234))
    print(f"coroutine_{num} start")
    buffer = await read(client)
    print(f"coroutine_{num} recv: ", buffer)
    await asyncio.sleep(3)
    print(f"coroutine_{num} wake up ")
    client.close()


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.gather(coroutine(1), coroutine(2)))
几乎和我们的实现一模一样,其中await取代了yield from, 协程显式使用async来声明。

python协程的应用
python协程优势
通过上述例子我们可以很容易看出,python协程具有以下特点:

超级轻量,不需要维护协程栈,所有的上下文执行状态都被维护在了生成器中。
切换自由,通过yield from(python3.5以后是await)随意切换协程,协程切换完全可控以至于几乎不用加锁。
并发能力强,并发上限理论上取决于IO多路复用可注册的文件描述符的极限。
缺点
还是只能用到单核。
由于协程切换是非抢占式的,所以如果业务是CPU密集型的,可能其它协程长时间得不到执行。
综上所述,在使用python的高并发场景下,python多进程+协程是最优的解决方案

 

python使用协程并发

 

协程

协程是一种用户态的轻量级线程,又称微线程。

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

优点:

  1. 无需线程上下文切换的开销
  2. 无需原子操作锁定及同步的开销
  3. 方便切换控制流,简化编程模型
  4. 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。视作整体是原子性的核心。

缺点:

  1. 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
  2. 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

 

使用Gevent

gevent是python的一个并发框架,以微线程greenlet为核心,使用了epoll事件监听机制以及诸多其他优化而变得高效.

  • 简单示例

gevent的sleep可以交出控制权,当我们在受限于网络或IO的函数中使用gevent,这些函数会被协作式的调度, gevent的真正能力会得到发挥。Gevent处理了所有的细节, 来保证你的网络库会在可能的时候,隐式交出greenlet上下文的执行权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import gevent
 
def foo():
    print('running in foo')
    gevent.sleep(0)
    print('com back from bar in to foo')
 
def bar():
    print('running in bar')
    gevent.sleep(0)
    print('com back from foo in to bar')
 
# 创建线程并行执行程序
gevent.joinall([
    gevent.spawn(foo),
    gevent.spawn(bar),
])

  执行结果

1
2
3
4
running in foo
running in bar
com back from bar in to foo
com back from foo in to bar
  • 同步异步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import random
import gevent
 
def task(pid):
    gevent.sleep(random.randint(02* 0.001)
    print('Task %s done' % pid)
 
def synchronous():
    for in range(110):
        task(i)
 
def asynchronous():
    threads = [gevent.spawn(task, i) for in range(10)]
    gevent.joinall(threads)
 
print('Synchronous:')
synchronous()
 
print('Asynchronous:')
asynchronous()

  执行输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Synchronous:
Task 1 done
Task 2 done
Task 3 done
Task 4 done
Task 5 done
Task 6 done
Task 7 done
Task 8 done
Task 9 done
Asynchronous:
Task 1 done
Task 4 done
Task 5 done
Task 9 done
Task 6 done
Task 0 done
Task 2 done
Task 3 done
Task 7 done
Task 8 done
  • 以子类的方法使用协程

可以子类化Greenlet类,重载它的_run方法,类似多线程和多进程模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import gevent
from gevent import Greenlet
 
class Test(Greenlet):
 
    def __init__(self, message, n):
        Greenlet.__init__(self)
        self.message = message
        self.n = n
 
    def _run(self):
        print(self.message, 'start')
        gevent.sleep(self.n)
        print(self.message, 'end')
 
tests = [
    Test("hello"3),
    Test("world"2),
]
 
for test in tests:
    test.start()  # 启动
 
for test in tests:
    test.join()  # 等待执行结束
  • 使用monkey patch修改系统标准库(自动切换协程)

当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。

由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。

由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import gevent
import requests
from gevent import monkey
 
monkey.patch_socket()
 
def task(url):
    = requests.get(url)
    print('%s bytes received from %s' % (len(r.text), url))
 
gevent.joinall([
    gevent.spawn(task, 'https://www.baidu.com/'),
    gevent.spawn(task, 'https://www.qq.com/'),
    gevent.spawn(task, 'https://www.jd.com/'),
])
print "success"#调用完所有的协程才会输出,如果没有joinall这个函数,协程可能还没执行完程序就退出了。

  

执行输出

1
2
3
2443 bytes received from https://www.baidu.com/
108315 bytes received from https://www.jd.com/
231873 bytes received from https://www.qq.com/

可以看出3个网络操作是并发执行的,而且结束顺序不同

 

参考链接:http://hhkbp2.github.io/gevent-tutorial/

 

Python运维之路——协程、事件驱动与异步IO

Event事件与协程

Python 协程的详细用法和例子

posted @ 2019-11-07 13:29  南哥的天下  阅读(404)  评论(0编辑  收藏  举报