Python 协程

2019-01-09  本文已影响145人  Devops海洋的渔夫

仅供学习,转载请注明出处

协程

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

协程是啥

协程是python个中另外一种实现多任务的方式,只不过比线程更小占用更小执行单元(理解为需要的资源)。 为啥说它是一个执行单元,因为它自带CPU上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。

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

协程和线程差异

在实现多任务时, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。

简单实现协程 - yield转化为生成器以及next方法的结合使用

#coding=utf-8
import time

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

def work2():
    while True:
        print("----work2---")
        yield   # 使用yield关键字,使得work2方法变成生成器
        time.sleep(0.5)

def main():
    w1 = work1()
    w2 = work2()
    while True:
        next(w1)  # 调用next方法,执行生成器,使用生成器的print方法
        next(w2)

if __name__ == "__main__":
    main()

执行效果如下:

G:\Python27\python.exe F:/pythonProject/Iter/coroutine.py
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---

greenlet

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

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

pip install greenlet

查看安装好的第三方库:

[root@server01 work]# pip list
Package                            Version
---------------------------------- -------
backports.shutil-get-terminal-size 1.0.0  
backports.ssl-match-hostname       3.5.0.1
configobj                          4.7.2  
decorator                          3.4.0  
enum34                             1.1.6  
gevent                             1.3.7  
greenlet                           0.4.15 
iniparse                           0.4    
ipaddress                          1.0.16 
ipython                            5.8.0  
ipython-genutils                   0.2.0  
pathlib2                           2.3.3  
perf                               0.1    
pexpect                            4.6.0  
pickleshare                        0.7.5  
pip                                18.1   
prompt-toolkit                     1.0.15 
ptyprocess                         0.6.0  
pycurl                             7.19.0 
Pygments                           2.3.0  
pygobject                          3.22.0 
pygpgme                            0.3    
pyliblzma                          0.5.3  
python-linux-procfs                0.4.9  
pyudev                             0.15   
pyxattr                            0.5.1  
scandir                            1.9.0  
schedutils                         0.4    
setuptools                         40.6.3 
simplegeneric                      0.8.1  
six                                1.12.0 
slip                               0.4.0  
slip.dbus                          0.4.0  
traitlets                          4.3.2  
urlgrabber                         3.10   
wcwidth                            0.1.7  
yum-metadata-parser                1.1.4  
[root@server01 work]# 
#coding=utf-8

from greenlet import greenlet
import time

def test1():
    while True:
        print "---A--"
        gr2.switch()
        time.sleep(0.5)

def test2():
    while True:
        print "---B--"
        gr1.switch()
        time.sleep(0.5)

gr1 = greenlet(test1)
gr2 = greenlet(test2)

#切换到gr1中运行
gr1.switch()

运行如下,则报错:

[root@server01 work]# python greenlet.py 
Traceback (most recent call last):
  File "greenlet.py", line 3, in <module>
    from greenlet import greenlet
  File "/work/greenlet.py", line 3, in <module>
    from greenlet import greenlet
ImportError: cannot import name greenlet
[root@server01 work]# 

其实这个错误很明显就是我傻了,居然写了一个greenlet.py的命名文件,导致文件在import的时候,首先查询这个文件有没有greenlet的类方法,我这个文件没写,当然就报错了。

解决的方法:将文件名修改一下即可。我修改为test.py文件,执行一下看看。

[root@server01 work]# python test.py 
---A--
---B--
---A--
---B--
---A--
---B--
---A--
---B--
---A--

可以很清楚地看到test1()test2()的两个方法相互在切换调用,主要就是靠gr1.switch()gr2.switch()之间进行切换。

但是这里有个缺点,就是切换需要自己手动去处理,这个肯定比较麻烦了。下面如果用gevent来避免这种事情。

gevent

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

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

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

安装

pip install gevent

gevent的使用

#coding=utf-8

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()

从代码可以看出,这是开了三个gevent.spawn()进行调用f的函数方法,使用协程切换打印5次。
使用join方法来阻塞,使得协程可以执行完毕。

执行如下:

[root@server01 work]# python test1.py 
(<Greenlet "Greenlet-0" at 0x7fef6badfcb0: f(5)>, 0)
(<Greenlet "Greenlet-0" at 0x7fef6badfcb0: f(5)>, 1)
(<Greenlet "Greenlet-0" at 0x7fef6badfcb0: f(5)>, 2)
(<Greenlet "Greenlet-0" at 0x7fef6badfcb0: f(5)>, 3)
(<Greenlet "Greenlet-0" at 0x7fef6badfcb0: f(5)>, 4)
(<Greenlet "Greenlet-1" at 0x7fef6badfdb8: f(5)>, 0)
(<Greenlet "Greenlet-1" at 0x7fef6badfdb8: f(5)>, 1)
(<Greenlet "Greenlet-1" at 0x7fef6badfdb8: f(5)>, 2)
(<Greenlet "Greenlet-1" at 0x7fef6badfdb8: f(5)>, 3)
(<Greenlet "Greenlet-1" at 0x7fef6badfdb8: f(5)>, 4)
(<Greenlet "Greenlet-2" at 0x7fef6badfec0: f(5)>, 0)
(<Greenlet "Greenlet-2" at 0x7fef6badfec0: f(5)>, 1)
(<Greenlet "Greenlet-2" at 0x7fef6badfec0: f(5)>, 2)
(<Greenlet "Greenlet-2" at 0x7fef6badfec0: f(5)>, 3)
(<Greenlet "Greenlet-2" at 0x7fef6badfec0: f(5)>, 4)
[root@server01 work]# 

