python gevent asyncio_Python 协程:yield,greenlet,gevent,asyncio

进程

进程是资源分配的最小单位,拥有独立的内存空间,有寄存器信息、堆、栈、数据段、代码段、虚拟内存、文件句柄、IO 状态、信号信息等等内容,不同进程的切换开销比较大,同时进程比较独立稳定,通常不受其他进程影响

进程间的通信有管道(Pipe)、消息队列(Message Queue)、信号量(Semaphore)、共享内存(Shared Memory)、套接字(Socket)等等

线程

线程是系统调度的最小单位,只需要保存自己的栈、寄存器信息等少量内容,一个进程至少要有一个线程,不同线程的切换开销比进程切换要小很多,但线程不够独立稳定,容易受进程和其他线程的影响

由于不同线程都是共享同一段内存,线程间通信直接使用共享内存,就是使用全局定义的变量即可,另外不同的线程间通常还需要通过锁实现同步、互斥等功能

协程

进程和线程都是操作系统调度的,虽然线程切换开销比进程要小,但如果是频繁切换,依然会严重影响性能

操作系统通常在三种情况下会进行切换

程序运行时间比较长

有更高优先级的程序抢占

程序发生了阻塞

在很多网络应用中,会同时接受大量请求,这些请求计算量很小,主要的时间是耗在 IO 上了,并且最主要是网络的 IO 时间,导致了频繁的 IO 阻塞和线程切换,严重影响性能

协程就是为了解决以 IO 为主要开销的程序,在高并发场景下的性能问题

在一个线程内可以运行多个协程,当一个协程调用了需要 IO 阻塞的命令时,会使用异步 IO 的方式,避免触发操作系统进行切换,然后继续执行另一个协程,由于是在同一个线程内实现,切换开销非常小,性能会有很大提升

注意协程在 IO 并发量很大的情况下作用才比较明显,因为只有这种情况下才能保证随时有异步 IO 准备就绪可以执行的,如果 IO 量很小,比如 10 分钟才有一条请求,那做了异步操作后,还是得等待这个异步 IO 就绪,照样会导致线程切换

注意协程只在一种情况下会切换:IO 调用

这个功能需要由程序框架实现,对操作系统是透明的,对应用程序也是透明的,这样既避免了以 IO 为主要开销的程序在高并发时频繁地触发多线程的切换,又不增加应用程序开发的工作量

在 Go 语言中,这个功能是原生的,Go 语言本身就实现了这个功能,在语法层面上就支持

在 Python 语言中,这个功能由 gevent 包提供支持

下面主要讲 Python 的协程

yield

yield 是为了生成器使用的,比如下面的代码

def f(max):

n = 1

while n <= max:

yield n*n

n = n + 1

for i in f(5):

print(i)

如果不使用 yield,那么函数 f 就需要返回一个 list,如果 max 非常大,那么就需要创建一个很大的内存在放这个 list,而在使用了 yield 后,函数被当成迭代器,f(5) 返回的是一个迭代器,for 语句每次取值的时候触发迭代器,迭代器执行到 yield 命令时返回 n*n 并停止执行,直到 for 下一次取值,迭代器再从 n = n + 1 继续执行,这样无论 max 多大,内存的使用都是恒定的

再举一个例子

def f():

n = 1

print("f function with yield inside")

while True:

msg = yield n

print("msg: ", msg)

n = n + 1

iter = f()

print("before invoke next")

print("receive: ", next(iter))

print("after invoke next")

print("receive: ", next(iter))

返回的是

before invoke next

f function with yield inside

(‘receive: ‘, 1)

after invoke next

(‘msg: ‘, None)

(‘receive: ‘, 2)

可以看到调用 iter = f() 的时候没有打印任何信息出来,即 f() 函数其实没有被执行,而是返回了一个迭代器,当执行 next(iter) 函数时 (next 是 python 内置函数),f() 函数才被执行,并且这里只执行到 yield n 就停止继续执行并将 n 作为结果返回 (这里连 msg 的赋值都没执行,后面会进一步讲到),等下一个 next 函数时,会从 msg 的赋值开始继续执行,直到再次遇见 yield,如果迭代器已经执行完,那么 next 函数会报 StopIteration 异常

继续下一个例子

def f():

n = 1

print("f function with yield inside")

while True:

msg = yield n

print("msg: ", msg)

n = n + 1

iter = f()

print("before invoke next")

print("receive: ", next(iter))

print("after invoke next")

print("receive: ", iter.send("from outside"))

这里把第二个 next 换成调用迭代器的 send 函数

返回的是

before invoke next

f function with yield inside

(‘receive: ‘, 1)

after invoke next

(‘msg: ‘, ‘from outside‘)

(‘receive: ‘, 2)

和上一个例子的唯一区别就是打印的 msg 不是 None 而是 send 函数的参数,send 函数和 next 一样会触发迭代器继续执行,但同时会将参数作为 yield 语句的结果赋值给 msg

下面用 yield 模拟协程

def f_0():

n = 5

while n >= 0:

print(‘[f_0] ‘ + str(n))

yield

n = n - 1

def f_1():

m = 3

while m >= 0:

print(‘[f_1] ‘ + str(m))

yield

m = m - 1

iter_list = [f_0(), f_1()]

while True:

for it in iter_list:

try:

next(it)

except:

iter_list.remove(it)

if len(iter_list) == 0:

break

返回结果为

[f_0] 5

[f_1] 3

[f_0] 4

[f_1] 2

[f_0] 3

[f_1] 1

[f_0] 2

[f_1] 0

[f_0] 1

[f_0] 0

可以看到实现了两个函数不断切换的功能,但代码写起来麻烦点

greenlet

greenlet 是底层实现了原生协程的 C 扩展库

from greenlet import greenlet

def f_0():

