Python协程:开启高编程新境界 python3.9协程

liftword4个月前 (12-27)技术文章48

一、协程概述

协程作为一种单线程下的并发方式,在 Python 编程中发挥着重要作用。它是一种用户态的轻量级线程,区别于操作系统级别的线程,协程的切换由程序自身控制,而非操作系统。这使得协程在执行过程中具有更小的切换开销,更加轻量级。

协程的特点十分显著。首先,它可以在同一线程中实现并发运行,极大地提高了程序的执行效率。由于切换开销小,协程能够在单线程内充分利用 CPU 资源,实现类似于多线程的效果。其次,协程的状态由程序员自己管理,相比传统线程更加灵活和可控。此外,协程在遇到 IO 操作时可以自动切换到其他协程,进一步提高了程序的响应速度。

例如,在使用 gevent 模块实现协程时,当一个协程遇到 IO 操作,如网络访问或文件读取,gevent 会自动切换到其他协程,保证总有协程在运行,而不是等待 IO 操作完成。这种特性使得在处理大量 IO 密集型任务时,协程具有明显的优势。

以一个简单的网络爬虫为例,使用 gevent 可以同时发起多个请求,当一个请求遇到 IO 阻塞时,自动切换到其他请求,大大提高了爬虫的效率。同时,由于协程是在单线程内运行,不需要像多线程那样考虑线程安全问题,也不需要进行复杂的同步操作。

总的来说,协程作为一种高效的编程方式,在 Python 中具有广泛的应用前景。它能够在单线程内实现并发,提高程序的执行效率,同时减少了线程切换的开销和线程安全问题的复杂性。

二、Python 对协程的支持

(一)早期实现

1. greenlet,早期模块

greenlet 包是一个 Stackless(无栈化的)CPython 版本,支持微线程(tasklet)。tasklet 可以伪并行的运行并且同步的在信道上交换数据。例如,通过以下方式使用 greenlet:

from greenlet import greenlet

def func1():
    print(1)
    gr2.switch()
    print(2)
    gr2.switch()

def func2():
    print(3)
    gr1.switch()
    print(4)

gr1 = greenlet(func1)
gr2 = greenlet(func2)
gr1.switch()

2. yield 关键字(Python2.x 开始)

在 Python2.x 中,通过 yield 关键字支持的生成器实现了一部分协程的功能但不完全。例如:

def func1():
    yield 1
    yield from func2()
    yield 2

def func2():
    yield 3
    yield 4

f1 = func1()
for item in f1:
    print(item)


(二)现代方法

1. asyncio 装饰器(Python 3.4 开始)

在 Python3.4 版本之后,可以使用 asyncio 装饰器模式实现协程。它遇到 IO 等耗时操作会自动切换到其他任务执行。例如:

import asyncio

@asyncio.coroutine
def func1():
    print(1)
    yield from asyncio.sleep(3)
    print(2)

@asyncio.coroutine
def func2():
    print(3)
    yield from asyncio.sleep(2)
    print(4)

@asyncio.coroutine
def func3():
    print(5)
    yield from asyncio.sleep(2)
    print(6)

tasks = [asyncio.ensure_future(func1()), asyncio.ensure_future(func2()), asyncio.ensure_future(func3())]
start = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print(end - start)

2. async、await 关键字(Python 3.5 开始)

Python3.5 中引入了 async、await 关键字,使协程的实现更加清晰和简洁。比如:

import asyncio
import time

async def func1():
    print(1)
    await asyncio.sleep(3)
    print(2)

async def func2():
    print(3)
    await asyncio.sleep(2)
    print(4)

async def func3():
    print(5)
    await asyncio.sleep(2)
    print(6)

tasks = [asyncio.ensure_future(func1()), asyncio.ensure_future(func2()), asyncio.ensure_future(func3())]
start = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print(end - start)

3. gevent

gevent 是对 greenlet 进行的封装,拥有强大的 monkey 机制。假设你不愿意修改原来已经写好的 Python 代码,但是又想充分利用 gevent 机制,那么你就可以用 monkey 来做到这一点。你所要做的就是在文件开头打一个 patch,那么它就会自动替换你原来的 thread、socket、time、multiprocessing 等代码,全部变成 gevent 框架。这一切都是由 gevent 自动完成的。注意这个 patch 是在所有 module 都 import 了之后再打,否则没有效果。甚至在编写的 Web App 代码的时候,不需要引入 gevent 的包,也不需要改任何代码,仅仅在部署的时候,用一个支持 gevent 的 WSGI 服务器,就可以获得数倍的性能提升。例如:

