python --- 协程

embedded/2024/9/24 16:29:53/

文章目录

    • 1、协程的概念
    • 2、迭代器
      • 2.1 迭代的概念
      • 2.2 可迭代对象
      • 2.3 迭代器对象
      • 2.4、迭代器应用场景
    • 3、生成器 Generator
      • 3.1 创建方法
    • 4、yield from结合@asyncio.coroutine实现协程
    • 5、使用async和await实现协程
    • 6、使用协程实现异步

1、协程的概念

协程Coroutine又称微线程,纤程也叫用户级线程。允许多个入口点用于暂停和恢复执行,但它的切换和调度不是操作系统控制的,而是程序员自身。这种特性使得协程在处理并发时更加高效,尤其是I/O密集型应用中。

python协程发展历程
最初通过生成器(Generators)提供了一种原始的协程支持,随后,在python3.5版本中引入了async和await关键字,这些关键字简化了协程的书写和使用,使得协程代码的可读性和可维护性大大提高.官方内置的asyncio标准库实现协程特性,还有三方库比如greenlet、 gevent、 Tornado

协程工作原理
python中的协程依赖于事件循环(Event Loop)来实现异步操作。事件循环负责监听和分发事件,如网络请求、文件IO等,当协程遇到await表达式时,它会将控制权交还给事件循环,同时挂起自身的执行。这样事件循环可以继续处理其他的任务,知道被挂起的循环可以继续执行时,事件循环再将控制权交回给该协程

Python的协程提供了一种高效、易用的异步编程模式,对于提高程序的并发处理能力和I/O性能具有重要意义。

2、迭代器

协程是基于生成器实现的,但他不等于协程
生成器就是内部含有yield语句的函数。使用生成器可以优雅的实现一个迭代器,那什么是迭代器呢?

2.1 迭代的概念

使用for循环遍历取值的过程就叫迭代

python">for i in [1,2,3,4,5]:print(i)

2.2 可迭代对象

在类里面定义__iter__方法,并使用该类创建的对象就是可迭代对象,简单点就是for循环取值的对象就是可迭代对象,比如 字符串、列表、元组、字典、集合、range都是

python">from collections.abc import Iterable
print(isinstance([1,2,3],Iterable))
# True

2.3 迭代器对象

在类里面定义__iter__和__next__方法创建的对象就是迭代器对象
python">from collections.abc import Iterable, Iterator# 自定义可迭代对象
class Mylist(object):def __init__(self):self.mylist = list()def append_item(self, item):self.mylist.append(item)def __iter__(self):my_iterator = MyIterator(self.mylist)return my_iteratorclass MyIterator(object):def __init__(self, mylist):self.mylist = mylistself.current_index = 0result = isinstance(self, Iterator)print("MyIterator创建的对象是否是迭代器:", result)def __iter__(self):return selfdef __next__(self):if self.current_index < len(self.mylist):res = self.mylist[self.current_index]self.current_index += 1return reselse:raise StopIterationif __name__ == '__main__':my_list = Mylist()my_list.append_item(1)my_list.append_item(2)result = isinstance(my_list,Iterable)print('可迭代对象:',result)my_it = iter(my_list)res = isinstance(my_it,Iterator)print('迭代器对象:',res)while True:try:value = next(my_it)print(value)except StopIteration:break#可迭代对象: True
#MyIterator创建的对象是否是迭代器: True
#迭代器对象: True
#1
#2
iter函数: 获取可迭代对象的迭代器,会调用可迭代对象身上的__iter__方法
next函数: 获取迭代器中下一个值,会调用迭代器对象身上的__next__方法

2.4、迭代器应用场景

迭代器最核心的功能就是可以通过next()函数的调用来返回下一个数据值。如果每次返回的数据值不是在一个已有的数据集合中读取的,而是通过程序按照一定的规律计算生成的,那么也就意味着可以不用再依赖一个已有的数据集合。

著名的斐波拉契数列(Fibonacci),数列中第一个数为0,第二个数为1,其后的每一个数都可由前两个数相加得到:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …

