asyncio 并发编程

asyncio 并发编程

asyncio 是 Python 的一个库,用于编写单线程并发代码使用 async/await 语法。asyncio 提供了一种使用事件循环来编写并发代码的方式,这种方式非常适合于 I/O 密集型任务和协调多个异步操作的场景。

基本概念

  1. 协程 (Coroutine): 使用 async def 定义的函数。协程函数调用不会立即执行,而是返回一个协程对象,该对象需要被运行在事件循环中。
  2. 事件循环 (Event Loop): asyncio 的核心,用于管理和分发事件和任务。事件循环负责执行协程,以及处理异步 I/O, 延时调用等任务。
  3. 任务 (Task): 用于在事件循环中调度协程执行的未来式对象。通过 asyncio.create_task() 创建任务。

开始使用 asyncio

要使用 asyncio,首先需要导入库,然后定义一些协程,最后在事件循环中运行这些协程。

import asyncio

async def main():
    print('Hello')
    await asyncio.sleep(1)
    print('World')

# Python 3.7+
asyncio.run(main())

并发运行任务

如果你有多个协程需要并发运行,可以使用 asyncio.gather() 来同时调度它们。

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    task1 = asyncio.create_task(say_after(1, 'hello'))
    task2 = asyncio.create_task(say_after(2, 'world'))

    print('started at', time.strftime('%X'))

    # 等待两个任务完成
    await task1
    await task2

    print('finished at', time.strftime('%X'))

asyncio.run(main())

处理异步 I/O

asyncio 提供了对异步 I/O 操作的支持,比如读写文件、网络请求等。这些操作通常通过 await 来挂起当前协程,直到相应的操作完成,从而不会阻塞整个程序的执行。

async def fetch_data():
    reader, writer = await asyncio.open_connection('python.org', 80)
    writer.write(b'GET / HTTP/1.0\r\n\r\n')
    await writer.drain()
    data = await reader.read(100)
    print(data.decode())
    writer.close()
    await writer.wait_closed()

asyncio.run(fetch_data())

错误处理

asyncio 程序中处理错误通常使用标准的异常处理方法,即 try/except 块。

async def fetch_data():
    try:
        reader, writer = await asyncio.open_connection('python.org', 80)
        ...
    except Exception as e:
        print(f'An error occurred: {e}')
    finally:
        writer.close()
        await writer.wait_closed()

asyncio.run(fetch_data())

总结

asyncio 是 Python 中处理并发编程的强大工具,特别适合处理 I/O 密集型和高级别并发应用程序。通过使用 async/await 语法,asyncio 使得异步代码易于编写和理解。

asyncio 事件循环

asyncio 的事件循环是该库的核心组件之一,负责管理和执行异步任务。事件循环可以看作是一个无限循环,它接收并分发事件或任务,并保持程序运行直到所有任务完成或者被取消。事件循环的主要职责包括执行异步协程任务、处理网络IO操作、运行定时任务,以及在必要时调用相应的回调函数。

创建和运行事件循环

asyncio 中,通常有几种方式来创建和运行事件循环。

使用 asyncio.run()

从 Python 3.7 开始,asyncio.run() 是运行异步程序的推荐方式。它创建一个新的事件循环,并在循环中运行传入的协程,直到协程完成。完成后,它会关闭事件循环。

import asyncio

async def main():
    print('Hello')
    await asyncio.sleep(1)
    print('World')

asyncio.run(main())
手动管理事件循环

在某些情况下,你可能需要更细粒度地控制事件循环的创建和关闭。这可以通过直接使用事件循环的 API 来实现。

import asyncio

async def main():
    print('Hello')
    await asyncio.sleep(1)
    print('World')

# 获取当前平台的事件循环策略中的事件循环
loop = asyncio.get_event_loop()
try:
    # 运行直到协程完成
    loop.run_until_complete(main())
finally:
    # 关闭事件循环
    loop.close()

事件循环的方法

事件循环提供了多种方法来管理异步任务和其他事件:

  • run_until_complete(future): 运行传入的协程直到完成。
  • create_task(coro): 将协程封装为一个任务并调度其执行。
  • run_forever(): 运行事件循环直到 stop() 被调用。
  • stop(): 停止事件循环。
  • close(): 关闭事件循环。事件循环使用完毕后应当被关闭。

事件循环中的异步IO和其他操作

