python中的协程

目录

什么是协程?

asyncio模块

举个例子

解密协程运行时


协程是实现并发编程的一种形式。

说起并发编程就容易想到多进程/线程编程,最初的互联网中,多进程/线程在服务器并发中起到了举足轻重的作用。

随着互联网的快速发展,当同一时间连接到服务器的客户量达到一万,也就是C10K瓶颈。于是很多代码跑崩了,进程的上下文切换占用了大量的资源,线程也顶不住这么大的压力。于是NGINX站了出来,它带来的循环事件开始拯救世界。

事件循环启动一个统一的调度器,让调度器来决定一个时刻去运行哪个任务,于是省却了多线程中启动线程、管理线程、同步锁等各种开销。同一时期的 NGINX,在高并发下能保持低资源低消耗高性能,相比 Apache 也支持更多的并发连接。


什么是协程?

协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

  • 最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
  • 第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

asyncio模块

https://docs.python.org/zh-cn/3.7/library/asyncio.html

Python对协程的支持还非常有限,用在generator中的yield可以一定程度上实现协程。(yield和yield from 的用法参考https://www.cnblogs.com/gqtcgq/p/8126124.html

使用生成器,是 Python 2 开头的时代实现协程的老方法了,python也是在python 3.4中引入了协程的概念,Python 3.7 提供了新的基于asyncio 和 async / await 的方法。

asyncio 是干什么的?

  • 异步网络操作
  • 并发
  • 协程

python3.0时代,标准库里的异步网络模块:select(非常底层) python3.0时代,第三方异步网络库:Tornado python3.4时代,asyncio:支持TCP,子进程

现在的asyncio,有了很多的模块已经在支持:aiohttp,aiodns,aioredis等等 https://github.com/aio-libs 这里列出了已经支持的内容,并在持续更新

当然到目前为止实现协程的不仅仅只有asyncio,tornado和gevent都实现了类似功能

关于asyncio的一些关键字的说明:

  • event_loop 事件循环:程序开启一个无限循环,把一些函数注册到事件循环上,当满足事件发生的时候,调用相应的协程函数

  • coroutine 协程:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。

  • task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含了任务的各种状态

  • future: 代表将来执行或没有执行的任务的结果。它和task上没有本质上的区别

  • async/await 关键字:python3.5用于定义协程的关键字,async定义一个协程,await用于挂起阻塞的异步调用接口。

举个例子

举一个非常简单的爬虫

import time
now = lambda :time.perf_counter()

def crawl_page(url):
    print('crawling {}'.format(url))
    sleep_time = int(url.split('_')[-1])
    time.sleep(sleep_time)
    print('OK {}'.format(url))

def main(urls):
    for url in urls:
        crawl_page(url)

start = now()
main(['url_1', 'url_2', 'url_3', 'url_4'])
print(now()-start)

#输出
#crawling url_1
#OK url_1
#crawling url_2
#OK url_2
#crawling url_3
#OK url_3
#crawling url_4
#OK url_4
#9.999895046537901

(注意:我们简化爬虫的scrawl_page 函数为休眠数秒,休眠时间取决于 url 最后的那个数字。) 

五个页面分别用了 1 秒到 4 秒的时间,加起来一共用了 10 秒。这显然效率低下,该怎么优化呢?

于是,一个很简单的思路出现了——我们这种爬取操作,完全可以并发化。我们就来看看使用协程怎么写。

import asyncio
import time
now = lambda :time.perf_counter()

async def crawl_page(url):  #async定义一个协程,async修饰词声明异步函数,这里的crawl_page变成了异步函数
    print('crawling {}'.format(url))
    sleep_time = int(url.split('_')[-1])
    await asyncio.sleep(sleep_time)   #await用于挂起阻塞的异步调用接口
    print('OK {}'.format(url))

async def main(urls):
    for url in urls:
        await crawl_page(url)

start = now()
#在python 3.5 版本中
#调用异步函数便可得到一个协程对象(coroutine object),这里是一个协程对象,这个时候main函数并没有执行
coroutine = main(['url_1', 'url_2', 'url_3', 'url_4'])
#通过async关键字定义一个协程,当然协程不能直接运行,需要将协程加入到循环事件loop中
loop = asyncio.get_event_loop()  #创建一个事件循环
loop.run_until_complete(coroutine)  #将协程注册到事件循环,并启动事件循环,此时会把协程包装成task
#在python 3.7 及以上版本中加入了asyncio.run用法,更简洁。效果和上面几行一样
#asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))
print(now()-start)


#输出
#crawling url_1
#OK url_1
#crawling url_2
#OK url_2
#crawling url_3
#OK url_3
#crawling url_4
#OK url_4
#10.00548147187418

print(crawl_page(''))   #这是一个 Python 的协程对象,而并不会真正执行这个函数。
#输出
#<coroutine object crawl_page at 0x00000000033CA258>

首先来看 import asyncio,这个库包含了大部分我们实现协程所需的魔法工具。

async 修饰词声明异步函数,于是,这里的 crawl_page和main都变成了异步函数。而调用异步函数,我们便可得到一个协程对象(coroutine object)。

await 执行的效果,和 Python 正常执行是一样的,也就是说程序会阻塞在这里,进入被调用的协程函数,执行完毕返回后再继续,而这也是 await 的字面意思。代码中 await asyncio.sleep(sleep_time) 会在这里休息若干秒,await crawl_page(url)则会执行 crawl_page() 函数。

事件循环在线程中运行(通常是主线程),并在其线程中执行所有回调和任务。当一个任务在事件循环中运行时,没有其他任务可以在同一个线程中运行。当一个任务执行一个 await 表达式时,正在运行的任务被挂起,事件循环执行下一个任务。

asyncio.run 这个函数是 Python 3.7 之后才有的特性,可以让 Python的协程接口变得非常简单。一个非常好的编程规范是,asyncio.run(main()) 作为主程序的入口函数,在程序运行周期内,只调用一次 asyncio.run。

观察上面代码的运行结果,what,怎么还是 10 秒?

10 秒就对了,还记得上面所说的,await 是同步调用,因此,crawl_page(url) 在当前的调用结束之前,是不会触发下一次调用的。于是,这个代码效果就和上面完全一样了,相当于我们用异步接口写了个同步代码。

现在又该怎么办呢?

其实很简单,也正是我接下来要讲的协程中的一个重要概念,任务(Task)。

import asyncio
import time

now = lambda:time.perf_counter()

async def crawl_page(url):  #async定义一个协程,async修饰词声明异步函数,这里的crawl_page变成了异步函数
    print('crawling {}'.format(url))
    sleep_time = int(url.split('_')[-1])
    await asyncio.sleep(sleep_time)   #await用于挂起阻塞的异步调用接口
    print('OK {}'.format(url))


async def main(urls):
    #python 3.7及以上版本有asyncio.create_task
    tasks = [asyncio.create_task(crawl_page(url)) for url in urls]
    for task in tasks:
        await task

#在python 3.7 及以上版本中加入了asyncio.run用法
#asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))


start = now()
#在python 3.5 版本中
urls = ['url_1', 'url_2', 'url_3', 'url_4']
tasks = [asyncio.ensure_future(crawl_page(url)) for url in urls]
print(tasks)
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
print(tasks)
print(now()-start)


#输出
#[<Task pending coro=<crawl_page() running at E:/python_test/test_asyncio.py:6>>, <Task pending coro=<crawl_page() running at E:/python_test/test_asyncio.py:6>>, <Task pending coro=<crawl_page() running at E:/python_test/test_asyncio.py:6>>, <Task pending coro=<crawl_page() running at E:/python_test/test_asyncio.py:6>>]
#crawling url_1
#crawling url_2
#crawling url_3
#crawling url_4
#OK url_1
#OK url_2
#OK url_3
#OK url_4
#[<Task finished coro=<crawl_page() done, defined at E:/python_test/test_asyncio.py:6> result=None>, <Task finished coro=<crawl_page() done, defined at E:/python_test/test_asyncio.py:6> result=None>, <Task finished coro=<crawl_page() done, defined at E:/python_test/test_asyncio.py:6> result=None>, <Task finished coro=<crawl_page() done, defined at E:/python_test/test_asyncio.py:6> result=None>]
#4.00404582797189

(注意:task的输出信息中有task的状态,结果中可以看到两种状态,pending、finished)

你可以看到(在python3.7+版本),我们有了协程对象后,便可以通过 asyncio.create_task 来创建任务。任务创建后很快就会被调度执行,这样,我们的代码也不会阻塞在任务这里。所以,我们要等所有任务都结束才行,用for task in tasks: await task即可。python3.5版本也是如此。

其实,对于执行 tasks,还有另一种做法:

import asyncio

async def crawl_page(url):
    print('crawling {}'.format(url))
    sleep_time = int(url.split('_')[-1])
    await asyncio.sleep(sleep_time)
    print('OK {}'.format(url))

async def main(urls):
    tasks = [asyncio.create_task(crawl_page(url)) for url in urls]
    await asyncio.gather(*tasks)

asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))

