python协程和异步io

python协程和异步io

在 Python 中,协程和异步 I/O 提供了一种高效处理 I/O 密集型和高级别并发应用的方法。这些特性主要通过 asyncio 库实现,它是 Python 3.4+ 的标准库,用于编写单线程并发代码。

协程 (Coroutines)

协程是一种特殊类型的函数,可以在执行过程中暂停和恢复,它们是通过 async def 语法定义的。与传统的函数不同,协程在调用时不会立即执行,而是返回一个协程对象,这个对象需要被运行在事件循环中。

异步 I/O (Asynchronous I/O)

异步 I/O 是一种不需要阻塞线程,而是利用单线程进行多任务处理的技术。在等待 I/O 操作(如网络请求或磁盘读写)完成时,程序可以执行其他任务。asyncio 库提供了一套用于编写异步 I/O 代码的框架。

asyncio 库的核心组件

  1. 事件循环(Event Loop)
    负责执行协程,以及处理异步 I/O 事件。事件循环是 asyncio 程序的核心,所有的异步操作都应该在事件循环中执行。

  2. 协程(Coroutine)
    通过 async def 定义的函数。协程可以通过 await 暂停其执行,等待异步操作完成。

  3. 任务(Task)
    用于调度协程的执行。任务是对协程的一种封装,使得协程可以被调度和管理。

  4. Future
    表示一个异步操作的最终结果。它是一个低层次的可等待对象,通常不需要直接使用。

  5. 异步函数(Asynchronous Functions)
    使用 async def 定义的函数。这些函数可以包含 await 表达式。

示例代码

以下是一个使用 asyncio 的简单示例,演示了如何实现异步网络请求:

import asyncio
import aiohttp  # 需要安装 aiohttp 包

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

async def main():
    url = "http://example.com"
    content = await fetch(url)
    print(content)

# 运行事件循环
asyncio.run(main())

注意事项

  • 使用 asyncio 时,所有可能阻塞的操作都应该是异步的。例如,使用 aiohttp 替代 requests 进行 HTTP 请求。
  • 协程应该通过 await 调用另一个协程,这样才能正确地挂起和恢复执行。
  • 在设计异步程序时,避免使用同步和阻塞调用,因为它们会阻塞整个事件循环。

通过使用 asyncio 和协程,Python 程序可以在单线程内实现高效的并发处理,特别适合处理 I/O 密集型任务。

并发、并行、同步、异步、阻塞、非阻塞

在讨论多任务处理和 I/O 操作时,“并发”、“并行”、“同步”、“异步”、"阻塞"和"非阻塞"这些术语经常出现。了解它们的含义对于设计有效的多任务和 I/O 操作程序至关重要。

并发 (Concurrency)

并发是指系统能够处理多个任务的能力,这些任务可以是交替执行的,不一定同时进行。在单核处理器上,通过任务快速切换给用户一种同时执行多个任务的错觉,实际上这些任务是分时使用CPU的。

并行 (Parallelism)

并行是指多个任务或操作在同一时刻发生,通常依赖于多核或多处理器系统。每个核心可以同时执行不同的任务,从而真正意义上实现同时处理多个任务。

同步 (Synchronous)

同步操作是指任务按顺序执行,一个任务的完成通常依赖于前一个任务的完成。在同步操作中,程序执行流程在等待某个任务完成时会被阻塞,直到该任务完成后才继续执行。

异步 (Asynchronous)

异步操作允许程序在等待某个任务完成的同时继续执行其他任务。这种方式不会阻塞程序的主执行流程,可以提高程序的整体效率和响应性。异步通常用于 I/O 操作,如网络请求或文件读写。

阻塞 (Blocking)

阻塞调用是指调用结果必须在其他操作执行之前返回的操作。例如,当程序执行到一个阻塞 I/O 操作时(如读取硬盘文件),它将停止执行后续代码,直到 I/O 操作完成。

非阻塞 (Non-blocking)

非阻塞调用指的是调用在等待操作完成时不会阻塞程序执行。如果 I/O 操作准备就绪,它就会执行并返回结果;如果不准备就绪,它也会立即返回,但返回的是一个状态标识,告诉你数据还未准备好。

举例说明