asyncio 的事件循环支持异步执行多种操作,如网络请求、文件IO等。这些操作通常通过特定的 asyncio 函数实现,例如:

  • asyncio.sleep(): 异步版本的 sleep。
  • asyncio.open_connection(): 用于打开网络连接。
  • asyncio.start_server(): 启动一个异步服务器。

总结

asyncio 的事件循环是异步编程的核心,它允许代码以非阻塞的方式执行多个操作。通过合理使用事件循环和相关的 asyncio API,可以构建出高效的异步应用程序。

task取消和子协程调用原理

asyncio 中,任务(Task)是对协程的一种封装,使其可以被调度和管理。任务的取消以及子协程的调用是 asyncio 异步编程中的两个重要概念。

任务取消

asyncio 中,可以取消一个正在执行的任务。当一个任务被取消时,事件循环会向任务发送一个 CancelledError 异常。如果任务当前正在等待另一个异步操作(如 await asyncio.sleep(1)),则该操作会被中断,并抛出 CancelledError 异常。

示例代码:

import asyncio

async def cancel_me():
    print('cancel_me(): before sleep')
    try:
        # 这里等待一段时间
        await asyncio.sleep(10)
    except asyncio.CancelledError:
        print('cancel_me(): cancel sleep')
        raise
    finally:
        print('cancel_me(): after sleep')

async def main():
    # 创建任务
    task = asyncio.create_task(cancel_me())

    # 等待一秒后取消任务
    await asyncio.sleep(1)
    task.cancel()

    try:
        await task
    except asyncio.CancelledError:
        print('main(): cancel_me is cancelled now')

asyncio.run(main())

子协程调用原理

asyncio 中,一个协程可以调用另一个协程,这通常通过 await 表达式实现。当一个协程通过 await 调用另一个协程时,调用者会被挂起,直到被调用的协程完成执行。这种机制允许实现复杂的异步逻辑,同时保持代码的清晰和简洁。

示例代码:

import asyncio

async def nested():
    print("Nested coroutine")
    return 42

async def main():
    # 直接调用另一个协程
    result = await nested()
    print(f"Result of nested coroutine: {result}")

asyncio.run(main())

在这个例子中,main 协程调用了 nested 协程,并等待其完成。await 表达式使得 main 协程在 nested 协程完成前暂停执行。

总结

任务的取消和子协程的调用是 asyncio 异步编程中的两个基本操作。任务取消允许你优雅地中断可能长时间运行的异步操作,而子协程的调用则是构建复杂异步应用程序的基石。通过这些机制,asyncio 提供了强大的工具来处理并发和异步编程的复杂性。

call_soon、call_at、call_later、call_soon_threadsafe

asyncio 中,事件循环提供了几种方法来安排回调的执行。这些方法允许你在特定时间或尽快执行某个函数,这对于整合非异步代码到异步应用中或者处理定时事件非常有用。

call_soon

call_soon 方法用于尽快调度一个回调函数的执行。它不是立即执行回调,而是将回调放入事件循环的队列中,等待下一次事件循环迭代时执行。

import asyncio

def callback():
    print('Callback called!')

loop = asyncio.get_event_loop()
loop.call_soon(callback)
loop.run_until_complete(asyncio.sleep(1))
loop.close()

call_later

call_later 方法用于在指定的时间后调度一个回调函数执行。它接受一个时间延迟(以秒为单位)和要调用的回调函数。

import asyncio

def callback():
    print('Callback called!')

loop = asyncio.get_event_loop()
# 在1秒后调用callback
loop.call_later(1, callback)
loop.run_until_complete(asyncio.sleep(2))
loop.close()

call_at

call_at 方法用于在指定的绝对时间点调度一个回调。它接受一个特定的时间戳(相对于事件循环的时间方法,如 loop.time())和一个回调函数。

import asyncio

def callback():
    print('Callback called!')

loop = asyncio.get_event_loop()
# 当前时间加2秒
when = loop.time() + 2
loop.call_at(when, callback)
loop.run_until_complete(asyncio.sleep(3))
loop.close()

call_soon_threadsafe

call_soon_threadsafe 是线程安全版本的 call_soon。它用于从非异步代码或不同线程安排回调在异步事件循环中执行。这在你需要从另一个线程与异步代码交互时非常有用。

import asyncio
import threading

def callback():
    print('Callback called from', threading.current_thread())

def start_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()

loop = asyncio.new_event_loop()
t = threading.Thread(target=start_loop, args=(loop,))
t.start()

# 安全地从主线程调度回调
loop.call_soon_threadsafe(callback)

总结

