浅谈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实现爬取知乎神回复简单爬虫代码分享
Jan 04 Python
几个提升Python运行效率的方法之间的对比
Apr 03 Python
Python常用算法学习基础教程
Apr 13 Python
Python外星人入侵游戏编程完整版
Mar 30 Python
Python实现将一个正整数分解质因数的方法分析
Dec 14 Python
Django中使用Whoosh进行全文检索的方法
Mar 31 Python
Django实现跨域请求过程详解
Jul 25 Python
python如果快速判断数字奇数偶数
Nov 13 Python
使用Python获取当前工作目录和执行命令的位置
Mar 09 Python
Python 忽略文件名编码的方法
Aug 01 Python
pytorch中的model.eval()和BN层的使用
May 22 Python
使用tensorflow 实现反向传播求导
May 26 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
后宫无数却洁身自好的男主,唐三只爱小舞
2020/03/02 国漫
建立动态的WML站点(三)
2006/10/09 PHP
php+ajax实现文件切割上传功能示例
2020/03/03 PHP
TP5框架页面跳转样式操作示例
2020/04/05 PHP
在IE6下发生Internet Explorer cannot open the Internet site错误
2010/06/21 Javascript
页面实时更新时间的JS实例代码
2013/12/18 Javascript
Javascript高级技巧分享
2014/02/25 Javascript
使用nodejs、Python写的一个简易HTTP静态文件服务器
2014/07/18 NodeJs
通过构造函数实例化对象的方法
2017/06/28 Javascript
jQuery实现可兼容IE6的遮罩功能详解
2017/09/19 jQuery
为输入框加入数字js校验代码分享
2017/11/02 Javascript
动态加载JavaScript文件的3种方式
2018/05/05 Javascript
详解Ant Design of React的安装和使用方法
2018/12/27 Javascript
layui时间控件选择时间范围的实现方法
2019/09/28 Javascript
Node 使用express-http-proxy 做api网关的实现
2020/10/15 Javascript
Python对小数进行除法运算的正确方法示例
2014/08/25 Python
python编程测试电脑开启最大线程数实例代码
2018/02/09 Python
Python可变参数*args和**kwargs用法实例小结
2018/04/27 Python
Python获取Redis所有Key以及内容的方法
2019/02/19 Python
Python异步编程之协程任务的调度操作实例分析
2020/02/01 Python
Python调用ffmpeg开源视频处理库,批量处理视频
2020/11/16 Python
使用CSS3来匹配横屏竖屏的简单方法
2015/08/04 HTML / CSS
摩托车和ATV零件、配件和服装的首选在线零售商:MotoSport
2017/12/22 全球购物
财务会计专业推荐信
2013/11/30 职场文书
开学典礼主持词
2014/03/19 职场文书
会员卡清退活动总结
2014/08/27 职场文书
收款委托书
2014/10/14 职场文书
电子商务实训报告总结
2014/11/05 职场文书
农村婚礼司仪主持词
2015/06/29 职场文书
员工规章制度范本
2015/08/07 职场文书
社交电商模式的兴起:这些新的商机千万别错过
2019/07/26 职场文书
React Hook用法示例详解(6个常见hook)
2021/04/28 Javascript
React配置子路由的实现
2021/06/03 Javascript
拒绝盗图!教你怎么用python给图片加水印
2021/06/04 Python
Java 通过手写分布式雪花SnowFlake生成ID方法详解
2022/04/07 Java/Android
mysql使用 not int 子查询隐含陷阱
2022/04/12 MySQL