考虑一个简单的网络请求场景:

  • 同步阻塞方式:发起请求并等待服务器响应,期间程序其他部分无法执行。
  • 同步非阻塞方式:发起请求,如果数据未准备好,程序可以做一些其他的检查,然后再次检查数据是否就绪,如此循环。
  • 异步非阻塞方式:发起请求并继续执行其他任务,当响应就绪时,程序会通过回调或事件得到通知。

总结

  • 并发并行处理多任务的方式不同,前者是任务交替执行,后者是真正同时执行。
  • 同步异步关注任务处理的流程是否被阻塞。
  • 阻塞非阻塞描述的是程序在等待调用结果时的行为。

理解这些概念有助于开发者更好地设计和优化多任务处理和 I/O 密集型应用。

多路复用 (select、poll 和 epoll)

多路复用是一种允许单个进程或线程同时管理多个 I/O 操作的技术。在 Unix-like 系统中,多路复用通常通过 selectpollepoll 这三种系统调用实现。这些技术都是用来监视一组文件描述符,等待一个或多个文件描述符变得就绪(可读、可写或有异常)。

select

select 是最早的多路复用实现之一。它允许程序监视多个文件描述符,等待直到一个或多个文件描述符就绪或超时。

优点

  • 简单易用,广泛支持在各种操作系统上。

缺点

  • 文件描述符数量受限于 FD_SETSIZE,通常为 1024。
  • 每次调用 select 时,都需要重新传入文件描述符集合,这导致效率低下。
  • 随着文件描述符数量的增加,性能线性下降。

poll

poll 是对 select 的改进,解决了文件描述符数量的限制问题。

优点

  • 不再有 FD_SETSIZE 的限制,可以处理更多的文件描述符。
  • 接口相对简单,与 select 类似。

缺点

  • select 类似,每次调用 poll 时也需要传入所有监视的文件描述符。
  • 性能仍然随着文件描述符数量的增加而线性下降。

epoll

epoll 是 Linux 特有的多路复用解决方案,旨在解决 selectpoll 的性能问题。

优点

  • 只需向 epoll 实例注册一次文件描述符,除非文件描述符发生变化,否则无需重复注册。
  • 使用一种称为“事件通知”的机制,只返回就绪的文件描述符,避免了大量无效的遍历,提高了效率。
  • 能够扩展到大量的文件描述符,性能几乎不受文件描述符数量的影响。

缺点

  • 仅在 Linux 系统上可用。

使用场景

  • 对于文件描述符数量不多的应用,selectpoll 可能已足够使用。
  • 对于需要高性能和处理大量连接的服务器应用,如高性能 Web 服务器或数据库,epoll 是更好的选择。

示例代码

以下是一个使用 select 的 Python 示例,用于监视标准输入:

import select
import sys

# 创建一个文件描述符列表,这里只有标准输入
fds = [sys.stdin]

while True:
    # select 等待输入
    readable, _, _ = select.select(fds, [], [])
    for fd in readable:
        if fd is sys.stdin:
            line = sys.stdin.readline()
            if line:
                print(f"Received: {line.strip()}")
            else:  # EOF
                exit("Exiting.")

多路复用是处理大量并发 I/O 操作的一种高效方法,尤其在构建需要高性能网络通信的应用程序时非常有用。

select+回调+事件循环获取html

在 Python 中,结合使用 select 模块、回调机制和事件循环来异步获取 HTML 页面是一种高效的方法,尤其适用于处理多个网络请求。下面我将展示一个基本的例子,说明如何实现这一过程。

步骤说明

  1. 设置套接字:使用非阻塞套接字进行网络连接。
  2. 使用 select 监听套接字:使用 select 来检测套接字何时可读或可写。
  3. 事件循环:循环等待 select 事件,根据事件类型调用相应的回调函数处理。
  4. 回调函数:为连接、接收数据等定义回调函数。

示例代码

这个例子中,我们将创建一个简单的 HTTP 客户端,用于从指定的 URL 获取 HTML。我们将使用 Python 的标准库,包括 socketselect

import socket
import select

