Python之路(第四十七篇) 协程

 

一、协程介绍

协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。

协程相比于线程,最大的区别在于,协程不需要像线程那样来回的中断切换,也不需要线程的锁机制,因为线程中断或者锁机制都会对性能问题造成影响,所以协程的性能相比于线程,性能有明显的提高,尤其在线程越多的时候,优势越明显。

协程的好处:

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

  2. 无需原子操作锁定及同步的开销 "原子操作(atomic operation)是不需要synchronized",所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。视作整体是原子性的核心。

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

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

缺点:

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

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

总结协程特点:

  1. 必须在只有一个单线程里实现并发

  2. 修改共享数据不需加锁

  3. 用户程序里自己保存多个控制流的上下文栈

  4. 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))

Python2.x协程

类库:

  • yield

  • greenlet

  • gevent

Python3.x协程

  • asyncio

Python3.x系列的gevent用法和python2.x系列是一样的

在学习前,我们先来理清楚同步/异步的概念

·同步是指完成事务的逻辑,先执行第一个事务,如果阻塞了,会一直等待,直到这个事务完成,再执行第二个事务,顺序执行。。。也称作串行执行。

·异步是和同步相对的,异步是指在处理调用这个事务的之后,不会等待这个事务的处理结果,直接处理第二个事务去了,通过状态、通知、回调来通知调用者处理结果。也称作并行执行。

二、greenlet模块

第三方模块,可以在pycharm中选择虚拟环境安装,

也可以通过 pip install greenlet 安装

greenlet 通过 greenlet(func) 启动一个协程,通过 switch() 手动切换程序的执行

示例

from greenlet import greenlet
?
def func1(name):
    print("%s from func1"%name) #2执行这一句
    g2.switch("jack")  #3切换执行func2(),第一次执行要传入参数保存现在执行的状态
    print("from func1 end") #6执行这一句
    g2.switch()#7切换执行play(),保存现在执行的状态
?
def func2(name):
    print("%s from func2"%name) #4执行这一句
    g1.switch() #5切换执行func1(),保存现在执行的状态
    print("from func2 end") #8执行这一句
?
g1 = greenlet(func1)
g2 = greenlet(func2)
g1.switch("nick") #1执行func1(),在switch()里传参数 ,注意与一般的线程、进程传参方式的不同
#可以在第一次switch时传入参数,以后都不需要

??

分析:就是通过创建greenlet(func)对象,通过对象的switch()方法转移程序执行的不同步骤,但是这里无法自动识别IO后自动切换。

三、gevent模块

gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是greenlet, 它是以C扩展模块形式接入Python的轻量级协程。

安装 pip3 install gevent 或者在pycharm中选择虚拟环境安装

用法

#用法
g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如func1,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数func1的
?
g2=gevent.spawn(func2)
?
g1.join() #等待g1结束
?
g2.join() #等待g2结束
?
#或者上述两步合作一步:gevent.joinall([g1,g2])
?
g1.value#拿到func1的返回值

示例

import gevent
?
?
def func1():
    print(‘from func1: 1‘)
    gevent.sleep(0)
    print(‘from func1: 2‘)
    gevent.sleep(1)
?
?
def func2():
    print(‘from func2: 1‘)
    gevent.sleep(2)
    print(‘from func2: 2‘)
?
?
def func3():
    print(‘from func3: 1‘)
    gevent.sleep(1)
    print(‘from func3: 2‘)
?
?
gevent.joinall([
    gevent.spawn(func1),
    gevent.spawn(func2),
    gevent.spawn(func3),
])
?

输出结果

from func1: 1
from func2: 1
from func3: 1
from func1: 2
from func3: 2
from func2: 2

?

分析:可以从输出结果看到程序不断的在三个函数中跳跃执行,遇到IO了就去执行另外的函数,但是请注意一点

gevent.sleep() 是用于模仿 IO 操作的,实际使用中不需要 gevent.sleep(),这里如果单纯执行上述代码的话,gevent模块也是只能识别 gevent.sleep()产生的IO,而对系统产生的IO或者网络IO之类无法识别,所有需要打上补丁,使得gevent模块识别其他IO

gevent是不能直接识别的需要用下面一行代码,打补丁

要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头

示例

需求:爬取三个网站并打印网页字符串长度

?
from gevent import monkey;monkey.patch_all()
# 把当前程序的所有 IO 操作标记起来,否则模块无法知道 IO 操作
import gevent
import time
import requests
?
?
def get_page(url):
    headers = {
        ‘User-Agent‘: ‘Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36‘
    }
    page_text = requests.get(url=url, headers=headers).text
    print(‘网站长度‘, len(page_text))
?
?
def main():
    urls = [
        ‘https://www.sogou.com‘,
        ‘https://cn.bing.com‘,
        ‘https://cnblogs.com/Nicholas0707/‘,
    ]
    time_start = time.time()
    for url in urls:
        get_page(url)
?
    print(‘同步耗时:‘, time.time() - time_start)
?
    print("-"*50)
    async_time_start = time.time()
    gevent.joinall([
        gevent.spawn(get_page, ‘https://www.sogou.com‘),
        gevent.spawn(get_page, ‘https://cn.bing.com‘),
        gevent.spawn(get_page, ‘https://cnblogs.com/Nicholas0707/‘),
    ])
    print(‘异步协程耗时:‘, time.time() - async_time_start)
?
?
if __name__ == ‘__main__‘:
    main()

输出结果

网站长度 23795
网站长度 130248
网站长度 13761
同步耗时: 2.5321450233459473
--------------------------------------------------
网站长度 23795
网站长度 130221
网站长度 13761
异步协程耗时: 0.36602067947387695
?

分析:从结果可以看出采用协程异步明显更快

四、asyncio模块

asyncio是Python3.4(2014年)引进的标准库,直接内置了对IO的支持。

python2x没有加这个库,python3.5又加入了async/await特性。

示例

未完待续。。。

相关推荐