Python协程原理全面分析

yizhihongxing

Python 协程原理全面分析

在介绍Python协程原理之前,需要先了解一些概念:

  • 并发:同时处理多个任务。
  • 并行:同时处理多个任务并使它们同时运行。关注于任务的执行,强调在物理上同时运行多个任务。
  • 同步:任务按照一定的顺序进行,只有先完成前面任务才能完成后面任务。
  • 异步:不按照任务排定的先后顺序进行,而是根据情况随时安排执行任务。异步任务可以在等待IO的过程中进行其他任务,从而提高程序效率。

协程是为了处理异步的一种技术,协程比线程的切换更加轻量级,能够处理大量IO操作,提高程序的并发性和性能。

什么是协程

协程(Coroutine)是一种用户态的轻量级线程。用户可以自行控制协程的启动、暂停、恢复和终止。

协程的本质是控制流的上下文切换。

协程的启动、恢复和终止由用户直接控制,不需要操作系统的参与,避免了切换进程或线程的开销,因此协程的切换非常快速,有利于实现高效率的并发程序。

Python协程的实现

Python实现协程的方式分为三种:

  • 生成器(generator);
  • asyncio库;
  • gevent库。

这里我们主要介绍使用生成器实现协程。

生成器实现协程

使用生成器实现协程,需要用到两个关键字:yieldsend()

yield 是将一个函数变为生成器函数,可以将代码执行到 yield 处时中断函数,并返回一个值暂停函数;

send() 是从生成器的外部向其中传入一个值,继续执行被中断的代码。

在协程中,yieldsend() 的搭配使用实现协程的切换。

首先,定义一个协程函数:

def coroutine_func():
    print('Coroutine started.')
    while True:
        x = yield
        print('Got:', x)

在该函数中使用 yield 关键字,使该函数变为生成器函数。

函数中的 yield 实现中断函数并返回一个值,而 x = yield 则将生成器函数定义为一个可接收外部传值的函数。

接下来,我们通过以下步骤使用上面定义的协程函数:

  1. 使用 next() 函数启动协程;
  2. 使用 send() 函数向协程发送值;
  3. 使用 throw() 函数关闭协程。
# 启动生成器
coroutine = coroutine_func()
next(coroutine)

使用 send() 函数向协程发送值:

coroutine.send('Hello')
coroutine.send('World')

最后,关闭协程:

coroutine.throw(StopIteration)

执行结果:

Coroutine started.
Got: Hello
Got: World
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

协程函数被成功启动并执行到第一个 yield 处,接着使用 send() 函数发送了两个值并分别得到输出,最终使用 throw() 函数关闭了协程。

使用协程的优点

避免频繁的IO调用

对于IO密集型的操作(如文件、网络IO等),使用传统的同步IO会导致程序长时间等待,浪费CPU时间的开销。而使用异步IO可以在等待IO操作完成的过程中,继续进行其他操作,从而提高程序的并发性和性能。

修复回调地狱

使用回调函数处理异步操作会导致函数嵌套层数增加,代码越来越难以维护的问题。而使用协程可以避免这种情况。

示例说明

示例一:爬取豆瓣图书数据

以豆瓣图书数据为例,使用协程实现异步爬取数据,加快爬取速度。

首先,我们需要安装 requestsbeautifulsoup4fake_useragent 库。

定义一个获取豆瓣图书信息的函数,该函数包含三个协程函数:

  1. fetch_content,获取HTTP响应内容;
  2. parse_html,解析HTML文本;
  3. save_csv,保存数据到CSV文件中。
import csv
import random
import time

import requests
from bs4 import BeautifulSoup
from fake_useragent import UserAgent


def fetch_content(url):
    headers = {
        'User-Agent': UserAgent().random,
        'Referer': 'https://book.douban.com/',
        'Connection': 'keep-alive'
    }
    response = requests.get(url, headers=headers)
    return response.text


def parse_html(html):
    soup = BeautifulSoup(html, 'lxml')
    for item in soup.select('li.subject-item'):
        uid = item.select_one('div.info label.uid').text.strip()
        title = item.select_one('div.info h2 a').text.strip()
        authors = item.select_one('div.info div.pub').text.strip().split('/')[0]
        rating = item.select_one('div.info div.star span.rating_nums').text.strip()
        yield uid, title, authors, rating


def save_csv(filename, data):
    with open(filename, 'a', encoding='utf-8-sig', newline='') as f:
        writer = csv.writer(f)
        writer.writerows(data)

定义一个协程函数,接收书籍的url和CSV文件名。该函数先获取豆瓣图书的HTML并解析出数据,然后将数据写入CSV文件中。

def crawl_book(url, filename):
    html = yield from fetch_content(url)
    data = list(parse_html(html))
    save_csv(filename, data)
    print(f'Crawled {url} successfully.')

接下来,我们在主程序中使用协程爬取豆瓣图书数据。

首先,定义待爬取的图书列表和CSV文件名。

urls = [f'https://book.douban.com/tag/编程?start={i*20}' for i in range(5)]
filename = 'books.csv'

定义一个协程任务调度函数,使用 asyncio 库实现。

import asyncio


async def task(urls, filename):
    tasks = [crawl_book(url, filename) for url in urls]
    await asyncio.gather(*tasks)

使用以下代码启动协程任务调度函数。

start_time = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(task(urls, filename))
elapsed_time = time.time() - start_time
print(f'Total time elapsed: {elapsed_time:.2f}s')

运行程序后,完成爬取任务,并将数据保存在 books.csv 文件中。运行时间大幅度降低,可以看出协程的优势。

示例二:实现异步UDP服务器

