Python之路(第四十七篇) 协程:greenlet模块\gevent模块\asyncio模块

时间:2021-10-12 04:41:32

一、协程介绍

协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。

协程相比于线程,最大的区别在于,协程不需要像线程那样来回的中断切换,也不需要线程的锁机制,因为线程中断或者锁机制都会对性能问题造成影响,所以协程的性能相比于线程,性能有明显的提高,尤其在线程越多的时候,优势越明显。

协程的好处:

  1. 无需线程上下文切换的开销

  2. 无需原子操作锁定及同步的开销 "原子操作(atomic operation)是不需要synchronized",所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。视作整体是原子性的核心。

  3. 方便切换控制流,简化编程模型

  4. 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

缺点:

  1. 无法利用多核资源:协程的本质是个单线程,它不能同时将单个 CPU 的多个核用上,协程需要和进程配合才能运行在多 CPU 上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是 CPU 集型应用。

  2. 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

总结协程特点:

  1. 必须在只有一个单线程里实现并发

  2. 修改共享数据不需加锁

  3. 用户程序里自己保存多个控制流的上下文栈

  4. 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))

Python2.x协程

类库:

  • yield

  • greenlet

  • gevent

Python3.x协程

  • asyncio

Python3.x系列的gevent用法和python2.x系列是一样的

在学习前,我们先来理清楚同步/异步的概念

·同步是指完成事务的逻辑,先执行第一个事务,如果阻塞了,会一直等待,直到这个事务完成,再执行第二个事务,顺序执行。。。也称作串行执行。

·异步是和同步相对的,异步是指在处理调用这个事务的之后,不会等待这个事务的处理结果,直接处理第二个事务去了,通过状态、通知、回调来通知调用者处理结果。也称作并行执行。

二、greenlet模块

第三方模块,可以在pycharm中选择虚拟环境安装,

也可以通过 pip install greenlet 安装

greenlet 通过 greenlet(func) 启动一个协程,通过 switch() 手动切换程序的执行

示例

from greenlet import greenlet

def func1(name):
print("%s from func1"%name) #2执行这一句
g2.switch("jack") #3切换执行func2(),第一次执行要传入参数保存现在执行的状态
print("from func1 end") #6执行这一句
g2.switch()#7切换执行play(),保存现在执行的状态

def func2(name):
print("%s from func2"%name) #4执行这一句
g1.switch() #5切换执行func1(),保存现在执行的状态
print("from func2 end") #8执行这一句

g1 = greenlet(func1)
g2 = greenlet(func2)
g1.switch("nick") #1执行func1(),在switch()里传参数 ,注意与一般的线程、进程传参方式的不同
#可以在第一次switch时传入参数,以后都不需要

  


分析:就是通过创建greenlet(func)对象,通过对象的switch()方法转移程序执行的不同步骤,但是这里无法自动识别IO后自动切换。

三、gevent模块

gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是greenlet, 它是以C扩展模块形式接入Python的轻量级协程。

安装 pip3 install gevent 或者在pycharm中选择虚拟环境安装

用法

#用法
g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如func1,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数func1的

g2=gevent.spawn(func2)

g1.join() #等待g1结束

g2.join() #等待g2结束

#或者上述两步合作一步:gevent.joinall([g1,g2])

g1.value#拿到func1的返回值

  

示例

import gevent


def func1():
print('from func1: 1')
gevent.sleep(0)
print('from func1: 2')
gevent.sleep(1)


def func2():
print('from func2: 1')
gevent.sleep(2)
print('from func2: 2')


def func3():
print('from func3: 1')
gevent.sleep(1)
print('from func3: 2')


gevent.joinall([
gevent.spawn(func1),
gevent.spawn(func2),
gevent.spawn(func3),
])

  

输出结果

from func1: 1
from func2: 1
from func3: 1
from func1: 2
from func3: 2
from func2: 2

  

分析:可以从输出结果看到程序不断的在三个函数中跳跃执行,遇到IO了就去执行另外的函数,但是请注意一点

gevent.sleep() 是用于模仿 IO 操作的,实际使用中不需要 gevent.sleep(),这里如果单纯执行上述代码的话,gevent模块也是只能识别 gevent.sleep()产生的IO,而对系统产生的IO或者网络IO之类无法识别,所有需要打上补丁,使得gevent模块识别其他IO

gevent是不能直接识别的需要用下面一行代码,打补丁

要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头

示例

需求:爬取三个网站并打印网页字符串长度


from gevent import monkey;monkey.patch_all()
# 把当前程序的所有 IO 操作标记起来,否则模块无法知道 IO 操作
import gevent
import time
import requests