python">class Feibonaqie(object):def __init__(self,num):self.num = numself.a = 0self.b = 1self.current_index = 0def __iter__(self):return selfdef __next__(self):if self.current_index < self.num:result = self.aself.a,self.b = self.b,self.a+self.bself.current_index +=1return resultelse:raise StopIterationres = Feibonaqie(5)
print(list(res))# [0, 1, 1, 2, 3]

迭代器的作用就是是记录当前数据的位置以便获取下一个位置的值

3、生成器 Generator

简单来说:只要在def中有yield关键字的 就称为 生成器。生成器是一类特殊的迭代器,它不需要再像上面的类一样写__iter__()和__next__()方法

3.1 创建方法

  • 使用元组tuple的推导式直接创建
python">res = (i*2 for i in range(5))
print(res)
print(tuple(res))
#<generator object <genexpr> at 0x00000236A6B86040>   生成器对象
#(0, 2, 4, 6, 8)
  • 使用yield关键字创建
python">def coroutine_example():while True:value = yield 0print(f'Received value: {value}')value = yield value + 1print(f'Received value: {value}')c = coroutine_example()
print('result:',next(c))
print('result2:',c.send(2))
print('result4:',c.send(4))# 输出
#result: 0
#Received value: 2
#result2: 3
#Received value: 4
#result4: 0

通过上面的代码可以看到,我们不仅可以通过 next() 从生成器中不断获取新的值,还能通过 send() 给生成器传递值,生成器可以根据实际得到值生成新的值。从而支持更灵活和复杂的场景。

  • yield from
    为了更好地支持协程,生成器被增强,支持子生成器 Subgeberator。这样可以引用其他生成器更加灵活。只要是和IO任务类似的、耗费时间的任务都需要使用yield from来进行中断,达到异步功能!
python">def gen():while True:value = yield 0print(f'Received value: {value}')value = yield value + 1print(f'Received value: {value}')def ren():yield from gen()c = ren()
print('result:',next(c))
print('result2:',c.send(2))
print('result4:',c.send(4))

通过上面的代码,两个函数都是生成器,和使用yield相比得到的结果也是一样的

python">def generator1():total = 0while True:x = yieldprint(f'{total}+{x}')if not x:breaktotal += xreturn totaldef generator2():  # 委托生成器while True:total = yield from generator1()  # 子生成器print('总和:', total)def main():  # 调用方# g1 = generator1()# g1.send(None)# g1.send(1)# g1.send(2)# g1.send(None)g2 = generator2()next(g2)g2.send(1)g2.send(2)g2.send(None)if __name__ == '__main__':main()

在最后调用的时候,把g1的注释打开,g2注释掉,会发现报StopIteration异常并返回了最后total值是5。但如果把g1注释掉g2打开,会发现正常没有异常出现。其中total = yield from generator1()返回给total的值是generator1()最终的return total
yield from在其中还有一个关键的作用是:建立调用方和子生成器的通道,

在上述代码中main()每一次在调用send(value)时,value不是传递给了委托生成器generator2(),而是借助yield from传递给了子生成器generator1()中的yield
同理,子生成器中的数据也是通过yield直接发送到调用方main()中。

4、yield from结合@asyncio.coroutine实现协程

python"># 使用同步方式编写异步功能
import time
import asyncio
@asyncio.coroutine # 标志协程的装饰器
def taskIO_1():print('开始运行IO任务1...')yield from asyncio.sleep(2)  # 假设该任务耗时2sprint('IO任务1已完成,耗时2s')return taskIO_1.__name__
@asyncio.coroutine # 标志协程的装饰器
def taskIO_2():print('开始运行IO任务2...')yield from asyncio.sleep(3)  # 假设该任务耗时3sprint('IO任务2已完成,耗时3s')return taskIO_2.__name__
@asyncio.coroutine # 标志协程的装饰器
def main(): # 调用方tasks = [taskIO_1(), taskIO_2()]  # 把所有任务添加到task中done,pending = yield from asyncio.wait(tasks) # 子生成器for r in done: # done和pending都是一个任务,所以返回结果需要逐个调用result()print('协程无序返回值:'+r.result())if __name__ == '__main__':start = time.time()loop = asyncio.get_event_loop() # 创建一个事件循环对象looptry:loop.run_until_complete(main()) # 完成事件循环,直到最后一个任务结束finally:loop.close() # 结束事件循环print('所有IO任务总耗时%.5f秒' % float(time.time()-start))