import gevent

def f1():
    for i in range(1,6):
        print('f1', i)
        gevent.sleep(0)

def f2():
    for i in range(6,11):
        print('f2', i)
        gevent.sleep(0)

t1 = gevent.spawn(f1)
t2 = gevent.spawn(f2)
gevent.joinall([t1, t2])

三、协程的运行原理

当程序运行时,操作系统为程序分配虚拟内存空间等资源,进程开始运行。协程在单线程内实现并发,通过执行者对控制流的 “主动让出” 和 “恢复”,实现任务交替执行。

协程的核心思想在于执行者对控制流的主动操作。在传统的线程模型中,线程的调度由操作系统内核负责,属于抢占式调度。而协程则是一种协作式调度方式,当前执行者在某个时刻主动让出控制流,并记住自身当前的状态,以便在控制流返回时能从上次让出的位置恢复执行。

例如,在 Python 中的协程实现中,当一个协程遇到 I/O 操作时,它可以主动 yield(让出)CPU 的执行权给其他协程。这样,其他协程就可以继续执行,而不会像传统的线程那样,在等待 I/O 操作完成时一直占用 CPU 资源。当 I/O 操作完成后,被让出的协程会被恢复执行,从上次让出的位置继续执行下去。

协程之间的切换不需要涉及任何 System Call(系统调用)或任何阻塞调用,切换由用户态控制。这使得协程相比线程节省了线程创建和切换的开销。同时,协程中不存在同时写变量的冲突,因此也就不需要用来守卫关键区块的同步性原语,比如互斥锁、信号量等,并且不需要来自操作系统的支持。

以 Python 中的 asyncio 模块为例,它使用事件循环来实现协程的调度。事件循环会不断地检查是否有协程可以执行,如果有协程处于就绪状态,就会将其调度执行。当一个协程执行到 await 关键字时,它会暂停执行,将控制权交还给事件循环,事件循环会选择其他就绪的协程执行。当被暂停的协程所等待的条件满足时,它会被恢复执行。

总的来说,协程的运行原理是通过执行者对控制流的主动让出和恢复,在单线程内实现任务的交替执行,提高了程序的执行效率,减少了资源的消耗。

四、协程的应用场景

协程在处理 IO 密集型程序时效率极高,有着广泛的应用场景。

(一)异步网络编程

在网络编程中,协程非常有用,特别是在处理大量并发连接的情况下。通过使用协程,可以实现高性能的异步网络服务器和客户端,处理并发的网络请求、响应和数据传输。例如,在一个实时聊天应用中,使用协程可以同时处理多个用户的连接和消息发送、接收,不会因为某个用户的网络延迟而影响其他用户的体验。假设一个聊天服务器需要同时处理 1000 个连接,使用协程可以在单线程内高效地切换任务,而不需要为每个连接分配一个线程,从而大大节省了系统资源。

(二)异步 IO 操作

协程适用于处理各种异步 IO 操作,如文件读写、数据库查询、网络请求等。通过使用协程,可以避免阻塞主线程,提高程序的吞吐量和响应能力。比如在一个数据处理系统中,需要从多个文件中读取数据并进行分析,如果使用传统的同步方式,每次读取文件都会阻塞程序,导致效率低下。而使用协程,可以在读取一个文件的同时,进行其他文件的读取准备工作,或者进行部分数据的处理,从而提高整体的处理速度。

(三)Web 开发框架

许多 Python Web 框架(如 Tornado、Sanic、FastAPI 等)使用协程作为并发处理请求的方式。这些框架利用协程的特性处理大量的并发请求,提供高性能的 Web 服务。例如,在一个高流量的电商网站中,使用这些框架可以快速处理用户的请求,提高网站的响应速度,提升用户体验。以 FastAPI 为例,它使用 async 和 await 关键字来定义异步路由处理函数,可以在处理一个请求的同时,准备接收下一个请求,从而提高服务器的并发处理能力。

(四)数据爬取和抓取

协程在数据爬取和抓取任务中非常常见。通过使用协程,可以并发地发送请求、解析响应和提取数据,从而加快数据获取的速度。在一个大规模的网页爬取任务中,使用协程可以同时发起多个请求,当一个请求等待响应时,切换到其他请求继续执行,提高爬取效率。假设要爬取 10000 个网页,使用协程可以在较短的时间内完成任务,而传统的同步方式可能需要很长时间。

(五)并发任务调度

协程可以用于并发任务的调度和执行。通过构建协程任务队列,并使用协程调度器进行任务的调度和执行,可以实现高效的并发任务处理。例如,在一个分布式计算系统中,可以使用协程来调度各个节点上的任务,实现任务的并行执行和高效管理。通过合理的任务调度,可以充分利用系统资源,提高计算效率。