def get_page(url):
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36'
}
page_text = requests.get(url=url, headers=headers).text
print('网站长度', len(page_text))


def main():
urls = [
'https://www.sogou.com',
'https://cn.bing.com',
'https://cnblogs.com/Nicholas0707/',
]
time_start = time.time()
for url in urls:
get_page(url)

print('同步耗时:', time.time() - time_start)

print("-"*50)
async_time_start = time.time()
gevent.joinall([
gevent.spawn(get_page, 'https://www.sogou.com'),
gevent.spawn(get_page, 'https://cn.bing.com'),
gevent.spawn(get_page, 'https://cnblogs.com/Nicholas0707/'),
])
print('异步协程耗时:', time.time() - async_time_start)


if __name__ == '__main__':
main()

  

输出结果


网站长度 23795
网站长度 130248
网站长度 13761
同步耗时: 2.5321450233459473
--------------------------------------------------
网站长度 23795
网站长度 130221
网站长度 13761
异步协程耗时: 0.36602067947387695

  

分析:从结果可以看出采用协程异步明显更快

四、asyncio模块

asyncio是Python3.4(2014年)引进的标准库,直接内置了对IO的支持。

python2x没有加这个库,python3.5又加入了async/await特性,python3.7新增了asyncio.run() api来执行异步函数.

协程示例

先简单看一个协程示例

运行协程函数的第一种方式(loop.run_until_complete())

#python 3.7+,本次测试环境python3.8

import asyncio,time


async def fun(): #定义一个协程函数
print('hello')
await asyncio.sleep(1) #模拟IO操作,等待调用
print('word')


if __name__ == '__main__':
begin = time.time()
# 创建一个事件loop
loop = asyncio.get_event_loop()
# 将协程函数加入到事件循环loop,并启动事件循环
loop.run_until_complete(fun())
loop.close()
print('用时共计',time.time()-begin)
print(fun)
print(loop)

  

输出结果

hello
word
用时共计 1.0010573863983154
<function fun at 0x00000000022CD0D0>
<ProactorEventLoop running=False closed=True debug=False>

  

上面代码等同于下面(不推荐使用,python3.8已经不支持此写法了)

##python 3.7,本次测试环境python3.7

import asyncio,time

@asyncio.coroutine #这种写法在python3.8之后被抛弃了
def fun(): #定义一个协程函数
print('hello')
yield from asyncio.sleep(1) #模拟IO操作,等待调用
print('word')


if __name__ == '__main__':
begin = time.time()
# 创建一个事件loop
loop = asyncio.get_event_loop()
# 将协程函数加入到事件循环loop,并启动事件循环
loop.run_until_complete(fun())
loop.close()
print('用时共计',time.time()-begin)

  



分析:使用async关键字定义一个协程函数,用asyncio.get_event_loop()创建一个事件循环,然后使用run_until_complete将协程注册到事件循环,并启动事件循环。

运行协程函数的第二种方式( asyncio.gather()---asyncio.run())

示例

# ## python 3.7+,本次测试环境python3.8
#
import asyncio,time


async def foo():
print('start foo')
await asyncio.sleep(1)
print('end foo')
return 'foo'

async def bar():
print('start bar')
await asyncio.sleep(2)
print('end bar')
return ('1','2')

async def main():
res = await asyncio.gather(foo(), bar())
#同时将两个异步函数对象加入事件循环,
# 但并不运行,等待调用。
print(res)

if __name__ == '__main__':
begin = time.time()
asyncio.run(main())
print('共计用时',time.time()-begin)
# 执行协程事件循环并返回结果。

  

输出结果

start foo
start bar
end foo
end bar
['foo', ('1', '2')]
共计用时 2.003114700317383

  

分析:如果要同时异步执行两个异步函数,需要用asyncio.gather(fun1(), fun2())将两个异步函数对象加入事件循环,这里不用显示的创建异步事件循环,因为asyncio.gather()方法中如果检测到你没有创建异步事件循环会自动帮你创建,见源代码


def gather(*coros_or_futures, loop=None, return_exceptions=False):
"""..."""
if not coros_or_futures:
if loop is None:
loop = events.get_event_loop()
else:
warnings.warn("The loop argument is deprecated since Python 3.8, "
"and scheduled for removal in Python 3.10.",
DeprecationWarning, stacklevel=2)

  

启动事件循环是通过 asyncio.run()方法进行启动

运行协程函数的第三种方式( asyncio.create_task()---asyncio.run())

## python 3.7+,本次测试环境python3.8
import asyncio,time