执行结果

python">开始运行IO任务1...
开始运行IO任务2...
IO任务1已完成,耗时2s
IO任务2已完成,耗时3s
协程无序返回值:taskIO_2
协程无序返回值:taskIO_1
所有IO任务总耗时3.00209

使用:
@asyncio.coroutine装饰器是协程函数的标志,我们需要在每一个任务函数前加这个装饰器,并在函数中使用yield from

在同步IO任务的代码中使用的time.sleep(2)来假设任务执行了2秒。但在协程中yield from后面必须是子生成器函数,而time.sleep()并不是生成器,所以这里需要使用内置模块提供的生成器函数asyncio.sleep()。

5、使用async和await实现协程

python">import time
import asyncio
async def taskIO_1():print('开始运行IO任务1...')await asyncio.sleep(3)  # 假设该任务耗时3sprint('IO任务1已完成,耗时3s')return taskIO_1.__name__
async def taskIO_2():print('开始运行IO任务2...')await asyncio.sleep(2)  # 假设该任务耗时2sprint('IO任务2已完成,耗时2s')return taskIO_2.__name__
async def main(): # 调用方   result = await asyncio.gather(taskIO_1(), taskIO_2())print(result)# tasks = [taskIO_1(), taskIO_2()]  # 把所有任务添加到task中# done,pending = await asyncio.wait(tasks)# for i in done:#     print('协程无序返回值:', i.result())# for completed_task in asyncio.as_completed(tasks):#     resualt = await completed_task # 子生成器#     print('协程无序返回值:', resualt)if __name__ == '__main__':start = time.time()loop = asyncio.get_event_loop() # 创建一个事件循环对象looptry:loop.run_until_complete(main()) # 完成事件循环,直到最后一个任务结束finally:loop.close() # 结束事件循环print('所有IO任务总耗时%.5f秒' % float(time.time()-start))

执行过程:

  1. 先通过get_event_loop()获取了一个标准事件循环loop(因为是一个,所以协程是单线程)
  2. 通过run_until_complete(main())来运行协程(把调用方协程main()作为参数,使用过调用方来调用其他委托生成器)run_until_complete的特点就像该函数的名字,直到循环事件的所有事件都处理完才能完整结束。
  3. 在调用方协程
    使用asyncio.wait(tasks) 来获取awaitable objects即可等待对象的集合,其中tasks多个任务的列表。返回包含done,pending。done表示已经完成的任务列表,pending表示未完成的任务列表
 注:
①只有当给wait()传入timeout参数时才有可能产生pending列表。
②通过wait()返回的结果集是按照事件循环中的任务完成顺序排列的,所以其往往和原始
任务顺序不同
python"># 输出结果
开始运行IO任务2...
开始运行IO任务1...
IO任务2已完成,耗时2s
IO任务1已完成,耗时3s
协程无序返回值: taskIO_2
协程无序返回值: taskIO_1
所有IO任务总耗时3.00230

使用asyncio.gather(taskIO_1(), taskIO_2()),它不仅通过await返回仅仅一个结果集,而且结果集的结果顺序是传入的任务的原始顺序

python"># 输出结果
开始运行IO任务1...
开始运行IO任务2...
IO任务2已完成,耗时2s
IO任务1已完成,耗时3s
['taskIO_1', 'taskIO_2']
所有IO任务总耗时3.00220

使用asyncio.as_completed(tasks),管理一个协程列表,当任务集合中的某个任务率先执行完毕时,直接通过await关键字返回任务结果,可见返回的结果的顺序也是按照完成任务的顺序排列的。

python"># 输出结果
开始运行IO任务2...
开始运行IO任务1...
IO任务2已完成,耗时2s
协程无序返回值: taskIO_2
IO任务1已完成,耗时3s
协程无序返回值: taskIO_1
所有IO任务总耗时3.00337

使用as_completed(tasks)和wait(tasks)相同之处是返回结果的顺序是协程的完成顺序,这与gather()恰好相反。而不同之处是as_completed(tasks)可以实时返回当前完成的结果,而wait(tasks)需要等待所有协程结束后返回的done去获得结果。

