python --- 协程

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循环遍历取值的过程就叫迭代

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

2.2 可迭代对象

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

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

2.3 迭代器对象

在类里面定义__iter__和__next__方法创建的对象就是迭代器对象
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_iterator


class MyIterator(object):
    def __init__(self, mylist):
        self.mylist = mylist
        self.current_index = 0
        result = isinstance(self, Iterator)
        print("MyIterator创建的对象是否是迭代器:", result)

    def __iter__(self):
        return self

    def __next__(self):
        if self.current_index < len(self.mylist):
            res = self.mylist[self.current_index]
            self.current_index += 1
            return res
        else:
            raise StopIteration

if __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, …

class Feibonaqie(object):
    def __init__(self,num):
        self.num = num
        self.a = 0
        self.b = 1
        self.current_index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.current_index < self.num:
            result = self.a
            self.a,self.b = self.b,self.a+self.b
            self.current_index +=1
            return result
        else:
            raise StopIteration

res = Feibonaqie(5)
print(list(res))

# [0, 1, 1, 2, 3]

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

3、生成器 Generator

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

3.1 创建方法

  • 使用元组tuple的推导式直接创建
res = (i*2 for i in range(5))
print(res)
print(tuple(res))
#<generator object <genexpr> at 0x00000236A6B86040>   生成器对象
#(0, 2, 4, 6, 8)
  • 使用yield关键字创建
def coroutine_example():
    while True:
        value = yield 0
        print(f'Received value: {value}')
        value = yield value + 1
        print(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来进行中断,达到异步功能!
def gen():
    while True:
        value = yield 0
        print(f'Received value: {value}')
        value = yield value + 1
        print(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相比得到的结果也是一样的

def generator1():
    total = 0
    while True:
        x = yield
        print(f'{total}+{x}')
        if not x:
            break
        total += x
    return total

def 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实现协程

# 使用同步方式编写异步功能
import time
import asyncio
@asyncio.coroutine # 标志协程的装饰器
def taskIO_1():
    print('开始运行IO任务1...')
    yield from asyncio.sleep(2)  # 假设该任务耗时2s
    print('IO任务1已完成,耗时2s')
    return taskIO_1.__name__
@asyncio.coroutine # 标志协程的装饰器
def taskIO_2():
    print('开始运行IO任务2...')
    yield from asyncio.sleep(3)  # 假设该任务耗时3s
    print('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() # 创建一个事件循环对象loop
    try:
        loop.run_until_complete(main()) # 完成事件循环,直到最后一个任务结束
    finally:
        loop.close() # 结束事件循环
    print('所有IO任务总耗时%.5f秒' % float(time.time()-start))

执行结果

开始运行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实现协程

import time
import asyncio
async def taskIO_1():
    print('开始运行IO任务1...')
    await asyncio.sleep(3)  # 假设该任务耗时3s
    print('IO任务1已完成,耗时3s')
    return taskIO_1.__name__
async def taskIO_2():
    print('开始运行IO任务2...')
    await asyncio.sleep(2)  # 假设该任务耗时2s
    print('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() # 创建一个事件循环对象loop
    try:
        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()返回的结果集是按照事件循环中的任务完成顺序排列的,所以其往往和原始
任务顺序不同
# 输出结果
开始运行IO任务2...
开始运行IO任务1...
IO任务2已完成,耗时2s
IO任务1已完成,耗时3s
协程无序返回值: taskIO_2
协程无序返回值: taskIO_1
所有IO任务总耗时3.00230

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

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

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

# 输出结果
开始运行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库提供了事件循环的支持。
  • 协程调度:事件循环会根据协程的状态和优先级调度协程的执行,使得程序能够在不同的协程之间切换执行,实现异步编程的效果。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值