五、协程的优缺点

(一)优点

  1. 执行效率高,切换开销小,避免多线程切换开销。

协程的执行效率极高,这是因为协程的切换由程序自身控制,而非像多线程那样由操作系统进行线程切换。根据搜索结果中的数据,和多线程相比,线程数量越多,相同数量的协程体现出的优势越明显。例如,在处理大量并发任务时,如果使用多线程,随着线程数量的增加,线程切换的开销会迅速增大,而协程则可以避免这种开销,从而提高程序的执行效率。

  1. 无需多线程锁机制,不存在同时写变量冲突。

由于只有一个线程,协程不存在同时写变量的冲突,在协程中控制共享资源不需要加锁,只需要判断数据的状态。这使得协程的执行更加高效,避免了多线程中因锁机制带来的资源消耗和性能下降。例如,在一个多线程程序中,为了保证共享资源的安全,需要使用锁机制来防止多个线程同时写入变量,这会增加程序的复杂性和开销。而在协程中,由于只有一个线程,不存在这种冲突,因此可以更加简洁地控制共享资源。

  1. 可以无限创建,适用于高并发场景。

协程可以无限创建,而线程和进程的数量是有限的。这使得协程在高并发场景下具有很大的优势,可以轻松应对大量的并发任务。例如,在一个高并发的网络服务器中,如果使用线程来处理每个连接,当连接数量很大时,线程的创建和管理会成为一个巨大的负担。而使用协程,可以根据需要创建大量的协程来处理连接,而不会受到线程数量的限制。

(二)缺点

  1. 复杂性较高,需要程序员手动控制程序运行状态。

协程较为特殊,需要程序员自己手动来控制程序运行状态,其复杂性相对较高,需要付出更多的努力。与传统的多线程编程相比,协程的编程模型更加复杂,需要程序员更加深入地理解协程的运行机制和控制流。例如,在协程中,程序员需要手动控制协程的切换时机和顺序,这需要对程序的逻辑有更深入的理解和把握。此外,协程的错误处理也比较复杂,需要程序员手动处理各种异常情况,这增加了程序的开发难度和维护成本。

  1. 错误处理机制不完善,异常处理和调试困难。

协程代码本身没有错误处理机制,导致代码的异常处理和调试变得更加困难。由于协程的执行是由程序自身控制的,当协程中出现异常时,异常的传播和处理方式与传统的多线程编程有所不同。例如,在多线程编程中,异常可以通过线程的异常处理机制进行捕获和处理。而在协程中,异常的传播和处理需要程序员手动控制,这使得异常处理变得更加复杂。此外,协程的调试也比较困难,因为协程的执行是在单线程内进行的,传统的多线程调试工具可能无法有效地调试协程程序。这需要程序员使用专门的调试工具和技术来进行协程的调试。

相关文章

Python启航:30天编程速成之旅(第26天)- pathlib

喜欢的条友记得关注、点赞、转发、收藏,你们的支持就是我最大的动力源泉。前期基础教程:「Python3.11.0」手把手教你安装最新版Python运行环境讲讲Python环境使用Pip命令快速下载各类库...

赶紧收藏!编程python基础知识,本文给你全部整理好了

想一起学习编程Python的同学,趁我粉丝少,可以留言、私信领编程资料~Python基础入门既然学习 Python,那么至少得了解下这门编程语言,知道 Python 代码执行过程吧。Python 的历...

恕我直言!你对Python里的import一无所知

文章来源:https://mp.weixin.qq.com/s/4WAOU_Lzy651IE-2zZSFfQ原文作者:写代码的明哥写 Python 通常我们会怎样导包?可能大部分情况下都是用 impo...

python+OPC UA,实现数据边缘采集

概要:边缘计算,是指在靠近物或数据源头的一侧,采用网络、计算、存储、应用核心能力为一体的开放平台,就近提供最近端服务。其应用程序在边缘侧发起,产生更快的网络服务响应,满足行业在实时业务、应用智能、安全...

[python] Python异步编程库asyncio使用指北

Python的asyncio模块提供了基于协程(coroutines)的异步编程(asynchronous programming)模型。作为一种高效的编程范式,异步编程允许多个轻量级任务并发执行,且...

Python 什么情况下会生成 pyc 文件?

作者:折木奉太郎来源:https://www.zhihu.com/question/30296617/answer/112564303作为 Python 爱好者,需要了解 .py 脚本的基本运行机制及...