这些调度方法为 asyncio 提供了灵活的方式来集成和管理回调,使得可以在适当的时间执行特定的函数。它们在处理定时任务、集成同步代码或从其他线程与异步代码交互时非常有用。

ThreadPollExecutor 和 asycio 完成阻塞 IO 请求

在 Python 的异步编程中,asyncio 与线程池(如 ThreadPoolExecutor)的结合使用可以有效地处理阻塞 IO 操作,而不会阻塞整个异步事件循环。这种方法尤其适用于需要执行密集文件读写、网络请求或其他阻塞系统调用的场景。

ThreadPoolExecutor

ThreadPoolExecutorconcurrent.futures 模块的一部分,它管理一个线程池,可以用来执行并发的执行阻塞操作。通过将阻塞操作放在一个线程池中执行,主事件循环可以继续处理其他非阻塞操作。

结合使用 ThreadPoolExecutor 和 asyncio

以下是如何结合使用 ThreadPoolExecutorasyncio 来处理阻塞 IO 操作的一个例子:

import asyncio
from concurrent.futures import ThreadPoolExecutor
import urllib.request

# 异步函数,用于在线程池中执行阻塞的 IO 操作
async def download_url(url):
    print(f"开始下载: {url}")

    # 在线程池中执行阻塞操作
    loop = asyncio.get_running_loop()
    with ThreadPoolExecutor() as executor:
        # run_in_executor 参数:执行器,要运行的函数,函数的参数
        result = await loop.run_in_executor(executor, urllib.request.urlopen, url)
        data = result.read()

    print(f"完成下载: {url}, 数据长度: {len(data)}")
    return data

async def main():
    urls = [
        'http://example.com',
        'http://example.org',
        'http://example.net',
    ]

    # 并发下载所有 URL
    await asyncio.gather(*(download_url(url) for url in urls))

# 运行主函数
asyncio.run(main())

工作原理

  1. 创建异步函数download_url 是一个异步函数,它接受一个 URL,用于下载数据。
  2. 使用 ThreadPoolExecutor:阻塞的 urllib.request.urlopen 函数在 ThreadPoolExecutor 中执行。这意味着它将在一个单独的线程中运行,而不会阻塞 asyncio 的事件循环。
  3. 等待结果await loop.run_in_executor(...) 等待线程池中的任务完成,并返回结果。这是一个异步等待,因此不会阻塞事件循环。
  4. 并发执行asyncio.gather 用于并发执行多个异步下载任务。

总结

通过结合使用 ThreadPoolExecutorasyncio,可以在异步应用中有效地处理阻塞 IO 操作,而不会影响程序的整体响应性。这种模式特别适合于需要处理大量独立的、耗时的阻塞操作的应用,如网络请求、文件操作等。

asyncio 模拟 http 请求

要在 asyncio 中模拟 HTTP 请求,我们可以使用 aiohttp 库,这是一个提供异步 HTTP 客户端和服务器的 Python 库。使用 aiohttp 进行 HTTP 请求是异步的,这意味着它不会阻塞事件循环,非常适合需要高性能的 IO 操作的应用程序。

首先,你需要安装 aiohttp 库,如果还没有安装的话,可以使用 pip 进行安装:

pip install aiohttp

示例:使用 aiohttp 发送异步 HTTP 请求

下面是一个使用 aiohttp 发送 GET 请求的简单示例:

import aiohttp
import asyncio

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        url = 'http://httpbin.org/get'
        html = await fetch(session, url)
        print(html)

asyncio.run(main())

代码解释

  1. 创建客户端会话aiohttp.ClientSession() 创建一个异步的 HTTP 客户端会话。使用 async with 确保会话在结束时正确关闭。
  2. 发送 GET 请求session.get(url) 异步发送一个 GET 请求到指定的 URL。
  3. 处理响应response.text() 异步获取响应内容。async with 确保响应在处理完毕后正确关闭。
  4. 打印结果:打印出 HTTP 响应的内容。

进阶使用

如果你需要发送 POST 请求或添加请求头、处理 cookies 等,aiohttp 也支持这些功能。下面是一个发送 POST 请求的示例:

import aiohttp
import asyncio

async def post_data(session, url, data):
    async with session.post(url, data=data) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        url = 'http://httpbin.org/post'
        data = {'key': 'value'}
        response_text = await post_data(session, url, data)
        print(response_text)

asyncio.run(main())

