【碎片记】协程中使用 await 操作来等待 I/O 操作完成,原理是什么?

在 Python 的协程中使用 `await` 来等待 I/O 操作的完成,其原理是协程在 I/O 操作执行期间将会被挂起,阻塞占用的 CPU 资源,直到 I/O 操作完成后再从挂起的地方恢复协程的执行。

在协程中使用 `await` 操作会将协程从事件循环中移除,让出 CPU 资源,等待 I/O 操作完成后再重新加入到事件循环中。当调用 `await` 操作时,协程会自动创建一个 `Future` 对象,并将其返回。这个 `Future` 对象的状态是 `PENDING`,表示当前操作正在等待处理。当 I/O 操作完成后,操作系统会向 Python 解释器发送一个通知,然后通过事件循环将 `Future` 对象状态设置为 `DONE`,表示操作已完成。这会唤醒挂起的协程并让其继续执行。

在协程中使用 `await` 操作来等待 I/O 操作的完成,避免了线程阻塞的问题,提高了 I/O 操作的效率和并发性能,让程序能够更好地利用 CPU 资源。同时,协程和事件循环的机制也能够让程序的编写和维护更加简单明了,避免了复杂的线程同步和锁机制,也让代码的可读性和可维护性更高。

简单爬虫代码案例:

import asyncio
import aiohttp
from asyncio.queues import Queue

# 声明一些常量
MAX_TASKS = 100
TIMEOUT = 10

class Crawler:
    def __init__(self):
        self._seen_urls = set()
        self._queue = Queue()
        self._session = None

    async def crawl(self, urls):
        tasks = []

        # 创建链接会话
        self._session = aiohttp.ClientSession()

        # 初始化队列
        for url in urls:
            await self._queue.put(url)

        # 创建任务
        for i in range(MAX_TASKS):
            task = asyncio.create_task(self._worker())
            tasks.append(task)

        # 等待任务完成
        await self._queue.join()

        # 取消任务
        for task in tasks:
            task.cancel()

        # 关闭链接会话
        await self._session.close()

    async def _fetch(self, url):
        try:
            async with self._session.get(url, timeout=TIMEOUT) as response:
                if response.status == 200:
                    content = await response.text()
                    self._seen_urls.add(url)
                    return content
        except Exception as e:
            print(f"Error fetching {url}: {e}")
            return None

    async def _worker(self):
        while True:
            # 获取并处理队列中的 URL
            url = await self._queue.get()
            # 如果 URL 已经被处理过了,就跳过
            if url in self._seen_urls:
                self._queue.task_done()
                continue
            # 抓取 URL
            html = await self._fetch(url)
            if html is not None:
                # 处理 HTML,提取所需的数据
                # ...
                # 将新发现的 URL 放入队列
                urls = self._extract_links(html)
                for new_url in urls:
                    await self._queue.put(new_url)
            # 标记这个 URL 已经处理过了
            self._queue.task_done()

    def _extract_links(self, html):
        # 从 HTML 中提取链接
        # ...
        return []

该示例程序通过使用 asyncio 和 aiohttp,实现了异步并发的爬虫功能,并且使用了队列来管理已访问的 URL,以避免重复访问。程序中的 Crawler 类定义了以下方法:

  • __init__(): 构造函数,初始化一些数据结构
  • crawl(urls): 爬虫程序的入口函数,接收一个 URL 列表并开始爬取
  • _fetch(url): 异步获取 URL 对应的网页内容
  • _worker(): 异步处理队列中的 URL,并从其中提取新的 URL 放入队列
  • _extract_links(html): 从 HTML 中提取链接

程序首先通过创建 Queue 对象创建了一个队列,然后将要访问的 URL 添加到了队列中。程序同时控制了最大的并行任务数,防止太多的并发 HTTP 请求导致网站负载暴涨。接着,程序使用了多个协程( _worker() 函数)并行地从队列中获取 URL,进行访问和处理。每个协程会从队列中获取一个 URL,如果这个 URL 已经被访问过就跳过,否则就使用 _fetch() 函数异步获取网页内容,提取所需的数据,并将新的 URL 放入队列中。当队列为空时,协程退出。最后,等待所有的队列都完成,并将取消所有的任务。

线程池版本:

import asyncio
import aiohttp
import concurrent.futures
from asyncio.queues import Queue

# 声明一些常量
MAX_TASKS = 100
MAX_THREADS = 10
TIMEOUT = 10

class Crawler:
    def __init__(self):
        self._seen_urls = set()
        self._queue = Queue()
        self._session = None        

    async def crawl(self, urls):
        tasks = []

        # 创建链接会话
        self._session = aiohttp.ClientSession()

        # 初始化队列
        for url in urls:
            await self._queue.put(url)

        # 创建任务
        for i in range(MAX_TASKS):
            task = asyncio.create_task(self._worker())
            tasks.append(task)

        # 设置线程池
        executor = concurrent.futures.ThreadPoolExecutor(max_workers=MAX_THREADS)

        # 等待任务完成
        await self._queue.join()

        # 取消任务
        for task in tasks:
            task.cancel()

        # 关闭链接会话
        await self._session.close()

    async def _fetch(self, url):
        try:
            async with self._session.get(url, timeout=TIMEOUT) as response:
                if response.status == 200:
                    content = await response.text()
                    self._seen_urls.add(url)
                    return content
        except Exception as e:
            print(f"Error fetching {url}: {e}")
            return None

    async def _worker(self):
        loop = asyncio.get_running_loop()
        while True:
            # 获取并处理队列中的 URL
            url = await self._queue.get()
            # 如果 URL 已经被处理过了,就跳过
            if url in self._seen_urls:
                self._queue.task_done()
                continue
            # 抓取 URL
            html = await loop.run_in_executor(None, self._fetch, url)
            if html is not None:
                # 处理 HTML,提取所需的数据
                # ...
                # 将新发现的 URL 放入队列
                urls = self._extract_links(html)
                for new_url in urls:
                    await self._queue.put(new_url)
            # 标记这个 URL 已经处理过了
            self._queue.task_done()

    def _extract_links(self, html):
        # 从 HTML 中提取链接
        # ...
        return []

该示例程序通过使用 asyncio 和 aiohttp 实现了异步并发的爬虫功能,并且使用了队列来管理已访问的 URL,以避免重复访问。同时,程序也使用了线程池,将 HTTP 请求和处理操作转移到了单独的线程中,并且使用 run_in_executor() 函数来执行。

除了之前提到的常量之外,程序中新增了一个常量 MAX_THREADS,它指定了线程池的最大大小。程序同样通过创建 Queue 对象创建了一个队列,然后将要访问的 URL 添加到了队列中。接着创建了多个协程( _worker() 函数)并行地从队列中获取 URL,使用 run_in_executor() 函数将网页内容获取和数据提取操作放到异步的线程池中运行,提高了程序的并发度和效率。最后,等待所有的队列都完成,并将取消所有的任务。

哇哦~~ 知识又增加了~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值