以下示例使用Python3.7+提供的新语法 asyncawait 实现异步UDP服务器。

import asyncio


async def handle_datagram(remote_addr, message):
    # 模拟消息处理的耗时过程
    await asyncio.sleep(0.1)
    print(f'Received a message {message.decode()} from {remote_addr[0]}:{remote_addr[1]}')


async def listen_udp_server(host, port):
    # 创建UDP服务器
    server = await asyncio.create_datagram_endpoint(
        protocol_factory=asyncio.DatagramProtocol,
        local_addr=(host, port)
    )

    print(f'Server started and listening on {host}:{port}.')

    # 监听客户端发送的UDP数据包
    async for datagram, remote_addr in server[0]:
        asyncio.create_task(handle_datagram(remote_addr, datagram))


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(listen_udp_server('127.0.0.1', 8888))
    except KeyboardInterrupt:
        pass
    finally:
        loop.close()

运行以上代码后,会启动一个监听本地8888端口的异步UDP服务器,并接收客户端发送的消息。当收到消息后,调用 handle_datagram 函数处理,模拟一个耗时的操作。

总结

协程是一种效率较高的异步编程方式,可以充分发挥计算机的性能,提高程序的并发性和性能。Python提供了多种实现协程的方式,如使用生成器实现、使用asyncio库实现、使用gevent库实现等。

在使用协程时,需要合理使用 yieldsend() 函数,避免死锁和资源泄漏等问题。同时也需要注意调整好协程任务的数量和优先级。

最后,协程的编写需要一定的技巧和经验,希望本文的介绍能够对读者有所帮助。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Python协程原理全面分析 - Python技术站

(0)
上一篇 2023年5月19日
下一篇 2023年5月19日

相关文章

  • 深入理解Python异常处理的哲学

    深入理解Python异常处理的哲学 异常处理的哲学 在编写代码时,异常处理是一个重要的部分。使用异常处理可以使代码更加清晰,易于调试,并且能够有效避免程序崩溃。在 Python 中,异常处理是一个基本的功能,任何开发人员都应该深入理解并掌握其哲学。 异常处理的核心思想是:让程序在遇到错误时不崩溃,而是以一种优雅的方式来处理错误。这种优雅的方式指的是我们可以在…

    python 2023年5月13日
    00
  • python调用API接口实现登陆短信验证

    Python调用API接口实现登录短信验证 在本文中,我们将介绍如何使用Python调用API接口实现登录短信验证。我们将使用requests库发送HTTP请求,并使用json库解析响应。 步骤1:导入必要的库 在使用Python调用API接口实现登录短信验证之前,我们需要先导入必要的库: import requests import json 在上面的示例…

    python 2023年5月15日
    00
  • PyautoGui常用教程(一篇掌握)

    PyAutoGUI常用教程 介绍 PyAutoGUI是一个用于自动化鼠标和键盘的Python库。通过PyAutoGUI,您可以编写脚本来自动点击鼠标和键盘,进行图像识别等操作,从而实现自动化任务。在本篇教程中,我们将介绍PyAutoGUI的常用方法。 安装 您可以通过以下命令安装PyAutoGUI: pip install pyautogui 常用方法 鼠标…

    python 2023年5月13日
    00
  • C++调用Python基础功能实例详解

    C++调用Python基础功能实例详解 背景介绍 C++作为一门强类型的编程语言,具有高效、稳定的特点,但在数据分析、机器学习、人工智能等领域,Python的使用越来越广泛。因此,如何在C++中调用Python的基础功能,成为了一个重要的问题。 准备条件 在开始之前,我们需要做好以下准备工作: 在系统中安装Python解释器; 安装C++与Python的代码…

    python 2023年5月30日
    00
  • Python 中获取数组的子数组示例详解

    Python 中获取数组的子数组示例详解 在 Python 中,我们可以通过一些简单的方式来获取数组的子数组。在这篇文章中,我们将介绍两种获取数组子数组的方法以及相应的代码示例。 方法一:切片法 切片法是 Python 中非常常用的一种遍历数组的方法,我们可以通过它快速获取一个数组的子数组。 例如,如果我们有如下的一个数组 arr: arr = [0, 1,…

    python 2023年6月5日
    00
  • Python实现注册登录功能

    Python实现注册登录功能需要以下步骤: 1. 创建数据库 首先需要创建一个数据库,保存用户的注册信息、登录信息。可以使用MySQL或SQLite等数据库管理系统。 示例代码(使用SQLite数据库): import sqlite3 conn = sqlite3.connect(‘user.db’) c = conn.cursor() c.execute(…

    python 2023年6月13日
    00
  • matplotlib之Font family [‘sans-serif‘] not found的问题解决

    确定问题: 在使用matplotlib绘图时,可能会遇到类似以下的报错: findfont: Font family [‘sans-serif’] not found. Falling back to DejaVu Sans. 这个错误通常表示matplotlib无法找到所需的字体包,从而默认使用“DejaVu Sans”字体。 解决问题: 安装所需的字体包…

    python 2023年5月20日
    00
  • Angular4.x Event (DOM事件和自定义事件详解)

    Angular4.x Event (DOM事件和自定义事件详解) 在Angular4.x中,事件是很重要的组成部分,它可以监听DOM事件和自定义事件,让我们以更快的速度、更高的效率处理用户交互和数据改变。 监听DOM事件 监听DOM事件是Angular4.x中最基本的事件处理方法。我们可以使用@HostListener装饰器为一个方法绑定一个DOM事件。 例…

    python 2023年6月13日
    00
合作推广
合作推广
分享本页
返回顶部