1 协程 Coroutine


协程不是计算机提供的,是程序员认为创造的。也可以称为微线程,是一种用户态的上下文切换技术。
简而言之,其实就是通过一个线程实现代码块相互切换执行。

  1. def func1():
  2. print(1)
  3. ...
  4. print(2)
  5. def func2():
  6. print(3)
  7. ...
  8. print(4)
  9. func1()
  10. func2()
  11. 1
  12. 2
  13. 3
  14. 4

上述代码是普通的函数定义和执行,按流程分别执行两个函数中的代码,并先后会输出:1、2、3、4。但如果介入协程技术那么就可以实现函数见代码切换执行,最终输入:1、3、2、4

在python中有很多种方法用来实现协程:

  • greenlet ,是一个第三方模块,用于实现协程代码(Gevent协程就是基于greenlet实现
  • yeild 生成器,借助生成器的特点也可以实现协程代码
  • asyncio ,在python3.4中引入该装饰器编写协程代码
  • async** await 在python3.5中引入的两个关键字,结合 asyncio 模块可以更方便编写协程代码。**

**

1.1 greenlet实现协程


greentlet是一个第三方模块,需要提前安装 pip3 install greenlet才能使用。

import greenlet
def func1(num):
    print(num)
    num+=1       # 第1步:输出 1
    gr2.switch(num)    # 第3步:切换到 func2 函数
    print(num)  # 第6步:输出 2
    num+=1
    gr2.switch(num)    # 第7步:切换到 func2 函数,从上一次执行的位置继续向后执行


def func2(num):
    print(num)
    num+=1        # 第4步:输出 3
    gr1.switch(num)    # 第5步:切换到 func1 函数,从上一次执行的位置继续向后执行
    print(num)        # 第8步:输出 4


gr1 = greenlet.greenlet(func1)
gr2 = greenlet.greenlet(func2)
gr1.switch(1)  # 第1步:去执行 func1 函数

1
2
2
3

注意:switch中也可以传递参数用于在切换执行时相互传递值。

1.2 yield 关键字


def func1():
    yield 1
    yield from func2()
    yield 2

def func2():
    yield 3
    yield 4

f1 = func1()
for item in f1:
    print(item)

1
3
4
2

1.3 asyncio


在Python3.4之前官方未提供协程的类库,一般大家都是使用greenlet等其他来实现。在Python3.4发布后官方正式支持协程,即:asyncio模块。
Python3.8之后 [@asyncio](https://github.com/asyncio).coroutine 装饰器就会被移除,推荐使用 async & awit 关键字实现协程代码

import asyncio

@asyncio.corountine
def func1():
    print(1)
    # 遇到IO耗时操作,自动切换到task中其中的其他任务
    # 比如 网络IO请求:下载一张图片
    yield from asyncio.sleep(2)
    print(2)

@asyncio.coroutine
def func2():
    print(3)
    # 网络IO请求:下载一张图片
    yield from asyncio.sleep(2)
    print(4)

 tasks = [
     asyncio.ensure_furture(func1()),
     asyncio.ensure_furture(func2())
 ]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(task))
DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
  def fun2():
1
3
2
4

遇到IO阻塞自动切换

1.4 async & await关键字

async & awit 关键字在Python3.5版本中正式引入,基于他编写的协程代码其实就是 上一示例 的加强版,让代码可以更加简便。

import asyncio

aynsc def func1():
    print(1)
    await asycio.sleep(2)
    print(2)

async def func2():
    print(3)
    await asyncio.sleep(2)
    print(4)

task = [
    asyncio.ensure_future(func1()),
    asyncio.ensure_future(func2())
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(task))

2 协程的意义


协程可以通过一个线程在多个上下文进行来回切换执行。

  1. 计算型的操作,利用协程来回切换执行,没有任何意义,来回切换并保存状态,反倒会降低性能
  2. IO型的操作,利用协程在IO等待时间就去切换执行其他任务,当IO操作结束后就自动回调,那么就会大大节省资源并提供性能,从而实现异步编程(不等待任务结束就可以去执行其他代码)

2.1 爬虫案例

方法一:同步爬虫

方式一:同步编程实现

"""
下载图片使用第三方模块requests,请提前安装:pip3 install requests
"""
import requests
def download_image(url):
    print("开始下载:",url)
    # 发送网络请求,下载图片
    response = requests.get(url)
    print("下载完成")
    # 图片保存到本地文件
    file_name = url.rsplit('_')[-1]
    with open(file_name, mode='wb') as file_object:
        file_object.write(response.content)
if __name__ == '__main__':
    url_list = [
        'https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg',
        'https://www2.autoimg.cn/newsdfs/g30/M01/3C/E2/120x90_0_autohomecar__ChcCSV2BBICAUntfAADjJFd6800429.jpg',
        'https://www3.autoimg.cn/newsdfs/g26/M0B/3C/65/120x90_0_autohomecar__ChcCP12BFCmAIO83AAGq7vK0sGY193.jpg'
    ]
    for item in url_list:
        download_image(item)

方法二:异步爬虫

import asyncio
import requests
import aiohttp


async def fetch(session, url):
    print('send request...', url)
    # 发送网络请求下载
    async with session.get(url, verity_ssl=False) as response:
        content = await response.content.read()
        file_name = url.rsplit('_')[-1]
        with open(fiel_name, mode='wb') as f:
            f.write(content)
async def main():
    async with aiohttp.ClientSession() as session:
        url_list = [
            'https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg',
            'https://www2.autoimg.cn/newsdfs/g30/M01/3C/E2/120x90_0_autohomecar__ChcCSV2BBICAUntfAADjJFd6800429.jpg',
            'https://www3.autoimg.cn/newsdfs/g26/M0B/3C/65/120x90_0_autohomecar__ChcCP12BFCmAIO83AAGq7vK0sGY193.jpg'
        ]
        tasks = [
            asyncio.create_task(fetch(session,url)) for url in url_list
        ]
        await asyncio.wait(tasks)

if __name__ == "__main__":
    asyncio.run(main())