这个示例展示了如何使用 aiohttp 发送 POST 请求。session.post(url, data=data) 用于发送 POST 请求,其中 data 是一个字典,包含你想要发送的数据。

总结

使用 aiohttpasyncio 进行异步 HTTP 请求是处理网络请求的高效方式,特别是在需要高并发的场景下。通过异步操作,你的应用可以在等待网络响应时继续执行其他任务,从而提高整体性能和响应速度。

future 和 task

在 Python 的 asyncio 模块中,FutureTask 是两个核心的概念,它们都用于表示将来完成的操作,但它们在用途和行为上有一些区别。

Future

Future 对象是一个低层次的可等待对象,表示异步操作的最终结果。它通常由低级系统代码创建和使用,例如在库或框架的内部。Future 对象是一个抽象层,它使得可以在其上附加回调,并在操作完成时获取结果。

一个 Future 对象有以下关键状态:

  • Pending: 初始状态,未完成的状态。
  • Done: 操作完成,可以获取结果或异常。
  • Cancelled: 操作被取消。

使用 Future 对象通常涉及以下步骤:

  1. 创建 Future 对象。
  2. Future 对象传递给某个能够影响其状态的底层代码(例如,执行某种操作的库)。
  3. 等待 Future 对象的结果。

Task

TaskFuture 的一个子类,它更高级且更具体,用于封装协程,是协程的运行容器。Task 对象用于调度协程的执行,让协程可以在事件循环中运行。当你创建一个 Task 对象时,你实际上将协程加入到了事件循环中,事件循环将负责执行它。

使用 Task 对象时,通常的步骤包括:

  1. 创建一个协程。
  2. 将协程封装到 Task 对象中(通常是通过 asyncio.create_task())。
  3. 等待 Task 对象的结果。

示例代码

下面是一个示例,展示如何使用 FutureTask

import asyncio

async def set_after(fut, delay, value):
    # 模拟延时操作
    await asyncio.sleep(delay)
    fut.set_result(value)  # 设置 Future 对象的结果

async def main():
    # 创建一个 Future 对象
    fut = asyncio.Future()

    # 等待 Future 对象设置结果
    await asyncio.create_task(set_after(fut, 1, '...done'))

    # 获取 Future 对象的结果
    print(fut.result())

asyncio.run(main())

在这个示例中,set_after 是一个协程,它接受一个 Future 对象并在延迟后设置其结果。main 函数创建了一个 Future 对象,并使用 asyncio.create_task 创建了一个 Task 来运行 set_after 协程。

总结

  • Future: 用于表示异步操作的最终结果,通常由底层库使用。
  • Task: 是 Future 的子类,用于封装和调度协程的执行。

在实际应用中,当你使用 asyncio 编写异步代码时,通常会更多地与 Task 对象打交道,而 Future 对象更多地被库和框架内部使用。

asyncio同步和通信

在使用 asyncio 编写异步 Python 程序时,可能会遇到需要同步和通信的场景。asyncio 提供了多种机制来帮助协程之间进行同步和通信,包括事件、锁、信号量、条件变量和队列等。这些工具可以帮助你管理并发协程之间的交互,确保数据的一致性和操作的顺序性。

1. 事件(Event)

asyncio.Event 用于在协程之间发送信号。它的状态可以被设置为真或假。等待事件的协程将在事件被设置时继续执行。

import asyncio

async def waiter(event):
    print('waiting for the event to be set')
    await event.wait()
    print('event is set')

async def main():
    event = asyncio.Event()

    # 将等待 event 的协程放入事件循环
    asyncio.create_task(waiter(event))

    # 模拟延迟,然后设置事件
    await asyncio.sleep(1)
    event.set()

asyncio.run(main())

2. 锁(Lock)

asyncio.Lock 可以用来保护共享资源。它类似于传统的线程锁,但是是为协程设计的,不会阻塞线程。

import asyncio

async def coro1(lock):
    print('Coro1 waiting for the lock')
    async with lock:
        print('Coro1 acquired lock')
        await asyncio.sleep(2)
    print('Coro1 released lock')

async def coro2(lock):
    print('Coro2 waiting for the lock')
    async with lock:
        print('Coro2 acquired lock')
        await asyncio.sleep(2)
    print('Coro2 released lock')

async def main():
    lock = asyncio.Lock()
    await asyncio.gather(coro1(lock), coro2(lock))

asyncio.run(main())

3. 信号量(Semaphore)

asyncio.Semaphore 用于限制同时访问某个资源的协程数量。

import asyncio