async def say_after(delay, what):
await asyncio.sleep(delay)
print(f"{what} at {time.strftime('%X')}")


async def main():
task1 = asyncio.create_task(
say_after(1, 'hello')) #创建任务事件,异步函数加入参数,
task2 = asyncio.create_task(
say_after(2, 'world'))
print(f"started at {time.strftime('%X')}")
await task1 #将任务事件加入异步事件循环,等待调用
await task2

print(f"finished at {time.strftime('%X')}")


if __name__ == '__main__':
begin = time.time()
asyncio.run(main()) #启动异步事件循环
print('共计用时',time.time()-begin)

  

输出结果

started at 20:01:51
hello at 20:01:52
world at 20:01:53
finished at 20:01:53
共计用时 2.002114772796631

  

分析:通过asyncio.create_task()创建等待异步执行的任务事件,这里也是自动创建了事件循环loop,

源码

def create_task(coro, *, name=None):
"""...
"""
loop = events.get_running_loop()

  

然后使用await将任务事件加入异步事件循环。

关于asyncio的一些关键字的说明:

  • event_loop 事件循环:程序开启一个无限循环,把一些函数注册到事件循环上,当满足事件发生的时候,调用相应的协程函数

  • coroutine 协程:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。

  • task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含了任务的各种状态

  • future: 代表将来执行或没有执行的任务的结果。它和task上没有本质上的区别

  • async/await 关键字:python3.5用于定义协程的关键字,async定义一个协程,await用于挂起阻塞的异步调用接口,等待调用。

  • sleep:暂停执行此任务,为事件循环分配要竞争的任务,并且它(事件循环)监视其所有任务的状态并从一个任务切换到另一个,这里是模拟io任务花费的时间。

asyncio方法

"""
Asyncio.get_event_loop()

返回一个事件循环对象,是asyncio.Baseeventloop的实例

Abstracteventloop.stop()

停止运行事件循环


Abstracteventloop.run_forever()

一直运行直到stop()

Abstracteventloop.run_until_complete(future)

运行直至future对象运行完

Abstracteventloop.close()

关闭事件循环

Abstracteventloop.is_running()

返回事件循环的是否运行

asyncio.gather(*aws, loop=None, return_exceptions=False)
同时在协程事件循环中运行定义的异步函数对象


task = asyncio.create_task(func());task.cancel()
请求取消任务。调用它将导致Task将CancelledError异常抛出到协程事件循环中。

"""

  

为异步函数绑定回调函数

## python 3.7+,本次测试环境python3.8
import asyncio

async def fun():
print('hello word')
return 'nick'


def callback(future):
print('Callback: ', future.result()) # 通过result()方法获得异步函数的返回值


loop = asyncio.get_event_loop() # 创建异步事件循环
task = loop.create_task(fun()) # 将异步函数加入loop
task.add_done_callback(callback) # 添加回调函数
loop.run_until_complete(task)

  

输出结果

hello word
Callback: nick

  

示例二

## python 3.7+,本次测试环境python3.8
import asyncio

async def fun():
print('hello')
await asyncio.sleep(1)
print('fun --end')
return 'nick'
async def bar():
print('word')
await asyncio.sleep(2)
print('bar --end')
return 'jack'


def callback(future):
print('Callback: ', future.result()) # 通过result()方法获得异步函数的返回值


async def main():
loop = asyncio.get_event_loop() # 创建异步事件循环
task1 = loop.create_task(fun()) # 将异步函数加入loop
task2 = loop.create_task(bar()) # 将异步函数加入loop
task1.add_done_callback(callback) # 添加回调函数
task2.add_done_callback(callback) # 添加回调函数
await task1
await task2

if __name__ == '__main__':
asyncio.run(main())

  

输出结果

hello
word
fun --end
Callback: nick
bar --end
Callback: jack

  

分析:通过add_done_callback方法给task任务添加回调函数,当task(也可以说是coroutine)执行完成的时候,就会调用回调函数,通过result()方法获得异步函数的返回值。

