上一篇中主要基于yield一种方式来实现一个基本雏形的协程的实现。从上面的yield的可以看到,其实我们的协程需要处理的问题,就是
❝在一个线程内可以实现不同的函数的调用切换的执行,通俗的说其实就是可以实现多个任务在同一个线程里面并发的执行。
但是从上面我们的也可以得到一个关键点的问题是:
协程是单线程的下的一种并发模式,这个就有一点的弊端,就是无法利用多核,但是多核的问题,其实我们的可以直接多开几个进程来解决。但是相对多线程模式来说节省了线程切换的开销
协程任务切换其实类似一种程序中断,但是它与函数调用不同,中断通常是发生阻塞的情况下。才会去切换(需要不是需要手动切换的库的支持)
上一篇也说道关于IO多路复用和协程的之间的对比,IO多路复用其实是在一个进程里面除了多个逻辑的流程,且依赖依赖于事件的回调通知,在某种程度上导致了我们的处理流程函数的分散,一个逻辑会被切割成多个函数去处理,但是协程上,则没有这种问题。
协程需要保证是非堵塞的且没有相互依赖
协程基本上不能同步通讯,多采用异步的消息通讯,效率比较高
协程和多进程和多线程,再扯皮一次并行和并发:
所以协程这玩意某种意义上肯定是非常好的,其实GO上也有协程的说话,后续有机会的话,也得研究学习一下。
下面的说的关于协程的其他库的补充的话,估计已经是老生常谈了,但是这里我还是补充到我们的系列梳理里面,主要为为自己重头再理解一次!
https://greenlet.readthedocs.io/en/latest/#introduction 最好的学习资料是官网文档
用于进程内顺序并发编程的轻量级协同器
import greenlet
def run():
print("Running in the greenlet function.")
glet = greenlet.greenlet(run)
print(glet.parent)
print(glet.getcurrent().parent)
"""
Author = zyx
@version: v1.0.0
@File: greenlet.py
@文件功能描述:------
"""
import time
from greenlet import greenlet
def test1():
while True:
print("___test1--执行了___")
g2.switch()
print("___g2.切换到test2执行后,又切换回来后--执行了___")
time.sleep(0.5)
def test2():
while True:
print("___test2执行了___")
g1.switch()
print("___ g1.切换到test1执行后,又切换回来后--执行了___")
time.sleep(0.5)
g1 = greenlet(test1)
g2 = greenlet(test2)
# 切换到gr1中运行
g1.switch()
输出结果:
___test1--执行了___
___test2执行了___
___g2.切换到test2执行后,又切换回来后--执行了___
___test1--执行了___
___ g1.切换到test1执行后,又切换回来后--执行了___
___test2执行了___
___g2.切换到test2执行后,又切换回来后--执行了___
___test1--执行了___
___ g1.切换到test1执行后,又切换回来后--执行了___
___test2执行了___
___g2.切换到test2执行后,又切换回来后--执行了___
___test1--执行了___
___ g1.切换到test1执行后,又切换回来后--执行了___
___test2执行了___
___g2.切换到test2执行后,又切换回来后--执行了___
上面流程的说明:
1:首先先把两个协程任务丢到我们的greenlet里面,创建出来协程的对象(仅仅是创建对象,不会执行函数的执行)
2:调用协程对象自身的.switch()来进行任务的执行,首先执行了test1的任务。
3:g1.switch()----开始执行我们的test1函数内部的任务
4:test1函数内部开始执行,然后开始进行了g2.switch()任务的切换,此时我们的test1任务会被切断,暂停执行,然后我们的test2函数内部开始执行
5:test2函数内部开始执行后,又遇到了切换到我们的g1.switch(),此时我们的test2任务会被切断暂停,然后返回我们的test1的任务暂停的地方继续执行,所以此时输出了
_g2.切换到test2执行后,又切换回来后--执行了___,
然后等到test1这个执行结束的时候,因为我们的test1第一次的循环完成了,然后再开始了第二次的循环---此时我们的test1函数内部【又开始】执行,然后【又开始】进行了g2.switch()任务的切换,test1又开始暂停了!
___ g1.切换到test1执行后,又切换回来后--执行了___
然后完成了test2任务的第一次的循环的执行,接着第二次的循环又切换到了g1.switch()。
····· 所以关键几个步骤就是上面的那些地方!因为这个地方,在我们的web开发部署的时候,其实也有用到,在使用我们的python gunicorn里面有配置默认使用greenlet的方式还是gevent的方式。
但是单独拎出来使用,目前还是相对少~~所以仅仅当做补充协程的一个认识就好!有兴趣的人可以继续看官网文档继续研究!
这个gevent如果你再部署flask项目的时候,估计肯定是有遇到过,他的启动的模式是使用这个模型的话,则内部我们的FLask则是使用协程的并发模式。
还有如果你使用消息队列:Celery的话,应该也知道gevent还可以引入到celery里面。启动相关的worker的时候指定:
celery worker -P gevent
celery beat-P gevent
❝题外话
不过之前我再部署的时候遇到过一个关于使用gevent的问题,就是单独的跑一个简单的FLask的项目的话,是没有什么问题!但是遇到复杂的项目的时候,突然发现在进行猴子布丁的时候,他就无法启动服务了!!这个让我很疑惑,大概估计是某个第三方库引发的 不支持的猴子布丁的模式,当时也么去认真的细究,直接换了另一种模式进行部署了!!!有机会再看看.
猴子补丁其实就是gevent的一个牛逼的地方,它能够修改标准库里面大部分的阻塞式系统调用,包括socket、ssl、threading和 select等模块,让他们变为协作式运行。,一句话搞定所有同步的方法变为异步的方法。当然如我前面说的,仅限于大部分的标准库,第三方库的的话,有点难说!
gevent,相比greenlet的话,主要区别就是比较智能化,在遇到IO(比如网络、文件操作等)操作时,可以自动切换到其他的greenlet,等到IO操作完成,在适当的时候再次的切换回来继续执行。由于IO操作非常耗时,如果我们的程序一直处于等待状态,那肯定不行,gevent可以为我们自动化管理IO阻塞态的时候,进行相关任务协程的切换 ,不需要再手动进行切换了。
关于gevent我们需要了解几点:
gevent,是通过封装了 libev(基于epoll的快速事件循环) 和 greenlet 两个库来实现协程的。
gevent主要是针对我们的IO阻塞的一个自动切换协程库。
gevent内部调度其实是所有其它的greenlet实例
import gevent
import requests
def task_1(num):
for i in range(num):
print(gevent.getcurrent(), i)
# 模拟一个网络请求耗时操作
print(requests.get('http://www.baidu.com?q='+str(1)).text)
if __name__ == "__main__":
g1 = gevent.spawn(task_1, 5) # 创建协程
g2 = gevent.spawn(task_1, 5)
g3 = gevent.spawn(task_1, 5)
# 等待协程运行完毕,再退出主进程
g1.join()
g2.join()
g3.join()
如果感兴趣你还可以研究一下grequets这个库,这库主要是基于gevent实现了对requets的的异步支持。
pip install gevent grequests
示例:
import grequests
urls = [
'http://www.heroku.com',
'http://python-tablib.org',
'http://httpbin.org',
'http://python-requests.org',
'http://fakedomain/',
'http://kennethreitz.com'
]
rs = (grequests.get(u) for u in urls)
或者
res_list = grequests.map(urls) # 并行发送
print(res_list[0].text) # 打印第一个请求的响应文本
使用猴子补丁的:
from gevent import monkey
monkey.patch_all() # 给所有的耗时操作打上猴子补丁
import gevent
import requests
def task_1(num):
for i in range(num):
print(gevent.getcurrent(), i)
# 模拟一个网络请求耗时操作
print(requests.get('http://www.baidu.com?q='+str(1)).text)
if __name__ == "__main__":
g1 = gevent.spawn(task_1, 5) # 创建协程
g2 = gevent.spawn(task_1, 5)
g3 = gevent.spawn(task_1, 5)
# 等待协程运行完毕,再退出主进程
g1.join()
g2.join()
g3.join()
❝PS:from gevent import monkey monkey.patch_all() # 给所有的耗时操作打上猴子补丁 需要放在最开始的地方!
也不要这样的进行导入使用:
from gevent import monkey
import gevent
import requests
monkey.patch_all() # 给所有的耗时操作打上补丁
上面会提示:
MonkeyPatchWarning: Monkey-patching ssl after ssl has already been imported may lead to errors, including RecursionError on Python 3.6. It may also silently lead to incorrect behaviour on Python 3.7. Please monkey-patch earlier. See https://github.com/gevent/gevent/issues/1016. Modules that had direct imports (NOT patched): ['urllib3.util (C:\\Users\\15944\\Desktop\\pyproj\\Python\\CSGFrame\\venv\\lib\\site-packages\\urllib3\\util\\__init__.py)', 'urllib3.util.ssl_ (C:\\Users\\15944\\Desktop\\pyproj\\Python\\CSGFrame\\venv\\lib\\site-packages\\urllib3\\util\\ssl_.py)'].
monkey.patch_all()
标准的就是:
from gevent import monkey
monkey.patch_all() # 给所有的耗时操作打上补丁
import gevent
import requests
前两种方式:
from gevent import monkey
monkey.patch_all() # 给所有的耗时操作打上补丁
import gevent
import requests
from gevent import Timeout
import time
def task_1(num):
# 模拟一个网络请求耗时操作
# print(requests.get('http://www.baidu.com?q='+str(1)).text)
# 没有引入的猴子补丁的情况下,不要使用time.sleep,而应该使用 gevent.sleep()模拟
time.sleep(5)
if __name__ == "__main__":
g1 = gevent.spawn(task_1, 5) # 创建协程
g2 = gevent.spawn(task_1, 5)
g3 = gevent.spawn(task_1, 5)
# 等待协程运行完毕,再退出主进程
try:
# 如果任务1的超时时间到了则直接的抛出异常
# 第一种方式
timer = Timeout(0.01).start() # 必须设置这个来定义的超时时间
g1.join(timeout=timer)
# 第二钟方式
g1.get(timeout=timer)
except Timeout:
print('g1任务执行超时!')
g2.join()
g3.join()
with_timeout约束的方式:
from gevent import monkey
monkey.patch_all() # 给所有的耗时操作打上补丁
import gevent
import requests
from gevent import Timeout
import time
def task_1(num):
# 模拟一个网络请求耗时操作
# print(requests.get('http://www.baidu.com?q='+str(1)).text)
# 没有引入的猴子补丁的情况下,不要使用time.sleep,而应该使用 gevent.sleep()模拟
time.sleep(5)
if __name__ == "__main__":
# g1 = gevent.spawn(task_1, 5) # 创建协程
g1 =None
try:
g1 = gevent.with_timeout(1, task_1, 5)
g1.join()
except Timeout:
print('task_1 timed out')
g2 = gevent.spawn(task_1, 5)
g3 = gevent.spawn(task_1, 5)
# 等待协程运行完毕,再退出主进程
g2.join()
g3.join()
但凡是涉及到多任务协作,其实我们会发现,不管是协程,还是线程,还是进程都会涉及到同步问题的处理。
相同的,他们的处理的同步的问题的方式,也是大同小异的,也无非是哪几种:
事件(Event)
队列(Quest)
锁(Lock)和信号量
组(类似障碍对象)和池 等等
他们的其实也和我们的线程进程一样的,有set之类的。
import gevent
from gevent.event import Event
# 事件对象
evt = Event()
def xiaofeizhe():
print(' 客人: 1:服务员给我来一个鸡腿!')
# 模拟阻塞---阻塞后---我马上切换到fuwuyuan里面
gevent.sleep(3)
print("客人: 3:我最爱吃鸡腿啊!")
evt.set()
evt.wait()
print("客人: 5:你还真会推销啊!")
def fuwuyuan():
print("服务员:2:大早上的吃什么鸡腿啊!喝粥不香吗?")
# 开始调用wait进行blocking阻塞等待
evt.wait()
print("服务员:4:要不来再来给鸭腿!")
evt.set()
evt.wait()
print("服务员:6:呵呵哒!!!!")
def main():
gevent.joinall([
gevent.spawn(xiaofeizhe),
gevent.spawn(fuwuyuan)
])
if __name__ == '__main__':
main()
输出结果:
客人: 1:服务员给我来一个鸡腿!
服务员:2:大早上的吃什么鸡腿啊!喝粥不香吗?
客人: 3:我最爱吃鸡腿啊!
服务员:4:要不来再来给鸭腿!
客人: 5:你还真会推销啊!
服务员:6:呵呵哒!!!!
其他的暂时不重复了~~~
其中部署我们的Flask的时候我使用的一种方式就是:
from gevent import monkey, spawn
monkey.patch_all()
from flask import Flask, request, url_for, current_app, jsonify, g
app = Flask(__name__)
@app.route('/whhy', methods=['POST', 'GET'])
def returl():
# 记录下所有的请提交的数据信息
return jsonify({
"code": 0,
"message": "success"
})
@app.route('/whhy/bakurl/callback', methods=['POST', 'GET'])
def bakurl():
return jsonify({
"code": 0,
"message": "success"
})
if __name__ == '__main__':
print(app.url_map)
# -------flask系统启动配置 ---------
WSGIServer(('0.0.0.0', 7790),app).serve_forever() # 配置gevent后的启动方式
from gevent import pywsgi
server = pywsgi.WSGIServer(('0.0.0.0', 20012), app)
server.serve_forever()
https://www.jianshu.com/p/4dca99ffc0b4
❝个人其他博客地址
❝简书:https://www.jianshu.com/u/d6960089b087
掘金:https://juejin.cn/user/2963939079225608
小钟同学 | 文 【原创】| QQ:308711822
1:本文相关描述主要是个人的认知和见解,如有不当之处,还望各位大佬指正。 2:关于文章内容,有部分内容参考自互联网整理,如有链接会声明标注;如没有及时标注备注的链接的,如有侵权请联系,我会立即删除处理哟。
原文作者:程序员小钟同学;如有侵权,请联系管理员删除。