测试开发之Python核心笔记(26): 协程

协程是实现并发编程的一种方式。 Python 3.7 以上版本中,使用协程写异步程序非常简单。

26.1 同步与异步

我们首先来区分一下 Sync(同步)和 Async(异步)的概念。

所谓 Sync,是指操作一个接一个地执行,下一个操作必须等上一个操作完成后才能执行。

而 Async 是指不同操作间可以相互交替执行,如果其中的某个操作被 block 了,程序并不会等待,而是会找出可执行的操作继续执行。从而可以提高效率。

26.2 Asyncio原理

Python使用Asyncio库进行异步编程。Asyncio可以在一个主线程中,交替进行多个不同的任务(task),这里的任务,就是特殊的 future 对象。这些不同的任务,被一个叫做 event loop 的对象所控制。

  • 不同task可以相互交替执行,如果其中的task被 block 了,程序并不会等待,event_loop会找出可执行的task继续执行。
  • task有ready状态,和wait状态,当task处于wait状态时,event_loop就切换到其他可以执行状态的task去执行。

下面看看Asyncio切换task的细节。

task有两个关键的状态:一是ready状态;二是wait状态。ready状态,是task随时待命准备运行。而wait状态,是指任务已经运行,但是处于等待I/O操作完成的状态。

event loop 会维护两个任务列表,分别存放ready状态的task和wait状态的task。event loop从ready状态的task列表中,选取一个任务,使其运行,一直到这个任务把控制权交还给 event loop 为止。

当任务把控制权交还给 event loop 时,event loop 会根据其是否完成,把任务放到ready状态列表或wait状态列表,然后遍历wait状态列表中的任务,查看他们是否完成。

如果完成,则将其放到ready状态的列表;如果未完成,则继续放在wait状态的列表。

这样,当所有任务被重新放置在合适的列表后,新一轮的循环又开始了:event loop 继续从ready状态的列表中选取一个任务使其执行…如此周而复始,直到所有任务完成。

对于 Asyncio 来说,它的任务在运行时不会被外部的一些因素打断,因此 Asyncio 内的操作不会出现 race condition 的情况,不需要担心线程安全的问题。

26.3 协程执行过程(跳来跳去)

import asyncio
import time


async def worker_1():  # def前面加一个async表示是个异步函数
    print('worker_1 start')
    await asyncio.sleep(1)  # ⑤ 遇到 await,事件调度器切断当前任务执行
    print('worker_1 done')  # ⑧ worker_1彻底执行完毕,调度器去调度worker_2


async def worker_2():
    print('worker_2 start')
    await asyncio.sleep(2)  # ⑦ 遇到 await,事件调度器切断当前任务执行,去调度worker_1
    print('worker_2 done')  # ⑨ 【1】秒钟后,worker_1彻底执行完毕,等切回到worker_2时在等1秒就好了

async def main():
    task1 = asyncio.create_task(worker_1())  # ② 创建两个task,立即进入事件循环等待被调度
    task2 = asyncio.create_task(worker_2())
    print('before await')  # ③ 执行到此
    await task1  # ④ 从当前的主任务中切出,事件调度器开始调度worker_1
    print('awaited worker_1')
    await task2  # ⑥ 事件调度器开始调度worker_2
    print('awaited worker_2')  # ⑩ 主任务输出 'awaited worker_2',协程全任务结束,事件循环结束


start = time.time()
asyncio.run(main())  # ①程序进入 main() 函数,事件循环开启;
print("Escape {}".format(time.time() - start))  # 一共花2秒

26.4 编码套路

import time

import asyncio  # ① 首先 import asyncio


async def crawl_page(url):  # ② async 声明异步函数
    print('crawling {}'.format(url))
    sleep_time = int(url.split('_')[-1])
    await asyncio.sleep(sleep_time)  # ③ await进入被调用的协程函数,执行完毕返回后再继续,会阻塞
    print('OK {}'.format(url))
    return sleep_time

async def main(urls):
    tasks = []
    for url in urls:
        coroutine_object = crawl_page(url)  # ④调用异步函数crawl_page并不会执行,而是会得到一个协程对象coroutine_object
        tasks.append(asyncio.create_task(coroutine_object))  # ⑤create_task将协程对象制作成任务,并立即执行
    res = await asyncio.gather(*tasks,
                               return_exceptions=True)  # ⑤阻塞在这,等所有任务都结束,return_exceptions=True可以避免个别协程对象执行时出错影响大局
    return res  # ⑥ 返回每一个协程对象的执行结果


if __name__ == '__main__':
    start = time.time()
    result = asyncio.run(main(['url_1', 'url_2', 'url_3', 'url_4']))  # asyncio.run执行协程
    print("Escape {}".format(time.time() - start))
    print(result)

26.5 举例应用

asyncio.queue模块则实现了面向多生产协程、多消费协程的队列。

26.5.1 生产者消费者模型

import asyncio
import random


async def consumer(queue, ids):
    while True:
        value = await queue.get()
        print('{} get a val: {}'.format(ids, value))
        await asyncio.sleep(1)


async def producer(queue, ids):
    for i in range(5):
        val = random.randint(1, 10)
        await queue.put(val)
        print('{} put a val: {}'.format(ids, val))
        await asyncio.sleep(1)


async def main():
    queue = asyncio.Queue() 
    consumer1 = asyncio.create_task(consumer(queue, "consumer1"))
    consumer2 = asyncio.create_task(consumer(queue, "consumer2"))
    producer1 = asyncio.create_task(producer(queue, "producer1"))
    producer2 = asyncio.create_task(producer(queue, "producer2"))

    await asyncio.sleep(10)
    consumer2.cancel()
    consumer1.cancel()
    await asyncio.gather(consumer1, consumer2, producer1, producer2, return_exceptions=True)

asyncio.run(main())

26.5.2 并发爬虫

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']))


asyncio.run(main())

26.6 兼容性问题

  • 要使用与asynicio兼容的库
  • http的cleint要用 aiohttp 库,原因就是 requests 库并不兼容 asyncio,但是 aiohttp 库兼容。

26.7 如何选择用多进程、多线程还是协程呢?

  • 如果是 I/O bound,并且 I/O 操作很慢,需要很多任务 / 线程协同实现,那么使用 asyncio 更合适。
  • 如果是 I/O bound,但是 I/O 操作很快,只需要有限数量的任务 / 线程,那么使用多线程就可以了。
  • 如果是 CPU bound,则需要使用多进程来提高程序运行效率。

猜你喜欢

转载自blog.csdn.net/liuchunming033/article/details/107963165
今日推荐