Python协程

※ python协程

协程也称之为微线程,协程的作用是在执行函数A的情况下,可以随时中断去执行函数B,然后终端继续执行函数A,但是这个过程并没有函数调用,协程只有一个线程在执行。
所以协程由于是程序主动控制切换的,所以没有线程切换的开销(存在两个必要的开销:线程的创建和上下文的切换,切换前,上个任务的状态会被保存,切换回来,再加载这个任务的状态),所以执行效率很高,使用于IO密集型,如果是CPU密集型,推荐多进程+协程。

python对于协程的支持是通过生成器实现的,协程是遵循某些规则的生成器。

生成器的yield表达式:
__next__ 启动或者恢复generator的执行,相当于send(None)
send() 作用是发送值给yield表达式

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

现在改成使用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率就很高了:

首先来看一下yield到底是怎么工作的:

def test():
	print('start')
	n = 1
	while True:
		r = yield n
		print('ok', r)
		n += 1

# 创建生成器对象
gen = test()

# 启动生成器
res = gen.__next__()
# res = gen.send(None) 其实是跟上面一行一样的作用
print('res', res)
>>> start
>>> res 1

# 发送具体的值给yield表达式
send_res = gen.send(100)
print('send res', send_res)
>>> ok 100
>>> send res 2

上面可以看到使用生成器的最开始要调用__next__或者send(None)方法,然后代码执行到yield n的地方会直接暂停,直到下一次被send(100)方法给唤醒了。

def consumer():  # 消费者
	r = 'start'
	while True:
		n = yield r
		if not n:
			print{'n is empty')
			continue
		print('consumer consume', n)
		r = '200 ok'  # 运行到这里相当于r重新赋值

def producer(c):  # 生产者
	start_value = c.send(None)  # 这里启动了生成器
	print(start_value)
	n = 0
	while n < 3:
		n += 1
		print('producer produce', n)
		r = c.send(n)  # 一旦生成了东西,就切换到消费者执行
		print('consumer return', r)
	c.close()

c = consumer() # 创建了一个生成器
producer(c)  # 将生成器当做是参数传入
>>> start
>>> producer produce 1
>>> consumer consume 1
>>> consumer return 200 ok
>>> ...

yield from的用法:
当一个生成器函数需要处理另一个生成器生成的值,通常的用法是两个for循环嵌套:

for a in iterables:
	for b in a:
		yield b

但是yield from完全替代了内层的for循环:

for a in iterables:
	yield from b

asyncio

这个库直接内置了对异步io的支持,这个库的模型就是消息循环,从该模块获取一个EventLoop的引用,然后把需要执行的协程放到里面,就实现了异步IO

import threading
import asyncio

@asyncio.coroutine  # 把这个生成器标记成协程
def hello():
    print('Hello1 world! (%s)' % threading.currentThread())
    yield from asyncio.sleep(4)  # 这里调用了另一个生成器
    # 由于上面生成器也是一个协程,所以不会等待,而是直接中断并执行下一个消息循环
    # 把上面的sleep看成是1秒的IO操作,此期间,线程不会等待,而是找loop里面其他可以执行的协程
    print('Hello1 again! (%s)' % threading.currentThread())

@asyncio.coroutine
def hello():
    print('Hello2 world! (%s)' % threading.currentThread())
    yield from asyncio.sleep(2)
    print('Hello2 again! (%s)' % threading.currentThread())

loop = asyncio.get_event_loop()
tasks = [hello1(), hello2()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

输出:

Hello1 world! (<_MainThread(MainThread, started 140368765929216)>)
Hello2 world! (<_MainThread(MainThread, started 140368765929216)>)
暂停2秒
Hello2 again! (<_MainThread(MainThread, started 140368765929216)>)
再暂停2秒
Hello1 again! (<_MainThread(MainThread, started 140368765929216)>)

其实上面就模拟了,两个协程切换执行的情况,hello1先执行,遇到了相当于4秒的IO操作,然后切换到协程hello2执行,遇到了2秒的IO操作,这个时候没有可以切换的协程了,所以先执行完协程hello2,然后此时hello1还有2秒的IO操作。

上面根据线程ID可以看出,2个函数通过由一个线程通过coroutine并发完成


async/await

上面的asyncio其实就是通过该库提供的@asyncio.coroutine装饰器将一个生成器标记为协程类型,然后在协程内部调用yield from调用另一个协程实现异步操作。

使用最新的async和await只需要做一个简单的替换:

  1. @asyncio.coroutine --> async
  2. yield from --> await
    使得回调的写法更加直观

所以改写上面的例子:

async def hello1()
	print('hello1')
	r = await asyncio.sleep(1)
	print('hello1 again')

greenlet

前面说到的使用原生的generator是一个semicoroutine半协同程式,但是greenlet是一个真正的协程,生成器实现时yield value的时候,只能将value返回给调用者,但是在greenlet中,可以切换到指定的协程。

from greenlet import greenlet
def test1():
    print 12
    gr2.switch()
    print 34

def test2():
    print 56
    gr1.switch()
    print 78

gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
>>> 12 56 34

创建一个greenlet时,会初始化一个空的栈。switch这个栈的时候,会运行构造greenlet时传入的函数,如果过程中切换到了另一个,那么该协程会被挂起,等到切换回来。

对于greenlet,最常用的写法就是x = gr.switch(y),就是切换到gr协程,并且传入参数y,然后切换回来的时候,将值赋值给x

协程的生命周期:只有当协程对应的函数执行完毕后,协程才会挂掉。如果试图切换到一个已经挂掉的协程上时,实际上会切换到他的parent greenlet。


gevent!!!

gevent是Python的第三方库,通过greenlet实现了协程,核心思想是:当一个greenlet遇到IO操作的时候,就自动切换到其他的greenlet,等到IO操作完成再切换回来。gevent主要目的是自动切换协程,这样就能保证总有greenlet在运行。

如上面所说,因为切换greenlet是在IO操作的时候,自动完成的,所以gevent需要修改python的一些自带的标准库,这个过程是通过monkey patch完成的:

from gevent import monkey; monkey.patch_socket()
import gevent

def f(n):
    for i in range(n):
        print gevent.getcurrent(), i

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

在这种情况下,其实会发现三个greenlet是依次运行的,这是因为并没有遇到IO操作,所以如果我们加上一个gevent.sleep()运行的话,那么他们就会交替运行。

gevent只能在Linux下运行,在Windows下不保证正常安装和运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值