def fetch_html(url):
    # 解析 URL 获取主机名和路径
    host, path = url.split('/', 3)[-2:]
    path = '/' + url.split('/', 3)[-1] if len(url.split('/', 3)) > 3 else '/'

    # 创建一个非阻塞的套接字
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.setblocking(False)
    
    try:
        client_socket.connect((host, 80))
    except BlockingIOError:
        pass  # 非阻塞连接立即抛出异常

    request = f"GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n"

    # 事件循环
    while True:
        # 使用 select 监视套接字
        read_ready, write_ready, _ = select.select([client_socket], [client_socket], [])
        
        if write_ready:
            # 套接字可写,发送请求
            client_socket.send(request.encode())
            break  # 继续到读取阶段

    response = b''
    while True:
        read_ready, _, _ = select.select([client_socket], [], [])
        if read_ready:
            # 套接字可读,接收数据
            data = client_socket.recv(4096)
            if not data:
                break  # 没有数据,结束接收
            response += data

    # 关闭套接字
    client_socket.close()
    return response.decode()

# 使用示例
url = "http://example.com"
html_content = fetch_html(url)
print(html_content)

注意事项

  • 本示例仅适用于 HTTP/1.1 和非加密的 HTTP 连接。
  • 实际应用中,错误处理和异常管理需要更加详尽。
  • 对于 HTTPS 或更复杂的 HTTP 功能,考虑使用现成的库如 aiohttp

这个例子展示了如何使用底层的 socketselect 来异步获取网页内容。在实际应用中,可能需要更复杂的错误处理和性能优化。

python回调之痛

在 Python 中,虽然回调模式不像在 JavaScript 中那样普遍,但在处理异步编程、事件驱动编程或某些类型的 I/O 操作时,回调仍然会被使用。当涉及到复杂的异步操作或多层嵌套的回调时,Python 开发者也可能遇到类似“回调之痛”的问题。这主要表现为代码结构复杂、难以维护和理解、错误处理困难等。

回调之痛的表现

  1. 复杂的嵌套结构:多层嵌套的回调函数可以使代码变得难以阅读和维护。
  2. 错误处理困难:在多层嵌套的回调中进行错误处理通常很麻烦,容易遗漏处理某些错误的情况。
  3. 作用域和闭包问题:回调函数可能引用外部变量,容易导致难以追踪的 bugs 和内存泄漏。
  4. 测试困难:嵌套的回调使得单元测试变得更加困难。

Python 中的解决方案

Python 提供了几种机制来避免回调之痛,使异步编程更加直观和易于管理:

  1. 使用生成器 (Generators)

    • Python 的生成器允许你编写看似同步的代码来执行异步操作。
  2. 使用协程 (Coroutines) with asyncio

    • Python 3.5 引入了 asyncawait 关键字,它们是基于 asyncio 库的,可以用来编写异步代码,类似于同步代码的风格。
  3. 使用第三方库

    • Twisted, Tornado, gevent 等,这些库提供了自己的解决方案来处理异步编程和避免回调地狱。

示例:使用 asyncio

下面是一个使用 asyncio 的示例,展示如何使用 asyncawait 来简化异步HTTP请求的处理:

import asyncio
import aiohttp