n = 5

while n >= 0:

print(‘[f_0] ‘ + str(n))

parent_greenlet.switch()

n = n - 1

def f_1():

m = 3

while m >= 0:

print(‘[f_1] ‘ + str(m))

parent_greenlet.switch()

m = m - 1

def parent():

while True:

for task in greenlet_list:

task.switch()

if task.dead:

greenlet_list.remove(task)

if len(greenlet_list) == 0:

break

parent_greenlet = greenlet(parent)

greenlet_list = [greenlet(f_0, parent_greenlet), greenlet(f_1, parent_greenlet)]

parent_greenlet.switch()

返回

[f_0] 5

[f_1] 3

[f_0] 4

[f_1] 2

[f_0] 3

[f_1] 1

[f_0] 2

[f_1] 0

[f_0] 1

[f_0] 0

switch 也可以传值,根据程序运行情况会传给函数参数,或是传给 switch 的返回

def test1(x, y):

z = gr2.switch(x+y)

print(z)

def test2(u):

print(u)

gr1.switch(42)

print "end"

gr1 = greenlet(test1)

gr2 = greenlet(test2)

gr1.switch("hello", " world")

返回

hello world

42

可以看到没有打印出 end,因为没指定 parent,默认有一个结束就返回 main,另一个就不会执行了,如果有指定 parent,则结束后会返回 parent

gevent

greenlet 写起来也比较复杂,并且 greenlet 只实现了协程,却没有实现捕获 IO 操作并进行切换的功能,实际上一般的计算并不需要协程的切换,性能没什么影响,只有在高并发 IO 操作时能切换程序,其性能才会有较大提升

gevent 基于 greenlet,使用了包括 linux 的 epoll 事件监听机制在内的许多优化措施,以提升高并发 IO 的性能,比如当一个 greenlet 程序需要做网络 IO 操作时,就将其注册为异步监听,并切换到其他 greenlet 程序,等 IO 完成,在适当的时候会再切回来继续执行,这样当 IO 很高时,可以让程序一直在运行,而不是把时间耗在 IO 等待上,同时又能避免线程的切换开销

import gevent

def f_0(param):

n = param

while n >= 0:

print(‘[f_0] ‘ + str(n))

gevent.sleep(0.1)

n = n - 1

def f_1(param):

m = param

while m >= 0:

print(‘[f_1] ‘ + str(m))

gevent.sleep(0.1)

m = m - 1

g1 = gevent.spawn(f_0, 5)

g2 = gevent.spawn(f_1, 3)

gevent.joinall([g1, g2])

返回

[f_0] 5

[f_1] 3

[f_0] 4

[f_1] 2

[f_0] 3

[f_1] 1

[f_0] 2

[f_1] 0

[f_0] 1

[f_0] 0

可以看到代码简洁清楚,和正常程序相比,就是用 gevent.sleep() 替换了 time.sleep() 是 gevent 能在需要阻塞的地方做协程的切换

实际上还可以更简单

import time

import gevent

from gevent import monkey

monkey.patch_all()

def f_0(param):

n = param

while n >= 0:

print(‘[f_0] ‘ + str(n))

time.sleep(0.1)

n = n - 1

def f_1(param):

m = param

while m >= 0:

print(‘[f_1] ‘ + str(m))

time.sleep(0.1)

m = m - 1

g1 = gevent.spawn(f_0, 5)

g2 = gevent.spawn(f_1, 3)

gevent.joinall([g1, g2])

通过 monkey.patch_all() 打补丁,可以拦截到大量 IO 操作,比如 time sleep,http request 等,对其做异步执行,并切换协程,这种做法允许原函数不用修改就能直接使用,对程序开发人员而言,协程就是透明的,不用特意修改代码,交给 gevent 打理就可以

asyncio

Python 3.6 中正式引入了 asyncio 库作为 python 标准库

最主要是 async 和 await 关键字

async 用来声明一个函数为异步函数,可以被挂起

await 用来用来声明程序被挂起,await 后面只能跟异步程序或有 __await__ 属性的对象

import asyncio

import aiohttp

async def f_0(param):

n = param

while n >= 0:

print(‘[f_0] ‘ + str(n))

await asyncio.sleep(0.1)

n = n - 1

async def f_1(param):

m = param

while m >= 0:

print(‘[f_1] ‘ + str(m))

await asyncio.sleep(0.1)

m = m - 1

loop = asyncio.get_event_loop()

tasks = [

f_0(5),

f_1(3)

]

loop.run_until_complete(asyncio.wait(tasks))

loop.close()

返回

[f_0] 5

[f_1] 3

[f_0] 4

[f_1] 2

[f_0] 3

[f_1] 1

[f_0] 2

[f_1] 0

[f_0] 1

[f_0] 0

另一个例子

import asyncio

import aiohttp

async def request(session, url):

async with session.get(url) as response:

return await response.read()

async def fetch(url):

await asyncio.sleep(1)

async with aiohttp.ClientSession() as session:

html = await request(session, url)

print(html)

url_list = [

"http://www.qq.com",

"http://www.jianshu.com",

"http://www.cnblogs.com"

]

tasks = [fetch(url) for url in url_list]

loop = asyncio.get_event_loop()

loop.run_until_complete(asyncio.wait(tasks))

loop.close()

可以看到需要加上 async 表示支持异步调用,并且要用 await 指定被挂起的地方

如果 await 指定的代码无法被挂起的话,是会出错的

并且需要使用特定的异步方法,或是类

相比较而言 gevent 则可以做到对程序透明

一个正常的同步程序,不需要任何修改就可以通过 gevent 实现异步

但 gevent 是借助三方包,asyncio 则是 python 标准库,在语法层面提供支持

原文:https://www.cnblogs.com/moonlight-lin/p/12732813.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值