(注意:*tasks 解包列表,将列表变成了函数的参数;与之对应的是, ** dict 将字典变成了函数的参数。)

asyncio.create_task,asyncio.run 这些函数都是 Python 3.7 以上的版本才提供的,自然,相比于旧接口它们也更容易理解和阅读。

解密协程运行时

先来看看单个task的例子:

import asyncio

async def compute(x, y):
    print("Compute %s + %s ..." % (x, y))
    await asyncio.sleep(1.0)
    return x + y

async def print_sum(x, y):
    result = await compute(x, y)
    print("%s + %s = %s" % (x, y, result))

#python 3.5 版本
loop = asyncio.get_event_loop()
loop.run_until_complete(print_sum(1, 2))
loop.close()

 示例的时序图 :

再来看看多个任务并发执行,并行执行3个任务(A、B、C)的示例:

import asyncio

@asyncio.coroutine
def factorial(name, number):
    f = 1
    for i in range(2, number+1):
        print("Task %s: Compute factorial(%s)..." % (name, i))
        yield from asyncio.sleep(1)
        f *= i
    print("Task %s: factorial(%s) = %s" % (name, number, f))

loop = asyncio.get_event_loop()
tasks = [
    asyncio.ensure_future(factorial("A", 2)),
    asyncio.ensure_future(factorial("B", 3)),
    asyncio.ensure_future(factorial("C", 4))]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

