Python中的协程

  • baagee 发布于 2017-08-02 10:28:30
  • 分类:Python
  • 1089 人围观
  • 0 人喜欢

1,协程是啥

协程,又称微线程,纤程。英文名Coroutine。

首先我们得知道协程是啥?协程其实可以认为是比线程更小的执行单元。 为啥说他是一个执行单元,因为他自带CPU上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。

通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定

2,协程和线程差异

那么这个过程看起来和线程差不多。其实不然, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文

3,协程的好处

在IO密集型的程序中由于IO操作远远慢于CPU的操作,所以往往需要CPU去等IO操作。 同步IO下系统需要切换线程,让操作系统可以在IO过程中执行其他的东西。 这样虽然代码是符合人类的思维习惯但是由于大量的线程切换带来了大量的性能的浪费,尤其是IO密集型的程序。

所以人们发明了异步IO。就是当数据到达的时候触发我的回调。来减少线程切换带来性能损失。 但是这样的坏处也是很大的,主要的坏处就是操作被 “分片” 了,代码写的不是 “一气呵成” 这种。 而是每次来段数据就要判断 数据够不够处理哇,够处理就处理吧,不够处理就在等等吧。这样代码的可读性很低,其实也不符合人类的习惯。

但是协程可以很好解决这个问题。比如 把一个IO操作 写成一个协程。当触发IO操作的时候就自动让出CPU给其他协程。要知道协程的切换很轻的。 协程通过这种对异步IO的封装 既保留了性能也保证了代码的容易编写和可读性。在高IO密集型的程序下很好。但是高CPU密集型的程序下没啥好处。

4,协程一个简单实现原理

import time

def A():
    while True:
        print("----A---")
        yield
        time.sleep(0.5)

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

if __name__=='__main__':
    a = A()
    B(a)

就是通过yield实现的。

运行结果:

--B--
--A--
--B--
--A--
--B--
...省略...

5,协程-greenlet版

为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单

安装方式

使用如下命令安装greenlet模块:

sudo pip install greenlet

实例代码:

from greenlet import greenlet
import time

def work1():
    while True:
        print('-------work1-------')
        gr2.switch()
        time.sleep(0.5)

def work2():
    while True:
        print('-------work2-------')
        gr1.switch()
        time.sleep(0.5)

gr1=greenlet(work1)
gr2=greenlet(work2)

gr1.switch()

在需要切换的地方由开发者自己控制。

6,gevent

greenlet已经实现了协程,但是这个还的人工切换,是不是觉得太麻烦了,不要捉急,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent

其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。

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

安装gevent:

sudo pip install greenlet

实例代码:

import gevent

def work(c):
	for x in range(c):
		print(gevent.getcurrent(),x)
		gevent.sleep(1)

g1=gevent.spawn(work,5)
g2=gevent.spawn(work,5)
g3=gevent.spawn(work,5)

g1.join()
g2.join()
g3.join()
print('over')

结果:

<Greenlet at 0xb6ff4f2c: work(5)> 0
<Greenlet at 0xb6ee57ac: work(5)> 0
<Greenlet at 0xb701e0cc: work(5)> 0
<Greenlet at 0xb6ff4f2c: work(5)> 1
<Greenlet at 0xb6ee57ac: work(5)> 1
<Greenlet at 0xb701e0cc: work(5)> 1
<Greenlet at 0xb6ff4f2c: work(5)> 2
<Greenlet at 0xb6ee57ac: work(5)> 2
<Greenlet at 0xb701e0cc: work(5)> 2
<Greenlet at 0xb6ff4f2c: work(5)> 3
<Greenlet at 0xb6ee57ac: work(5)> 3
<Greenlet at 0xb701e0cc: work(5)> 3
<Greenlet at 0xb6ff4f2c: work(5)> 4
<Greenlet at 0xb6ee57ac: work(5)> 4
<Greenlet at 0xb701e0cc: work(5)> 4
over
[Finished in 5.1s]
gevent.sleep(1)

是为了模拟这个函数比较耗时。不能用time.sleep(1)。

当然,实际代码里,我们不会用gevent.sleep()去切换协程,而是在执行到IO操作时,gevent自动切换,代码如下

from gevent import monkey
import gevent
import urllib.request

monkey.patch_all()

def downHtml(url):
	print('get=>%s'%url)
	resp=urllib.request.urlopen(url)
	html=resp.read()
	print("%d bytes received from %s"%(len(html),url))

gevent.joinall([
	gevent.spawn(downHtml,'http://baagee.vip/index/articles/cid/30.html'),
	gevent.spawn(downHtml,'http://www.baidu.com/'),
	gevent.spawn(downHtml,'http://www.baagee.vip/')
	])

运行结果:

get=>http://baagee.vip/index/articles/cid/30.html
get=>http://www.baidu.com/
get=>http://www.baagee.vip/
42599 bytes received from http://www.baagee.vip/
111541 bytes received from http://www.baidu.com/
38415 bytes received from http://baagee.vip/index/articles/cid/30.html
[Finished in 0.5s]

monkey.patch_all()

有IO操作时才需要这一句,目的就是让程序自己判断遇到IO时执行另一个协程,不必等待。

收到数据的先后顺序不一定与发送顺序相同,这也就体现出了异步,即不确定什么时候会收到数据,顺序不一定,由系统自己识别调度,节约资源

简单的gevent版本TCP服务器示例:

import sys
import time
import gevent

from gevent import socket,monkey
monkey.patch_all()

def handle_request(conn):
    while True:
        data = conn.recv(1024)
        if not data:
            conn.close()
            break
        print("recv:", data)
        conn.send(data)


def server(port):
    s = socket.socket()
    s.bind(('', port))
    s.listen(5)
    while True:
        cli, addr = s.accept()
        gevent.spawn(handle_request, cli)

if __name__ == '__main__':
    server(7788)

这里要用gevent里面的socket,还要加上monkey.patch_all(),让协程不会阻塞。


转载请说明出处:baagee博客 » Python中的协程

评论

点击图片切换
还没有评论,快来抢沙发吧!