async def worker(semaphore, num):
    async with semaphore:
        print(f'Worker {num} is working')
        await asyncio.sleep(2)

async def main():
    semaphore = asyncio.Semaphore(2)  # 同时只允许2个协程工作
    await asyncio.gather(*(worker(semaphore, i) for i in range(4)))

asyncio.run(main())

4. 条件变量(Condition)

asyncio.Condition 用于协程间的复杂同步,允许协程等待或通知特定条件的变更。

import asyncio

async def consumer(condition, n):
    async with condition:
        print(f'consumer {n} is waiting')
        await condition.wait()
        print(f'consumer {n} triggered')

async def producer(condition):
    await asyncio.sleep(2)
    async with condition:
        print('producer is notifying')
        condition.notify_all()

async def main():
    condition = asyncio.Condition()
    consumers = [asyncio.create_task(consumer(condition, i)) for i in range(3)]
    await asyncio.sleep(0.1)  # 确保消费者先运行
    await producer(condition)
    await asyncio.gather(*consumers)

asyncio.run(main())

5. 队列(Queue)

asyncio.Queue 用于协程间的消息传递。它是线程安全的,适合用于生产者-消费者问题。

import asyncio

async def producer(queue):
    for i in range(5):
        await queue.put(i)
        print(f'produced {i}')
        await asyncio.sleep(1)

async def consumer(queue):
    while True:
        item = await queue.get()
        print(f'consumed {item}')
        queue.task_done()

async def main():
    queue = asyncio.Queue()
    producer_task = asyncio.create_task(producer(queue))
    consumer_task = asyncio.create_task(consumer(queue))
    await asyncio.sleep(10)
    consumer_task.cancel()

asyncio.run(main())

总结

通过使用 asyncio 提供的同步和通信机制,你可以有效地管理和协调协程之间的交互,确保程序的正确性和效率。这些工具可以应对不同的并发编程需求,帮助你构建健壮的异步应用。

aiohttp实现高并发爬虫

使用 aiohttp 库实现高并发的爬虫是 Python 异步编程的一个常见应用。aiohttp 是一个提供异步 HTTP 客户端和服务器功能的库,它基于 asyncio,因此可以处理大量并发 HTTP 请求,非常适合用于构建高效的网络爬虫。

步骤概述

  1. 安装 aiohttp:首先需要安装 aiohttp 库。
  2. 创建异步 HTTP 会话:使用 aiohttp.ClientSession() 创建会话。
  3. 发起异步请求:使用会话对象发起 GET 或 POST 请求。
  4. 处理响应:对返回的响应进行处理,如解析 HTML。
  5. 并发控制:使用异步信号量 (asyncio.Semaphore) 控制并发量,防止过多请求压垮服务器或触发反爬机制。
  6. 异常处理:正确处理网络请求过程中可能出现的异常。
  7. 数据存储:将爬取的数据存储到文件或数据库中。

示例代码

下面是一个简单的示例,展示如何使用 aiohttp 实现一个基本的高并发爬虫:

import aiohttp
import asyncio
from bs4 import BeautifulSoup

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def parse(html):
    soup = BeautifulSoup(html, 'html.parser')
    titles = soup.find_all('h2')
    for title in titles:
        print(title.get_text())

async def main(urls, max_concurrency):
    # 控制并发数量
    semaphore = asyncio.Semaphore(max_concurrency)
    
    async with aiohttp.ClientSession() as session:
        async def bounded_fetch(url):
            async with semaphore:
                html = await fetch(session, url)
                await parse(html)
        
        tasks = [bounded_fetch(url) for url in urls]
        await asyncio.gather(*tasks)

urls = [
    'https://example.com/page1',
    'https://example.com/page2',
    # 更多 URL
]

# 设置最大并发数
max_concurrency = 10

asyncio.run(main(urls, max_concurrency))

重要注意事项

  1. 合理设置并发数:并发数不宜过高,以免给目标服务器带来过大压力,也可能导致被封IP。
  2. 遵守 Robots 协议:在进行网络爬虫开发时,应该遵守目标网站的 robots.txt 文件规定,尊重网站的爬虫政策。
  3. 异常处理:网络请求可能会因为各种原因失败,如超时、连接错误等,应适当处理这些异常。
  4. 异步安全:确保你的代码在异步环境中是安全的,特别是在操作共享资源如数据库时。

通过这种方式,你可以有效地利用 aiohttpasyncio 的异步特性来构建一个高效且强大的网络爬虫。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值