但是从执行的结果来看,其实并没有达到并发的效果,而是一个协助循环打印完毕,才进行下一个协程的循环进行打印。
为什么没有达到并发的效果呢?
主要的原因是没有使用上gevent的sleep方法,进行耗时执行的切换。

gevent切换执行

#coding=utf-8

import gevent

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

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

在循环打印了一次之后,立即使用gevent.sleep方法,此时就会立即切换到另一个gevent进行执行,类似切换的操作。
执行效果如下:

[root@server01 work]# python test1.py 
(<Greenlet "Greenlet-0" at 0x7f0db293bcb0: f(5)>, 0)
(<Greenlet "Greenlet-1" at 0x7f0db293bdb8: f(5)>, 0)
(<Greenlet "Greenlet-2" at 0x7f0db293bec0: f(5)>, 0)
(<Greenlet "Greenlet-0" at 0x7f0db293bcb0: f(5)>, 1)
(<Greenlet "Greenlet-1" at 0x7f0db293bdb8: f(5)>, 1)
(<Greenlet "Greenlet-2" at 0x7f0db293bec0: f(5)>, 1)
(<Greenlet "Greenlet-0" at 0x7f0db293bcb0: f(5)>, 2)
(<Greenlet "Greenlet-1" at 0x7f0db293bdb8: f(5)>, 2)
(<Greenlet "Greenlet-2" at 0x7f0db293bec0: f(5)>, 2)
(<Greenlet "Greenlet-0" at 0x7f0db293bcb0: f(5)>, 3)
(<Greenlet "Greenlet-1" at 0x7f0db293bdb8: f(5)>, 3)
(<Greenlet "Greenlet-2" at 0x7f0db293bec0: f(5)>, 3)
(<Greenlet "Greenlet-0" at 0x7f0db293bcb0: f(5)>, 4)
(<Greenlet "Greenlet-1" at 0x7f0db293bdb8: f(5)>, 4)
(<Greenlet "Greenlet-2" at 0x7f0db293bec0: f(5)>, 4)
[root@server01 work]# 

从上面的结果来看,已经达到了并发切换协程的效果。
那么,如果这种耗时的操作如果都要改写为gevent的特定方法,那不就是要将以前写过的类型time.sleep方法全部改写才行?
这样相当耗时耗力,那么有没有上面好方法呢?
下面介绍采用打补丁的方式来处理。

给程序打补丁

from gevent import monkey
monkey.patch_all()

#coding=utf-8

import gevent
import time
from gevent import monkey

monkey.patch_all() # 将程序中用到的耗时操作代码,换为gevent中自己实现的模块

def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        #gevent.sleep(0.5)
        time.sleep(0.5)

# 批量创建协程
gevent.joinall([
    gevent.spawn(f, 5),
    gevent.spawn(f, 5),
    gevent.spawn(f, 5)
])

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

执行如下:

[root@server01 work]# python test1.py 
(<Greenlet "Greenlet-0" at 0x7fa2410a8ba8: f(5)>, 0)
(<Greenlet "Greenlet-1" at 0x7fa2410a8cb0: f(5)>, 0)
(<Greenlet "Greenlet-2" at 0x7fa2410a8db8: f(5)>, 0)
(<Greenlet "Greenlet-0" at 0x7fa2410a8ba8: f(5)>, 1)
(<Greenlet "Greenlet-1" at 0x7fa2410a8cb0: f(5)>, 1)
(<Greenlet "Greenlet-2" at 0x7fa2410a8db8: f(5)>, 1)
(<Greenlet "Greenlet-0" at 0x7fa2410a8ba8: f(5)>, 2)
(<Greenlet "Greenlet-1" at 0x7fa2410a8cb0: f(5)>, 2)
(<Greenlet "Greenlet-2" at 0x7fa2410a8db8: f(5)>, 2)
(<Greenlet "Greenlet-0" at 0x7fa2410a8ba8: f(5)>, 3)
(<Greenlet "Greenlet-1" at 0x7fa2410a8cb0: f(5)>, 3)
(<Greenlet "Greenlet-2" at 0x7fa2410a8db8: f(5)>, 3)
(<Greenlet "Greenlet-0" at 0x7fa2410a8ba8: f(5)>, 4)
(<Greenlet "Greenlet-1" at 0x7fa2410a8cb0: f(5)>, 4)
(<Greenlet "Greenlet-2" at 0x7fa2410a8db8: f(5)>, 4)
[root@server01 work]# 

这样就可以省去很多改写代码的工作了。

关注微信公众号,回复【资料】、Python、PHP、JAVA、web,则可获得Python、PHP、JAVA、前端等视频资料。

上一篇下一篇

猜你喜欢

热点阅读