🐴上我们就要进入到真正的协程概念学习了,这之前我们先了解一下C10M
问题。
C10M
:如何利用8核心CPU,64G内存,在10gbps的网络上保持1000万并发连接。
通过协程就能解决这类问题。
在我们正式学习协程含义之前,我们先看下面临的问题:
- 回调模式编码复杂度高
- 同步编程的并发性不高
- 多线程编程需要线程间同步(线程间同步我们使用锁
Lock
机制,但是锁机制会降低并发性能)
如果我们想解决👆的问题,我们应该需要做的是:
采用同步的方式编写异步的代码
使用单线程去切换任务
如何使用单线程切换任务呢,解决这个问题我们又有👇的挑战:
- 线程是由操作系统切换的,如果我们声明一个线程操作系统会帮我们自动切换。如果我们想在单线程中切换,就需要我们自己去调度任务。
- 实现了单线程中切换是不需要锁🔐机制的(锁的目的是实现线程间的同步,我们在一个线程内进行切换 意味着我们不再需要锁了),并且并发性高。我们类比线程内切换为函数切换,单线程内切换函数性能远高于线程切换,并发性更高。
在传统模式下的函数调用中A->B->C
:
函数只要执行一次就不会再执行了(即不会再进入函数了),因此是无法进行在函数之间切换。
当我们在单线程中想进行函数切换的时候,需要面临的一个挑战就是:如何实现一个可以暂停的函数,并且在适当的时候恢复该函数的继续执行。
为了解决上面的问题,就出现了协程。
协程的概念
协程
:的概念很多。我们可以理解为有多个入口的函数,可以暂停的函数(可以向暂停的地方传入值)。
生成器进阶-send
、close
和throw
方法
我们知道协程是一个可以暂停的函数,而生成器就是可以暂停的。如何将生成器和协程关联起来呢?
我们先学一下生成器的高级知识吧!
生成器的send方法
我们要学习的第一个知识点就是:
生成器不仅可以产出值,还可以接收值
1 | def gen_func(): |
第二个知识点:
启动生成器有两种方式:调用next()或者send()
1 | def gen_func(): |
注意
send
方法的作用:send
方法可以传递值进入生成器内部,同时还可以重启生成器执行到下一个yield
位置,next
是不能传递值的
正是生成器的send
方法使暂停了的方法能够重新运行起来,这是生成器变成协程的基础。
生成器的close方法
我们可以使用close
方法关闭生成器,关闭之后再次调用生成器就会报错。
1 | def gen_func(): |
为什么会出现上面的报错呢?
这就是close
方法导致的了,close
方法会在调用的yield
语句地方抛出一个异常GeneratorExit
。
而异常GeneratorExit
是继承BaseException
不是Exception
。
BaseException
比Exception
更基础
这个方法我们只要知道会关闭已经创建好的生成器即可。
生成器的throw方法
生成器的throw
方法是能够向生成器内部扔进去一个异常的。
1 | def gen_func(): |
扔进去的异常产生的位置是调用throw
方法之前的yield
位置处。
生成器进阶-yield from
我们通过yield from
来实现链式连接的功能
1 | my_list = [1, 2, 3] |
yield from
并不是简单地yield
而是,如果后面是可迭代对象,会依次yield
出可迭代对象里面的内容。
1 | def g1(iterable): |
下面我们看下yield from
的真正作用。
1 | def g1(gen): |
我们看下上面代码的关系:
main
为调用方,g1
为委托生成器,gen
为子生成器
yield from
会在调用方与子生成器之间建立一个双向通道,两者之间可以直接通信(调用方可以直接唤醒或传递信息给子生成器)
有了这个点,我们在将同步的思维运用到协程里面的时候才有可能。
我们看下老师写的一个例子:
1 | final_result = {} |
为什么要使用yield from
呢,为什么不直接在委托生成器中直接使用迭代器呢?
我们看下子生成器单独使用的时候会发生什么:
1 | def sales_sum(pro_name): |
在我们最后发送None
的时候报异常但是还是把数据返回了 。
如果我们不想展示出异常而是正常返回数据,需要处理一下异常。
1 | def sales_sum(pro_name): |
我们看到,当我们使用yield from
的时候,会自动帮我们处理好异常。
下面我们看下yield from
的具体实现原理:
1 | # pep380 |
1 | # 上面只是子生成器是一个理想的生成器的单一情况,当情况是下面的任一情况的时候,👆的逻辑就会有问题了。 |
1 | 看完代码,我们总结一下关键点: |
看完这个觉得yield from
真的是个好东西啊!
async和await
在3.5之前都是通过生成器来实现协程的。
python
为了使语义更加明确,就引入了async和await关键词用于定义原生的协程
1 | async def downloader(url): |
我们通过async
和await
实现的协程是不能使用next
激活的。
注意:
async
和await
是成对出现的,这async
中是不能使用yield
的。
我们可以把await
理解成yield from
。
在await
语句后面的对象其实是一个Awaitable
对象。
我们可以使用装饰器,将一个普通函数改变为Awaitable
对象。
1 | async def downloader(url): |
协程是需要运用到事件循环中的,下一小节我们就开始真正使用了。💪