6、使用协程实现异步

协程是一种轻量级的线程,可以在执行过程中暂停并恢复。在Python中,协程通过async和await关键字实现,这也是异步编程 的关键。协程的实现包括以下几点:

  • 异步函数定义:使用async def定义的函数可以在函数内部使用await关键字来挂起函数的执行,等待异步操作完成。
  • 事件循环:异步编程通常需要一个事件循环来调度协程的执行,Python中的asyncio库提供了事件循环的支持。
  • 协程调度:事件循环会根据协程的状态和优先级调度协程的执行,使得程序能够在不同的协程之间切换执行,实现异步编程的效果。

异步事件循环的作用在于提供一个统一的调度器,使得异步任务能够在不同的协程之间切换执行,实现非阻塞的并发处理。


http://www.ppmy.cn/embedded/90770.html

相关文章

书生大模型实战营——入门岛第3关

任务1-破冰活动 从 https://github.com/InternLM/Tutorial fork一个分支到自己的仓库&#xff1a; 在自己的仓库下获取仓库链接&#xff1a; 下载项目代码到本地&#xff1a; git clone https://github.com/trunks2008/Tutorial.git查看当前分支&#xff0c;是我们要使用的ca…

用Python打造精彩动画与视频, 5.2 安装和设置Manim

5.2 安装和设置Manim Manim 是一个强大的动画库&#xff0c;用于创建高质量的数学动画。它最初由 3Blue1Brown 的 Grant Sanderson 开发&#xff0c;并被广泛用于教育和展示。以下是安装和设置 Manim 的详细步骤。 5.2.1 安装Manim Manim 需要 Python 环境和一些依赖库。在安…

PXE——安装,配置,测试(rhel7环境下)

什么是PXE PXE&#xff08;Preboot eXecution Environment&#xff0c;预启动执行环境&#xff09;允许计算机在开机时从网络而非本地硬盘或其他存储设备启动。这种技术主要用于网络启动和自动化安装系统&#xff0c;尤其在需要为大量计算机同时安装操作系统的情况下非常有用。…

图片怎么重命名批量修改?教你几种批量重命名小妙招

图片已成为我们工作、学习和生活中不可或缺的一部分。然而&#xff0c;随着图片数量的激增&#xff0c;如何高效地管理和整理这些图片成为了一个挑战。特别是当需要批量重命名图片时&#xff0c;手动操作不仅费时费力&#xff0c;还容易出错。下面交给大家几种图片批量重名方法…

“程序员面试中的“八股文”:知识与实践的平衡“

"八股文"在程序员面试中通常指的是一系列常见的面试问题和答案&#xff0c;这些问题往往围绕计算机科学的基础知识、编程语言特性、算法和数据结构、设计模式、系统架构等。这些内容是程序员必须掌握的核心知识&#xff0c;也是评估候选人专业能力的重要标准。 首先…

备受争议!如果这本双1区TOP也称“水刊”,那让我发一篇这样的“水一水”吧!

【SciencePub学术】本期&#xff0c;小编给大家推荐1本计算机领域的SCI期刊。隶属于Elsevier 出版社旗下&#xff0c;虽然今年的影响因子略有回落&#xff0c;但仍有7.2分&#xff0c;稳居中科院1区TOP地位&#xff0c;最重要的是国人友好&#xff0c;投稿难度也较小&#xff0…

live2d C++ sdk 分析

从网上收集的live2d模型 可以自己添加新的表情/姿态&#xff0c;不过只能是静态&#xff08;虽然可以用渐变过渡实现动态效果&#xff09; 前提要下载官方live2d应用&#xff08;免费版即可&#xff09; 双击moc3&#xff0c;会在Cubism Viewer中打开&#xff08;这是live2d…

Vue + View-ui-plus Upload实现手动上传

本文实现Vue Upload组件多文件手动上传&#xff0c;支持上传图片&#xff08;image&#xff09;、压缩文件(zip/rar)、表格(excel)、pdf 一、dom结构 <Row><Col :span"19"></Col><Col :span"2"><div class"ivu-btn-uplo…