浅谈Python协程


Posted in Python onJune 17, 2020

协程

协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程。

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:

协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。

协程的好处:

  • 无需线程上下文切换的开销
  • 无需原子操作锁定及同步的开销
  • "原子操作(atomic operation)是不需要synchronized",所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。视作整体是原子性的核心。
  • 方便切换控制流,简化编程模型
  • 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

缺点:

  • 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
  • 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

使用yield实现协程操作例子

import time
import queue

def consumer(name):
 print("--->starting eating baozi...")
 while True:
 new_baozi = yield
 print("[%s] is eating baozi %s" % (name, new_baozi))
 # time.sleep(1)

def producer(): # 生产者
 r = con.__next__()
 r = con2.__next__()
 n = 0
 while n < 5:
 n += 1
 con.send(n)
 con2.send(n)
 print("\033[32;1m[producer]\033[0m is making baozi %s" % n)


if __name__ == '__main__':
 con = consumer("c1")
 con2 = consumer("c2")
 p = producer()

程序执行的结果为:

--->starting eating baozi...
--->starting eating baozi...
[c1] is eating baozi 1
[c2] is eating baozi 1
[producer] is making baozi 1
[c1] is eating baozi 2
[c2] is eating baozi 2
[producer] is making baozi 2
[c1] is eating baozi 3
[c2] is eating baozi 3
[producer] is making baozi 3
[c1] is eating baozi 4
[c2] is eating baozi 4
[producer] is making baozi 4
[c1] is eating baozi 5
[c2] is eating baozi 5
[producer] is making baozi 5

 问题来了,现在之所以能够实现多并发的效果,是因为每一个生产者没有任何花时间的代码,所以他根本没有卡住,如果这个时候在生产者这里sleep(1),那么速度一下子就变慢了,来看下下面的函数

def home():
  print("in func 1")
  time.sleep(5)
  print("home exec done")

def bbs():
  print("in func 2")
  time.sleep(2)

def login():
  print("in func 2")

假如说nginx每次来一个请求都经过函数来处理,但它是一个单线程的情况,假如说nginx请求home页,因为nginx在后台处理是单线程,单线程的情况下同事过来三次请求,那该怎么办?肯定是一次次的串行的执行啊,但是我为了让他实现感觉是并发的效果,我是不是该在各个协程之间实行切换啊,但什么时候切换呢?那么,我问你,如果从一个请求进来直接打印一个print,那么我会在这个地方立刻切换吗?因为这里面没有任何的阻塞,不会被卡主,所以不需要立刻切换。如果他需要干一件事,比如整个home花了5s钟,单线程是串行的,即便是使用了协程,那它还是串行的,为了保证并发的效果,什么时候进行切换?应该time.sleep(5)这里切换到bbs请求,那么bbs如果也sleep呢?那它就切换到下一个login,那么就是这么的切换。怎么才能实现一个单线程下实现上面程序的并发效果呢?就一句话,遇到io操作就切换,协程之所以能处理大并发,其实就是把io操作给挤掉了,就是io操作就切换,也就是这个程序只有CPU在运算,所以速度很快!那么问题又来了切换完之后,那么什么时候在切换回去啊?也就是说,怎么实现程序自动监测io操作完成了?那么就看下一个知识点吧!

Greenlet

greenlet是一个用C实现的协程模块,相比与python自带的yield,它是一块封装好了的协程,可以使你在任意函数之间随意切换,而不需把这个函数先声明为generator。

from greenlet import greenlet
def test1():
 print(12)
 gr2.switch() # 切换到gr2
 print(34)
 gr2.switch() # 切换到gr2

def test2():
 print(56)
 gr1.switch() # 切换到gr1
 print(78)


gr1 = greenlet(test1) # 启动一个协程
gr2 = greenlet(test2) #
gr1.switch() # 切换到gr1

程序执行后的结果为:

12
56
34
78

Gevent

上面的greenlet为手动挡的自动切换,现在来看一下自动挡的自动切换Gevent,遇到IO就切换。

Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

来看下非常简单的协程切换小程序

import gevent

def func1():
 print('\033[31;1m李闯在跟海涛搞...\033[0m')
 gevent.sleep(2) # 模仿IO
 print('\033[31;1m李闯又回去跟继续跟海涛搞...\033[0m')

def func2():
 print('\033[32;1m李闯切换到了跟海龙搞...\033[0m')
 gevent.sleep(1)
 print('\033[32;1m李闯搞完了海涛,回来继续跟海龙搞...\033[0m')

gevent.joinall([
 gevent.spawn(func1), # spawn 启动一个协程
 gevent.spawn(func2),
])

程序执行后的结果为:

李闯在跟海涛搞...
李闯切换到了跟海龙搞...
李闯搞完了海涛,回来继续跟海龙搞...
李闯又回去跟继续跟海涛搞...

协程之爬虫

现在利用协程来实现简单的爬虫

from gevent import monkey; monkey.patch_all() # 把当前程序的所有的io操作单独给我做上标记
import gevent # 协程模块
from urllib.request import urlopen # 爬虫所需要的模块

def f(url):
 print('GET: %s' % url)
 resp = urlopen(url)
 data = resp.read()
 print('%d bytes received from %s.' % (len(data), url))

gevent.joinall([ # 利用协程大并发的爬取网页
 gevent.spawn(f, 'https://www.python.org/'),
 gevent.spawn(f, 'https://www.yahoo.com/'),
 gevent.spawn(f, 'https://github.com/'),
])

程序执行的结果为:

GET: https://www.python.org/
GET: https://www.yahoo.com/
GET: https://github.com/
59619 bytes received from https://github.com/.
495691 bytes received from https://www.yahoo.com/.
48834 bytes received from https://www.python.org/.

协程之Socket

通过gevent实现单线程下的多socket并发

# socket_server #

import sys
import socket
import time
import gevent

from gevent import socket,monkey
monkey.patch_all()

def server(port):
 s = socket.socket()
 s.bind(('HW-20180425SPSL', port))
 s.listen(500)
 while True:
 cli, addr = s.accept()
 gevent.spawn(handle_request, cli)

def handle_request(conn):
 try:
 while True:
 data = conn.recv(1024)
 print("recv:", data)
 conn.send(data)
 if not data:
 conn.shutdown(socket.SHUT_WR)
 except Exception as ex:
 print(ex)
 finally:
 conn.close()
if __name__ == '__main__':
 server(8001)
# socket_client #

import socket

HOST = 'HW-20180425SPSL' # The remote host
PORT = 8001 # The same port as used by the server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
while True:
 msg = bytes(input(">>:"),encoding="utf8")
 s.sendall(msg)
 data = s.recv(1024)
 #print(data)

 print('Received', repr(data))
 s.close()

程序执行后的结果为:

socket_client.py

>>:lala
Received b'lala'
>>:

socket_server.py

recv: b'heihei'

论事件驱动和异步IO

通常,我们写服务器处理模型的程序时,有以下几种模型:
(1)每收到一个请求,创建一个新的进程,来处理该请求;

(2)每收到一个请求,创建一个新的线程,来处理该请求;

