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

Python协程中的await机制与异步爬虫实现
文章详细解释了Python协程中使用await关键字等待I/O操作的原理,如何避免线程阻塞,提高并发性能。通过一个简单的异步爬虫例子,展示了如何利用asyncio和aiohttp库创建并发爬虫,以及如何引入线程池提升效率。

在 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() 函数将网页内容获取和数据提取操作放到异步的线程池中运行,提高了程序的并发度和效率。最后,等待所有的队列都完成,并将取消所有的任务。

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

### 区别 - **应用场景**:C# 协程通常用于 Unity 游戏开发,用于实现一些需要分步执行的逻辑,如动画控制、资源加载等。而 `async/await` 是 C# 语言层面的异步编程特性,适用于各种类型的应用程序,用于处理耗时的 I/O 操作或 CPU 密集型任务,以提高程序的并发性和响应性 [^1]。 - **实现原理**:协程基于 C# 的迭代器(`IEnumerator`)实现,本质上是单线程模式,通过分帧执行来实现伪异步,不会阻塞主线程。`async/await` 则是基于任务(`Task`)实现的,它利用线程池来执行异步操作,在等待异步操作完成时会释放当前线程,从而允许其他任务使用该线程 [^1][^2]。 - **语法结构**:协程函数需要返回 `IEnumerator` 类型,并且使用 `yield return` 语句来暂停协程的执行。`async/await` 则使用 `async` 关键字标异步方法,使用 `await` 关键字等待异步操作完成,`await` 只能在 `async` 方法内部使用 [^1]。 ### 联系 - **目标相同**:C# 协程和 `async/await` 都旨在避免阻塞主线程,提高程序的响应性和性能,让程序在执行耗时操作时能够继续处理其他任务。 - **可结合使用**:在某些情况下,可以将协程和 `async/await` 结合使用。例如,在 Unity 中,可以在协程中调用 `async` 方法,或者在 `async` 方法中启动协程。 以下是协程和 `async/await` 的代码示例: #### 协程示例 ```csharp using UnityEngine; public class CoroutineExample : MonoBehaviour { void Start() { StartCoroutine(TestWaitForSeconds()); } IEnumerator TestWaitForSeconds() { yield return new WaitForSeconds(3.0f); Debug.Log("启动协程 3s 后"); } } ``` #### `async/await` 示例 ```csharp using System; using System.Threading.Tasks; class Program { static async Task Main() { await TestAsync(); Console.WriteLine("异步操作完成"); } static async Task TestAsync() { await Task.Delay(3000); Console.WriteLine("等待 3s 后"); } } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值