async def fetch_data(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

async def main():
    url = "http://example.com"
    data = await fetch_data(url)
    print(data)

# 运行事件循环
asyncio.run(main())

在这个示例中,fetch_data 函数异步获取网页内容,而不需要回调函数。整个代码结构清晰,易于理解和维护。

总结

通过使用 asyncawait,Python 程序员可以有效地避免回调之痛,同时保持代码的清晰和简洁。这些工具也使得错误处理和测试变得更加简单。在现代 Python 异步编程中,推荐使用这些新的语言特性来处理并发和异步操作。

协程是什么

协程(Coroutines)是一种计算机程序组件,它广泛用于并发编程和异步编程。与传统的子程序(如函数或方法)不同,协程提供了多个入口点和手动控制的执行挂起与恢复的能力。这使得协程特别适合处理异步操作,如网络请求、文件 I/O 或任何可能导致长时间等待的操作,同时不阻塞程序的其余部分。

协程的核心特征

  1. 多个入口点:传统的函数在被调用时只有一个入口点(即函数的开始),执行到结束后返回。协程可以在一个入口点暂停执行,并在之后从另一个点继续执行。

  2. 执行控制:协程的执行可以在特定的挂起点暂停,并在需要时

Python 中的协程(coroutines)是一种用于并发编程的高级结构,它允许不同的执行部分在单个线程内相互协作运行。协程是一种特殊类型的函数,它可以在执行过程中暂停和恢复,从而在等待 I/O 操作或其他长时间运行的操作完成时让出控制权,使得其他协程可以运行。

协程的基本特性

  1. 异步执行:协程可以在等待操作完成时挂起,允许其他协程运行。
  2. 非阻塞:在一个协程等待时,不会阻塞程序的其他部分。
  3. 在单个线程中运行:与多线程相比,协程避免了线程切换的开销和复杂的同步问题。

协程与生成器

在 Python 中,协程最初是基于生成器(generators)实现的。生成器通过 yield 表达式提供了一种方式来暂停函数的执行。Python 3.3 引入了 yield from 语法,这进一步增强了生成器的能力,使其可以用于更复杂的协程场景。

Python 3.5+ 的 asyncawait

Python 3.5 引入了 asyncawait 两个新的关键字,专门用于简化和支持异步编程。这些关键字让协程的编写更加直观和易于管理。

  • async def:用来定义一个协程函数。
  • await:用于在协程内部挂起协程的执行,等待异步操作完成。

示例:使用 asyncio

import asyncio

async def count():
    print("One")
    await asyncio.sleep(1)  # 模拟 I/O 操作,协程在此挂起
    print("Two")

async def main():
    await asyncio.gather(count(), count(), count())

asyncio.run(main())

在这个示例中,count 是一个协程,它在打印 “One” 后暂停一秒钟。asyncio.gather 用于并发运行多个协程。整个程序在单个线程中异步执行。

使用场景

协程非常适合处理 I/O 密集型和高级别并发的应用程序。例如,它们在网络服务器、异步任务处理、实时数据处理等领域中非常有用。使用协程可以提高应用程序的响应性和吞吐量。

总结

协程是 Python 异步编程的核心,提供了一种有效的方法来处理并发,特别是在 I/O 密集型应用中。通过使用 asyncawait,开发者可以编写出清晰、高效且易于维护的异步代码。

生成器进阶-send、close和throw方法

在 Python 中,生成器是一种特殊的迭代器,它可以用来控制函数的执行过程。生成器不仅可以通过 next() 函数产生序列中的下一个值,还支持 send(), close(), 和 throw() 方法,使得生成器的交互性和控制能力更强。

1. send() 方法

send() 方法用于向生成器发送一个值,这个值会成为生成器内部 yield 表达式的结果。这允许外部代码与生成器内部进行交互。当你首次调用生成器时,需要使用 next()send(None) 来启动生成器。

示例

def counter():
    n = 0
    while True:
        received = yield n
        if received is not None:
            n = received
        n += 1

gen = counter()
print(next(gen))  # 输出 0,启动生成器
print(gen.send(10))  # 发送 10,生成器从 10 继续
print(next(gen))  # 输出 11

2. close() 方法

close() 方法用于关闭生成器。调用 close() 后,如果再尝试从该生成器获取或发送值,将会引发 StopIteration 异常。这可以用来在生成器不再需要时释放资源或者中断执行。

示例

def simple_gen():
    yield "Hello"
    yield "World"

gen = simple_gen()
print(next(gen))  # 输出 Hello
gen.close()  # 关闭生成器
try:
    print(next(gen))  # 尝试获取下一个值,将引发异常
except StopIteration:
    print("Generator is closed.")

3. throw() 方法

throw() 方法用于在生成器内部抛出一个指定的异常,然后返回下一个 yield 表达式的值。如果生成器不处理这个异常,异常将会传播到调用者那里。

示例

def handle_exception():
    try:
        yield "Hello"
    except Exception as e:
        yield "Caught: " + str(e)
    yield "World"

gen = handle_exception()
print(next(gen))  # 输出 Hello
print(gen.throw(Exception, "Something went wrong"))  # 抛出异常并捕获
print(next(gen))  # 输出 World

总结

send(), close(), 和 throw() 方法为 Python 的生成器提供了强大的控制和交互能力。通过这些方法,开发者可以更精细地控制生成器的执行流程,处理异常,或者在外部与生成器进行双向通信。这些特性使得生成器不仅仅是简单的迭代器,而是可以实现更复杂逻辑的强大工具。

生成器进阶-yield from

在 Python 中,yield from 是一个用于生成器的语法结构,它提供了一种简洁的方式来从另一个迭代器中产生值。这个语法不仅使代码更简洁,还增强了生成器的功能,使其能够更容易地委托子生成器处理部分任务。

基本用法

使用 yield from 可以将一个可迭代对象中的所有值逐一产出。这比在生成器中使用一个循环来逐个产出值要简洁得多。

示例

def subgenerator():
    yield from range(3)

def generator():
    yield from subgenerator()

for value in generator():
    print(value)  # 输出 0, 1, 2

在这个例子中,subgenerator 产生了一系列值,而 generator 使用 yield from 来从 subgenerator 中获取这些值并将它们传递给调用者。

委托给子生成器

yield from 的一个重要用途是将部分生成逻辑委托给子生成器。这样可以将一个大的生成器拆分成多个小的生成器,每个小生成器处理一部分任务,从而使代码更加模块化和可管理。

示例

def countdown(n):
    while n > 0:
        yield n
        n -= 1

def count_from_x_to_y(x, y):
    yield from countdown(x)
    yield from countdown(y)

for num in count_from_x_to_y(3, 2):
    print(num)  # 输出 3, 2, 1, 2, 1

传递值和异常

yield from 不仅可以传递值,还可以在外部生成器和子生成器之间传递异常和返回值。当外部生成器的 send() 方法被调用时,发送的值会直接传递给子生成器。如果子生成器抛出任何异常,这些异常也会被传递到外部生成器。

示例

def writer():
    while True:
        try:
            w = yield
        except Exception as e:
            yield "Caught: " + str(e)
        else:
            yield "Received: " + str(w)

def proxy():
    yield from writer()

p = proxy()
next(p)  # 启动生成器
print(p.send("Hello"))  # 输出 Received: Hello
print(p.throw(Exception, "Something went wrong"))  # 输出 Caught: Something went wrong

总结

yield from 是 Python 生成器的一个强大特性,它不仅简化了从其他迭代器产生值的代码,还使得生成器之间的委托和通信变得更加容易。通过使用 yield from,你可以构建更为复杂和模块化的生成器逻辑,从而提高代码的可读性和可维护性。

生成器实现协程

在 Python 中,生成器可以被用来实现协程(coroutines),这是一种支持协作式多任务的程序组件。在 Python 3.5 之前,协程通常是通过生成器来实现的,而 Python 3.5 引入了 asyncawait 语法后,现代的协程通常使用这些新特性来实现。不过,了解如何用生成器实现协程仍然是理解 Python 异步编程的一个重要步骤。

生成器基础

生成器最初被设计用来产生一系列的值,但它们也支持通过 send() 方法接收外部发送的值,这使得生成器可以用来实现协程。生成器协程可以在执行过程中暂停,等待外部的输入,处理输入后再继续执行。

协程的基本实现

在生成器协程中,yield 语句用来暂停协程的执行并等待外部的输入,外部通过 send() 方法向协程发送数据,发送的数据成为 yield 表达式的结果。

示例:简单的生成器协程

def simple_coroutine():
    print("Coroutine has started.")
    x = yield
    print("Coroutine received:", x)

my_coro = simple_coroutine()
next(my_coro)  # 启动协程,执行到第一个 yield
my_coro.send(10)  # 向协程发送值,协程从第一个 yield 继续执行

使用生成器实现状态机

生成器协程可以用来实现一个简单的状态机,根据接收到的输入改变内部状态,并根据状态做出不同的响应。

示例:状态机协程

def state_machine():
    while True:
        received = yield
        if received == 'ping':
            print("pong")
        elif received == 'quit':
            print("Quitting")
            break
        else:
            print("Unknown command")

sm = state_machine()
next(sm)  # 启动状态机
sm.send('ping')  # 输出 pong
sm.send('hello')  # 输出 Unknown command
sm.send('quit')  # 输出 Quitting

生成器协程的高级用法

生成器可以通过 yield from 语句来委托另一个生成器,这允许创建更复杂的协程结构。

示例:使用 yield from 委托子协程

def sub_coroutine():
    while True:
        received = yield
        if received == 'stop':
            break
        print("Sub received:", received)

def delegating_coroutine():
    yield from sub_coroutine()
    print("Sub coroutine has stopped")

dc = delegating_coroutine()
next(dc)
dc.send('hello')  # 输出 Sub received: hello
dc.send('stop')   # 输出 Sub coroutine has stopped

总结

虽然现代 Python 中协程通常使用 asyncawait 实现,但通过生成器实现协程仍然是一个有用的技巧,尤其是在需要理解或维护老代码时。生成器协程提供了一种方式来实现任务间的协作,而不是并发或并行执行。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值