Python之路(第四十七篇) 协程:greenlet模块\gevent模块\asyncio模块的更多相关文章

  1. Python之路【第十七篇】:Django【进阶篇 】

    Python之路[第十七篇]:Django[进阶篇 ]   Model 到目前为止,当我们的程序涉及到数据库相关操作时,我们一般都会这么搞: 创建数据库,设计表结构和字段 使用 MySQLdb 来连接 ...

  2. Python之路【第十七篇】:Django之【进阶篇】

    Python之路[第十七篇]:Django[进阶篇 ]   Model 到目前为止,当我们的程序涉及到数据库相关操作时,我们一般都会这么搞: 创建数据库,设计表结构和字段 使用 MySQLdb 来连接 ...

  3. Python之路【第十七篇】:Django【进阶篇】

    Python之路[第十七篇]:Django[进阶篇 ]   Model 到目前为止,当我们的程序涉及到数据库相关操作时,我们一般都会这么搞: 创建数据库,设计表结构和字段 使用 MySQLdb 来连接 ...

  4. 网络编程基础--协程--greenlet切换---gevent自动识别 IO ---

    协程: 1 单线程来实现并发---协程: 协程:是单线程下的并发,又称微线程,纤程.英文名Coroutine.一句话说明什么是线程:协程是一种用户态的轻量级线程, 即协程是由用户程序自己控制调度的 只 ...

  5. Python之路【第十七篇】&colon;Python并发编程&vert;协程

    一.协程 协程,又叫微线程,纤程.英文名Coroutine.协程本质上就是一个线程 优点1:协程极高的执行效率.因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线 ...

  6. Python之路&comma;进程、线程、协程篇

      本节内容 进程.与线程区别 cpu运行原理 python GIL全局解释器锁 线程 语法 join 线程锁之Lock\Rlock\信号量 将线程变为守护进程 Event事件 queue队列 生产者 ...

  7. Python之路,第十七篇:Python入门与基础17

    python3  面向对象编程 面向对象编程 什么是对象? 一切皆对象: 面向对象是思想: 描述思想的语言有:C++/Java/Python/Swift/C# 两个概念: 类  class 对象 ob ...

  8. python之路 线程、进程、协程、队列、python-memcache、python-redis

    一.线程 Threading用于提供线程相关的操作,线程是应用程序中工作的最小单元. #!/usr/bin/env python # -*- coding:utf-8 -*- import threa ...

  9. 洗礼灵魂,修炼python(90)-- 知识拾遗篇 —— 协程

    协程 1.定义 协程,顾名思义,程序协商着运行,并非像线程那样争抢着运行.协程又叫微线程,一种用户态轻量级线程.协程就是一个单线程(一个脚本运行的都是单线程) 协程拥有自己的寄存器上下文和栈.协程调度 ...

随机推荐

  1. JUnit报错:java&period;lang&period;ClassNotFoundException&colon;

    只要把Java--------compiler-------building-------Buil path problems ------- incomplete build path 和 Circ ...

  2. 【AngularJs】---JSONP跨域访问数据传输

    大家会自然想到只有一个字母之差的JSON吧~ JSON(JavaScript Object Notation)和JSONP(JSON with Padding)虽然只有一个字母的差别,但其实他们根本不 ...

  3. javasrcipt日期一些方法和格式转化

    Js获取当前日期时间及其它操作 var myDate = new Date();myDate.getYear();        //获取当前年份(2位)myDate.getFullYear();   ...

  4. ElasticSearch和Kibana 5&period;X集群的安装

    ElasticSearch和Kibana 5.X集群的安装 1.准备工作 1.1.下载安装包 1.2.系统的准备 2.ElasticSearch集群的安装 2.1.修改 config/elastics ...

  5. &lbrack;Swift&rsqb;LeetCode451&period; 根据字符出现频率排序 &vert; Sort Characters By Frequency

    Given a string, sort it in decreasing order based on the frequency of characters. Example 1: Input: ...

  6. 1-spring boot 入门

    我从08年到现在,毕业马山就10年了,一直从事.net平台开发工作(期间应该有1年时间从事java开发). 一.为什么要转java: 1.目前市场很多招聘java架构师的职位,且薪资都不错,但.net ...

  7. Objective-C 符号化

    符号化,顾名思义,就是把地址翻译成人能看懂的符号以及它们在文件中的位置,符号包括函数名和变量名等等. 1.什么地方有符号? 1)App Mach-O格式的二进制文件 2)DWARF(Debugging ...

  8. 5、Python-字典

    定义 info = {'name': '班长', 'id': 88, 'sex': 'man', 'address': '地球亚洲中国北京'} print(info['name']) print(in ...

  9. redis和memcache的区别(总结)

    1.Redis和Memcache都是将数据存放在内存中,都是内存数据库.不过memcache还可用于缓存其他东西,例如图片.视频等等: 2.Redis不仅仅支持简单的k/v类型的数据,同时还提供lis ...

  10. 打通Linux脉络系列:进程、线程和调度

    http://edu.csdn.net/huiyiCourse/series_detail/60 http://edu.csdn.net/mycollege