#输出
#Task A: Compute factorial(2)...
#Task B: Compute factorial(2)...
#Task C: Compute factorial(2)...
#Task A: factorial(2) = 2
#Task B: Compute factorial(3)...
#Task C: Compute factorial(3)...
#Task B: factorial(3) = 6
#Task C: Compute factorial(4)...
#Task C: factorial(4) = 24

任务在创建时自动计划执行。当所有任务完成时,事件循环停止。

实战

任务描述:https://movie.douban.com/cinema/later/beijing/这个页面描述了北京最近上映的电影,你能否通过 Python 得到这些电影的名称、上映时间和海报呢?这个页面的海报是缩小版的,我希望你能从具体的电影描述页面中抓取到海报。

同步版本:

import requests
from bs4 import BeautifulSoup
import time

now = lambda:time.perf_counter()


def main():
    url = "https://movie.douban.com/cinema/later/beijing/"
    init_page = requests.get(url).content
    init_soup = BeautifulSoup(init_page, 'lxml')

    all_movies = init_soup.find('div', id="showing-soon")
    for each_movie in all_movies.find_all('div', class_="item"):
        all_a_tag = each_movie.find_all('a')
        all_li_tag = each_movie.find_all('li')

        movie_name = all_a_tag[1].text
        url_to_fetch = all_a_tag[1]['href']
        movie_date = all_li_tag[0].text

        response_item = requests.get(url_to_fetch).content
        soup_item = BeautifulSoup(response_item, 'lxml')
        img_tag = soup_item.find('img')

        print('{} {} {}'.format(movie_name, movie_date, img_tag['src']))


start = now()
main()
print(now()-start)

#输出
#狮子王 07月12日 https://img3.doubanio.com/view/photo/s_ratio_poster/public/p2559742751.jpg
#命运之夜——天之杯II :迷失之蝶 07月12日 https://img3.doubanio.com/view/photo/s_ratio_poster/public/p2561910374.jpg
#素人特工 07月12日 https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2560447448.jpg
#机动战士高达NT 07月12日 https://img3.doubanio.com/view/photo/s_ratio_poster/public/p2558661806.jpg
#。。。
#33.97453982599911

异步版本:

#python 版本 >=3.7
import asyncio
import aiohttp

from bs4 import BeautifulSoup

async def fetch_content(url):
    async with aiohttp.ClientSession(
        headers=header, connector=aiohttp.TCPConnector(ssl=False)
    ) as session:
        async with session.get(url) as response:
            return await response.text()

async def main():
    url = "https://movie.douban.com/cinema/later/beijing/"
    init_page = await fetch_content(url)
    init_soup = BeautifulSoup(init_page, 'lxml')

    movie_names, urls_to_fetch, movie_dates = [], [], []

    all_movies = init_soup.find('div', id="showing-soon")
    for each_movie in all_movies.find_all('div', class_="item"):
        all_a_tag = each_movie.find_all('a')
        all_li_tag = each_movie.find_all('li')

        movie_names.append(all_a_tag[1].text)
        urls_to_fetch.append(all_a_tag[1]['href'])
        movie_dates.append(all_li_tag[0].text)

    tasks = [fetch_content(url) for url in urls_to_fetch]
    pages = await asyncio.gather(*tasks)

    for movie_name, movie_date, page in zip(movie_names, movie_dates, pages):
        soup_item = BeautifulSoup(page, 'lxml')
        img_tag = soup_item.find('img')

        print('{} {} {}'.format(movie_name, movie_date, img_tag['src']))

%time asyncio.run(main())

(注意:这里用的是aiohttp模块,并不是所有的模块都支持asyncio。现在有很多的模块已经在支持:aiohttp,aiodns,aioredis等等 https://github.com/aio-libs 这里列出了已经支持的内容,并在持续更新)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值