【python】协程

关于python协程这个知识点,我是在研究locust时候发现的。
locust是一款开源的性能测试工具,单机并发能力要比jmeter高,它的并发实现就是通过python协程去实现的。

说到并发,我猜你很容易想到的是多线程,其实协程也是实现并发的一种方式,只不过协程是单线程。

先上一段代码,假设我们在写爬虫抓取4个url的内容,并且我们让每一个url都停留一定的秒数,第一个1s,
第二个停2s,以此类推。所以,可以知道4个url爬取完毕一共要花费10s的时间。

import time


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


def main(urls):
    time_start = time.time()
    for url in urls:
        crawl_page(url)
    time_end = time.time()
    print("爬取耗时共计{}s".format(int(time_end - time_start)))


main(['url_1', 'url_2', 'url_3', 'url_4'])

运行一下:

D:\Programs\Python\Python39\python.exe D:/练习/xc.py
正在爬取 url_1
OK url_1
正在爬取 url_2
OK url_2
正在爬取 url_3
OK url_3
正在爬取 url_4
OK url_4
爬取耗时共计10s

Process finished with exit code 0

果然运行了10s,可以发现我们在等待中浪费了很多时间,那么如何提高这段代码的运行效率呢?
没错,协程该出场了。

一、async和await

目前,python3.7以上的版本提供了最新的写法,是基于asyncioasync / await 的方法,所以如果下面
的代码你执行不起来,检查一下是不是你的python版本不对。

import asyncio
import time


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


async def main(urls):
    time_start = time.time()
    for url in urls:
        await crawl_page(url)
    time_end = time.time()
    print("爬取耗时共计{}s".format(int(time_end - time_start)))


asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))
  • async:声明这是一个异步函数。调用异步函数,就会得到一个协程对象。
  • await:用于调用函数。注意,这里await 执行的效果,和 Python 正常执行是一样的。
    也就是说程序会阻塞在这里,进入被调用的协程函数,执行完毕返回后再继续。
  • asyncio.run:触发运行。这个函数是 Python 3.7 之后才有的特性,目的就是让Python 的协程接口变得非常简单。
    通常情况下,asyncio.run(main()) 作为主程序的入口函数,在程序运行周期内,只调用一次 asyncio.run

二、asyncio.create_task

上述代码其实还没完成,不信你执行一下,会发现还是10s。这正如上面await介绍说的,其实跟python正常执行一次没区别。
所以,为了实现协程,还需要asyncio.create_task来创建任务。

import asyncio
import time


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


async def main(urls):
    time_start = time.time()
    tasks = []
    for url in urls:
        tasks.append(asyncio.create_task(crawl_page(url)))  # 创建task
    for task in tasks:
        await task
    time_end = time.time()
    print("爬取耗时共计{}s".format(int(time_end - time_start)))


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

执行一下:

D:\Programs\Python\Python39\python.exe D:/练习/xc.py
正在爬取 url_1
正在爬取 url_2
正在爬取 url_3
正在爬取 url_4
OK url_1
OK url_2
OK url_3
OK url_4
爬取耗时共计4s

Process finished with exit code 0

这下仅需要4s,也就是4个任务中,耗时最长的那个任务所需要的时间。
执行过程:

  • 在第一个for循环里,通过调用异步函数crawl_page()得到4个协程对象。
  • 在第二个for循环里,通过asyncio.create_task给4个协程对象分别创建了task。
  • 接着,4个任务被await调用执行。

是不是很简单?再次印证了python哲学:简单胜过复杂。

最近可能会用到locust对项目进行性能测试,若有收获,届时分享。

posted @ 2021-04-20 00:15  把苹果咬哭的测试笔记  阅读(131)  评论(0编辑  收藏  举报