(3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求

上面的几种方式,各有千秋,

第(1)中方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单。

第(2)种方式,由于要涉及到线程的同步,有可能会面临死锁等问题。

第(3)种方式,在写应用程序代码时,逻辑比前面两种都复杂。

综合考虑各方面因素,一般普遍认为第(3)种方式是大多数网络服务器采用的方式

看图说话讲事件驱动模型

在UI编程中,常常要对鼠标点击进行相应,首先如何获得鼠标点击呢?

方式一:创建一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有以下几个缺点:

1. CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这会造成很多的CPU资源浪费;如果扫描鼠标点击的接口是阻塞的呢?

2. 如果是堵塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘;

3. 如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题;
所以,该方式是非常不好的。

方式二:就是事件驱动模型

目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下:

1. 有一个事件(消息)队列;

2. 鼠标按下时,往这个队列中增加一个点击事件(消息);

3. 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;

4. 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;

浅谈Python协程

什么是事件驱动模型?

其实就是根据事件做出反应!

事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。

让我们用例子来比较和对比一下单线程、多线程以及事件驱动编程模型。下图展示了随着时间的推移,这三种模式下程序所做的工作。这个程序有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了。

浅谈Python协程

在单线程同步模型中,任务按照顺序执行。如果某个任务因为I/O而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。如果任务之间并没有互相依赖的关系,但仍然需要互相等待的话这就使得程序不必要的降低了运行速度。

在多线程版本中,这3个任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。与完成类似功能的同步程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,因为这类程序不得不通过线程同步机制如锁、可重入函数、线程局部存储或者其他机制来处理线程安全问题,如果实现不当就会导致出现微妙且令人痛不欲生的bug。

在事件驱动版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时,注册一个回调到事件循环中,然后当I/O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽可能的得以执行而不需要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为,因为程序员不需要关心线程安全问题。

当我们面对如下的环境时,事件驱动模型通常是一个好的选择:

1、程序中有许多任务,而且…

2、任务之间高度独立(因此它们不需要互相通信,或者等待彼此)而且…

3、在等待事件到来时,某些任务会阻塞。

当应用程序需要在任务间共享可变的数据时,这也是一个不错的选择,因为这里不需要采用同步处理。

网络应用程序通常都有上述这些特点,这使得它们能够很好的契合事件驱动编程模型。

此处要提出一个问题,就是,上面的事件驱动模型中,只要一遇到IO就注册一个事件,然后主程序就可以继续干其它的事情了,只到io处理完毕后,继续恢复之前中断的任务,这本质上是怎么实现的呢?哈哈,下面我们就来一起揭开这神秘的面纱。。。。

请看详解Python IO口多路复用这篇文章

以上就是浅谈Python协程的详细内容,更多关于Python协程的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
对于Python编程中一些重用与缩减的建议
Apr 14 Python
python创建一个最简单http webserver服务器的方法
May 08 Python
python实现中文分词FMM算法实例
Jul 10 Python
python跳过第一行快速读取文件内容的实例
Jul 12 Python
pyqt5与matplotlib的完美结合实例
Jun 21 Python
python设置环境变量的原因和方法
Jun 24 Python
用python给自己做一款小说阅读器过程详解
Jul 11 Python
Django模板语言 Tags使用详解
Sep 09 Python
python文字转语音实现过程解析
Nov 12 Python
opencv 图像加法与图像融合的实现代码
Jul 08 Python
详解python字符串驻留技术
May 21 Python
利用 Python 的 Pandas和 NumPy 库来清理数据
Apr 13 Python
使用K.function()调试keras操作
Jun 17 #Python
哪些是python中web开发框架
Jun 17 #Python
python如何处理程序无法打开
Jun 16 #Python
python模块如何查看
Jun 16 #Python
python实现PDF中表格转化为Excel的方法
Jun 16 #Python
解决Keras 中加入lambda层无法正常载入模型问题
Jun 16 #Python
结束运行python的方法
Jun 16 #Python
You might like
php实现的用户查询类实例
2015/06/18 PHP
PHP如何通过传引用的思想实现无限分类(代码简单)
2015/10/13 PHP
thinkPHP框架实现类似java过滤器的简单方法示例
2018/09/05 PHP
js注意img图片的onerror事件的分析
2011/01/01 Javascript
multiSteps 基于Jquery的多步骤滑动切换插件
2011/07/22 Javascript
Jquery上传插件 uploadify v3.1使用说明
2012/06/18 Javascript
JS(JQuery)操作Array的相关方法介绍
2014/02/11 Javascript
jQuery中$.each使用详解
2015/01/29 Javascript
跟我学习javascript创建对象(类)的8种方法
2015/11/20 Javascript
js倒计时抢购实例
2015/12/20 Javascript
Javascript的表单验证-揭开正则表达式的面纱
2016/03/18 Javascript
easyui form validate总是返回false的原因及解决方法
2016/11/07 Javascript
扩展bootstrap的modal模态框-动态添加modal框-弹出多个modal框
2017/02/21 Javascript
详解使用JS如何制作简单的ASCII图与单极图
2017/03/31 Javascript
Vue的土著指令和自定义指令实例详解
2018/02/04 Javascript
Vue实现微信支付功能遇到的坑
2019/06/05 Javascript
jquery实现的放大镜效果示例
2020/02/24 jQuery
JS实现图片懒加载(lazyload)过程详解
2020/04/02 Javascript
Jquery高级应用Deferred对象原理及使用实例
2020/05/28 jQuery
在vue项目中promise解决回调地狱和并发请求的问题
2020/11/09 Javascript
Python中使用MELIAE分析程序内存占用实例
2015/02/18 Python
用Python编写web API的教程
2015/04/30 Python
独特的python循环语句
2016/11/20 Python
基于Python3.6+splinter实现自动抢火车票
2018/09/25 Python
python得到单词模式的示例
2018/10/15 Python
python获取中文字符串长度的方法
2018/11/14 Python
python 矢量数据转栅格数据代码实例
2019/09/30 Python
解决Jupyter notebook更换主题工具栏被隐藏及添加目录生成插件问题
2020/04/20 Python
css3 条纹化和透明化表格Firefox下测试成功
2014/04/15 HTML / CSS
编写一个 C 函数,该函数在一个字符串中找到可能的最长的子字符串,且该字符串是由同一字符组成的
2015/07/23 面试题
高级人员简历的自我评价分享
2013/11/03 职场文书
12月小学生校园广播稿
2014/02/04 职场文书
实习公司领导推荐函
2014/05/21 职场文书
党员教师学习党的群众路线教育实践活动心得体会
2014/10/31 职场文书
表扬通报怎么写
2015/01/16 职场文书
导师工作推荐信
2015/03/27 职场文书