协程

1、协程:

单线程实现并发

在应用程序里控制多个任务的切换+保存状态

优点:

应用程序级别速度要远远高于操作系统的切换

缺点:

多个任务一旦有一个阻塞没有切,整个线程都阻塞在原地,该线程内的其他的任务都不能执行了

一旦引入协程,就需要检测单线程下所有的IO行为, 实现遇到IO就切换,少一个都不行,以为一旦一个任务阻塞了,整个线程就阻塞了, 其他的任务即便是可以计算,但是也无法运行了

2、协程序的目的:

想要在单线程下实现并发

并发指的是多个任务看起来是同时运行的

并发=切换+保存状态

协程引入

cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长或有一个优先级更高的程序替代了它

ps:在介绍进程理论时,提及进程的三种执行状态,而线程才是执行单位,所以也可以将上图理解为线程的三种状态

一:其中第二种情况并不能提升效率,只是为了让cpu能够雨露均沾,实现看起来所有任务都被“同时”执行的效果,如果多个任务都是纯计算的,这种切换反而会降低效率。为此我们可以基于yield来验证。yield本身就是一种在单线程下可以保存任务运行状态的方法,我们来简单复习一下:

#串行执行
import timedef func1():for i in range(10000000):i+1def func2():for i in range(10000000):i+1start = time.time()
func1()
func2()
stop = time.time()
print(stop - start)#基于yield并发执行
import time
def func1():while True:yielddef func2():g=func1()for i in range(10000000):i+1next(g)start=time.time()
func2()
stop=time.time()
print(stop-start)

  二:第一种情况的切换。在任务一遇到io情况下,切到任务二去执行,这样就可以利用任务一阻塞的时间完成任务二的计算,效率的提升就在于此。

import time
def func1():while True:print('func1')yielddef func2():g=func1()for i in range(10000000):i+1next(g)time.sleep(3)print('func2')
start=time.time()
func2()
stop=time.time()
print(stop-start)yield不能检测IO,实现遇到IO自动切换

对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另外一个任务去计算,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给我们的线程。

协程的本质:

就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。为了实现它,我们需要找寻一种可以同时满足以下条件的解决方案:

#1. 可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行。

#2. 作为1的补充:可以检测io操作,在遇到io操作的情况下才发生切换

协程介绍

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

需要强调的是:

#1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
#2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)

对比操作系统控制线程的切换,用户在单线程内控制协程的切换

优点如下:

#1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
#2. 单线程内就可以实现并发的效果,最大限度地利用cpu

缺点如下:

#1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
#2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

总结协程特点:

  1. 必须在只有一个单线程里实现并发
  2. 修改共享数据不需加锁
  3. 用户程序里自己保存多个控制流的上下文栈
  4. 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))

Greenlet

如果我们在单个线程内有20个任务,要想实现在多个任务之间切换,使用yield生成器的方式过于麻烦(需要先得到初始化一次的生成器,然后再调用send。。。非常麻烦),而使用greenlet模块可以非常简单地实现这20个任务直接的切换

#安装
pip3 install greenlet

from greenlet import greenletdef eat(name):print('%s eat 1' %name)g2.switch('egon')print('%s eat 2' %name)g2.switch()
def play(name):print('%s play 1' %name)g1.switch()print('%s play 2' %name)g1=greenlet(eat)
g2=greenlet(play)g1.switch('egon')#可以在第一次switch时传入参数,以后都不需要

单纯的切换(在没有io的情况下或者没有重复开辟内存空间的操作),反而会降低程序的执行速度

greenlet只是提供了一种比generator更加便捷的切换方式,当切到一个任务执行时如果遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。

单线程里的这20个任务的代码通常会既有计算操作又有阻塞操作,我们完全可以在执行任务1时遇到阻塞,就利用阻塞的时间去执行任务2。。。。如此,才能提高效率,这就用到了Gevent模块(后续)。

greenlet module与class

我们首先看一下greenlet这个module里面的属性

>> dir(greenlet)
['GREENLET_USE_GC', 'GREENLET_USE_TRACING', 'GreenletExit', '_C_API', '__doc__', '__file__', '__name__', '__package__', '__version__', 'error', 'getcurrent', 'gettrace', 'greenlet', 'settrace']
>>>

其中,比较重要的是getcurrent(), 类greenlet、异常类GreenletExit。

getcurrent()返回当前的greenlet实例;

GreenletExit:是一个特殊的异常,当触发了这个异常的时候,即使不处理,也不会抛到其parent(后面会提到协程中对返回值或者异常的处理)

然后我们再来看看greenlet.greenlet这个类:

>>> dir(greenlet.greenlet)
['GreenletExit', '__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__getstate__', '__hash__', '__init__', '__new__', '__nonzero__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',   '__sizeof__', '__str__', '__subclasshook__', '_stack_saved', 'dead', 'error', 'getcurrent', 'gettrace', 'gr_frame', 'parent', 'run', 'settrace','switch', 'throw']
>>>

比较重要的几个属性:

run:当greenlet启动的时候会调用到这个callable,如果我们需要继承greenlet.greenlet时,需要重写该方法

switch:前面已经介绍过了,在greenlet之间切换

parent:可读写属性,后面介绍

dead:如果greenlet执行结束,那么该属性为true

throw:切换到指定greenlet后立即跑出异常

文章后面提到的greenlet大多都是指greenlet.greenlet这个class,请注意区别。

parent属性

每一个Greenlet都有一个parent,一个新的greenlet在哪里创生,当前环境的greenlet就是这个新greenlet的parent。所有的greenlet构成一棵树,其跟节点就是还没有手动创建greenlet时候的”main” greenlet(事实上,在首次import greenlet的时候实例化)。当一个协程 正常结束,执行流程回到其对应的parent;或者在一个协程中抛出未被捕获的异常,该异常也是传递到其parent。

import greenlet
def test1(x, y):print id(greenlet.getcurrent()), id(greenlet.getcurrent().parent) # 40240272 40239952z = gr2.switch(x+y)print 'back z', zdef test2(u):print id(greenlet.getcurrent()), id(greenlet.getcurrent().parent) # 40240352 40239952return 'hehe'gr1 = greenlet.greenlet(test1)
gr2 = greenlet.greenlet(test2)
print id(greenlet.getcurrent()), id(gr1), id(gr2)     # 40239952, 40240272, 40240352
print gr1.switch("hello", " world"), 'back to main'    # hehe back to main

上述例子可以看到,尽量是从test1所在的协程gr1 切换到了gr2,但gr2的parent还是’main’ greenlet,因为默认的parent取决于greenlet的创生环境。另外 在test2中return之后整个返回值返回到了其parent,而不是switch到该协程的地方(即不是test1),这个跟我们平时的函数调用不一样,记住“switch not call”。对于异常 也是展开至parent。

mport greenlet
def test1(x, y):try:z = gr2.switch(x+y)except Exception:print 'catch Exception in test1'def test2(u):assert Falsegr1 = greenlet.greenlet(test1)
gr2 = greenlet.greenlet(test2)
try:gr1.switch("hello", " world")
except:print 'catch Exception in main'

输出为:

catch Exception in main

Greenlet生命周期

文章开始的地方提到第一个例子中的gr2其实并没有正常结束,我们可以借用greenlet.dead这个属性来查看:

from greenlet import greenlet
def test1():gr2.switch(1)print 'test1 finished'def test2(x):print 'test2 first', xz = gr1.switch()print 'test2 back', zgr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
print 'gr1 is dead?: %s, gr2 is dead?: %s' % (gr1.dead, gr2.dead)
gr2.switch()
print 'gr1 is dead?: %s, gr2 is dead?: %s' % (gr1.dead, gr2.dead)
print gr2.switch(10)输出:test2 first 1
test1 finished
gr1 is dead?: True, gr2 is dead?: False
test2 back ()
gr1 is dead?: True, gr2 is dead?: True
10

从这个例子可以看出:

  • 只有当协程对应的函数执行完毕,协程才会die,所以第一次Check的时候gr2并没有die,因为第9行切换出去了就没切回来。在main中再switch到gr2的时候, 执行后面的逻辑,gr2 die。
  • 如果试图再次switch到一个已经是dead状态的greenlet会怎么样呢,事实上会切换到其parent greenlet

Greenlet Traceing

Greenlet也提供了接口使得程序员可以监控greenlet的整个调度流程。主要是gettrace 和 settrace(callback)函数。下面看一个例子:

def test_greenlet_tracing():def callback(event, args):print event, 'from', id(args[0]), 'to', id(args[1])def dummy():g2.switch()def dummyexception():raise Exception('excep in coroutine')main = greenlet.getcurrent()g1 = greenlet.greenlet(dummy)g2 = greenlet.greenlet(dummyexception)print 'main id %s, gr1 id %s, gr2 id %s' % (id(main), id(g1), id(g2))oldtrace = greenlet.settrace(callback)try:g1.switch()except:print 'Exception'finally:greenlet.settrace(oldtrace)test_greenlet_tracing()输出:main id 40604416, gr1 id 40604736, gr2 id 40604816
switch from 40604416 to 40604736
switch from 40604736 to 40604816
throw from 40604816 to 40604416
Exception

其中callback函数event是switch或者throw之一,表明是正常调度还是异常跑出;args是二元组,表示是从协程args[0]切换到了协程args[1]。上面的输出展示了切换流程:从main到gr1,然后到gr2,最后回到main。

Greenlet内存泄漏问题

使用greenlet需要注意一下三点:

第一:greenlet创生之后,一定要结束,不能switch出去就不回来了,否则容易造成内存泄露。

第二:python中每个线程都有自己的main greenlet及其对应的sub-greenlet ,不能线程之间的greenlet是不能相互切换的。

第三:不能存在循环引用,这个是官方文档明确说明。

  ”Greenlets do not participate in garbage collection; cycles involving data that is present in a greenlet’s frames will not be detected. “

from greenlet import greenlet, GreenletExit
huge = []
def show_leak():def test1():gr2.switch()def test2():huge.extend([x* x for x in range(100)])gr1.switch()print 'finish switch del huge'del huge[:]gr1 = greenlet(test1)gr2 = greenlet(test2)gr1.switch()gr1 = gr2 = Noneprint 'length of huge is zero ? %s' % len(huge)if __name__ == '__main__':show_leak() # output: length of huge is zero ? 100

  在test2函数中 第11行,我们将huge清空,然后再第16行将gr1、gr2的引用计数降到了0。但运行结果告诉我们,第11行并没有执行,所以如果一个协程没有正常结束是很危险的,往往不符合程序员的预期。greenlet提供了解决这个问题的办法,官网文档提到:如果一个greenlet实例的引用计数变成0,那么会在上次挂起的地方抛出GreenletExit异常,这就使得我们可以通过try … finally 处理资源泄露的情况。如下面的代码:

from greenlet import greenlet, GreenletExit
huge = []
def show_leak():def test1():gr2.switch()def test2():huge.extend([x* x for x in range(100)])try:gr1.switch()finally:print 'finish switch del huge'del huge[:]gr1 = greenlet(test1)gr2 = greenlet(test2)gr1.switch()gr1 = gr2 = Noneprint 'length of huge is zero ? %s' % len(huge)if __name__ == '__main__':show_leak()# output :# finish switch del huge# length of huge is zero ? 0

  上述代码的switch流程:main greenlet –> gr1 –> gr2 –> gr1 –> main greenlet, 很明显gr2没有正常结束(在第10行刮起了)。第18行之后gr1,gr2的引用计数都变成0,那么会在第10行抛出GreenletExit异常,因此finally语句有机会执行。同时,在文章开始介绍Greenlet module的时候也提到了,GreenletExit这个异常并不会抛出到parent,所以main greenlet也不会出异常。

Gevent介绍

#安装
pip3 install gevent

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

#用法
g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的g2=gevent.spawn(func2)g1.join() #等待g1结束g2.join() #等待g2结束#或者上述两步合作一步:gevent.joinall([g1,g2])g1.value#拿到func1的返回值

遇到IO阻塞时会自动切换任务

import gevent
def eat(name):print('%s eat 1' %name)gevent.sleep(2)print('%s eat 2' %name)def play(name):print('%s play 1' %name)gevent.sleep(1)print('%s play 2' %name)g1=gevent.spawn(eat,'egon')
g2=gevent.spawn(play,name='egon')
g1.join()
g2.join()
#或者gevent.joinall([g1,g2])
print('主')

上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,

而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了

from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前

或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头

from gevent import monkey;monkey.patch_all()import gevent
import time
def eat():print('eat food 1')time.sleep(2)print('eat food 2')def play():print('play 1')time.sleep(1)print('play 2')g1=gevent.spawn(eat)
g2=gevent.spawn(play_phone)
gevent.joinall([g1,g2])
print('主')

我们可以用threading.current_thread().getName()来查看每个g1和g2,查看的结果为DummyThread-n,即假线程

Gevent之同步与异步

from gevent import spawn,joinall,monkey;monkey.patch_all()import time
def task(pid):"""Some non-deterministic task"""time.sleep(0.5)print('Task %s done' % pid)def synchronous():for i in range(10):task(i)def asynchronous():g_l=[spawn(task,i) for i in range(10)]joinall(g_l)if __name__ == '__main__':print('Synchronous:')synchronous()print('Asynchronous:')asynchronous()
#上面程序的重要部分是将task函数封装到Greenlet内部线程的gevent.spawn。 初始化的greenlet列表存放在数组threads中,此数组被传给gevent.joinall 函数,后者阻塞当前流程,并执行所有给定的greenlet。执行流程只会在 所有greenlet执行完后才会继续向下走。

Gevent之应用举例一

from gevent import monkey;monkey.patch_all()
import gevent
import requests
import timedef get_page(url):print('GET: %s' %url)response=requests.get(url)if response.status_code == 200:print('%d bytes received from %s' %(len(response.text),url))start_time=time.time()
gevent.joinall([gevent.spawn(get_page,'https://www.python.org/'),gevent.spawn(get_page,'https://www.yahoo.com/'),gevent.spawn(get_page,'https://github.com/'),
])
stop_time=time.time()
print('run time is %s' %(stop_time-start_time))协程应用:爬虫

Gevent之应用举例二

通过gevent实现单线程下的socket并发(from gevent import monkey;monkey.patch_all()一定要放到导入socket模块之前,否则gevent无法识别socket的阻塞)

服务端

from gevent import monkey;monkey.patch_all()
from socket import *
import gevent#如果不想用money.patch_all()打补丁,可以用gevent自带的socket
# from gevent import socket
# s=socket.socket()def server(server_ip,port):s=socket(AF_INET,SOCK_STREAM)s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)s.bind((server_ip,port))s.listen(5)while True:conn,addr=s.accept()gevent.spawn(talk,conn,addr)def talk(conn,addr):try:while True:res=conn.recv(1024)print('client %s:%s msg: %s' %(addr[0],addr[1],res))conn.send(res.upper())except Exception as e:print(e)finally:conn.close()if __name__ == '__main__':server('127.0.0.1',8080)

客户端

from socket import *client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))while True:msg=input('>>: ').strip()if not msg:continueclient.send(msg.encode('utf-8'))msg=client.recv(1024)print(msg.decode('utf-8'))

多线程并发多个客户端

from threading import Thread
from socket import *
import threadingdef client(server_ip,port):c=socket(AF_INET,SOCK_STREAM) #套接字对象一定要加到函数内,即局部名称空间内,放在函数外则被所有线程共享,则大家公用一个套接字对象,那么客户端端口永远一样了c.connect((server_ip,port))count=0while True:c.send(('%s say hello %s' %(threading.current_thread().getName(),count)).encode('utf-8'))msg=c.recv(1024)print(msg.decode('utf-8'))count+=1
if __name__ == '__main__':for i in range(500):t=Thread(target=client,args=('127.0.0.1',8080))t.start()

  

转载于:https://www.cnblogs.com/wlx97e6/p/9595454.html

python中的协程(二)相关推荐

  1. 二十五、深入Python中的协程

    @Author: Runsen 一说并发,你肯定想到了多线程+进程模型,确实,多线程+进程,正是解决并发问题的经典模型之一.但对于多核CPU,利用多进程+协程的方式,能充分利用CPU,获得极高的性能. ...

  2. Python中的协程

    Python中的协程 文章目录 Python中的协程 一.什么是协程 1.概念 2.协程的好处 3.缺点 二.了解协程的过程 1.yield工作原理 2.协程在运行过程中有四个状态: 3.预激协程的装 ...

  3. Python 中 异步协程 的 使用方法介绍

    静觅 崔庆才的个人博客:Python中异步协程的使用方法介绍:https://cuiqingcai.com/6160.html Python 异步 IO .协程.asyncio.async/await ...

  4. python 协程可以嵌套协程吗_Python | 详解Python中的协程,为什么说它的底层是生成器?...

    今天是Python专题的第26篇文章,我们来聊聊Python当中的协程. 我们曾经在golang关于goroutine的文章当中简单介绍过协程的概念,我们再来简单review一下.协程又称为是微线程, ...

  5. python中多进程+协程的使用以及为什么要用它

    前面讲了为什么python里推荐用多进程而不是多线程,但是多进程也有其自己的限制:相比线程更加笨重.切换耗时更长,并且在python的多进程下,进程数量不推荐超过CPU核心数(一个进程只有一个GIL, ...

  6. Python 中的协程 (4) asyncio模块

    asyncio的核心概念与基本架构 本文针对的是python3.4以后的版本的,因为从3.4开始才引入asyncio,后面的3.5 3.6 3.7版本是向前兼容的,只不过语法上面有稍微的改变.比如在3 ...

  7. python中的协程:greenlet和gevent

    协程是一中多任务实现方式,它不需要多个进程或线程就可以实现多任务. 1.通过yield实现协程: 代码: import time def A():while 1:print('------A----- ...

  8. 【Python中异步协程的基本框架】

    import asyncio import aiohttp import aiofiles import time import osasync def download(url, path):# 文 ...

  9. Python与Golang协程异同

    背景知识 这里先给出一些常用的知识点简要说明,以便理解后面的文章内容. 进程的定义: 进程,是计算机中已运行程序的实体.程序本身只是指令.数据及其组织形式的描述,进程才是程序的真正运行实例. 线程的定 ...

最新文章

  1. equals()与hashCode()
  2. 《经济学人》也谈 Python:它会是我们的未来吗?
  3. [转] Ghost自动安装
  4. 斐波那契数列 (C#)
  5. 线性结构 —— 前缀和
  6. 含噪数据的有效训练,谷歌地标图像检索竞赛2020冠军方案解读
  7. php通过函数怎么禁止百度蜘蛛抓取,怎么屏蔽百度蜘蛛抓取网站?
  8. 全球首款5g全网通智能路由器华为5G CPE Pro发布 售价2499元
  9. bzoj1426 收集邮票
  10. 这段 JavaScript 代码能挖出你所有的访问记录!
  11. day73 中间件 以及模板引擎
  12. 【Delphi】Delphi11.1 版本 Android SDK 更新步骤
  13. dirent.h缺失,Microsoft Visual Studio 2019( Professional)解决方案
  14. C1083,无法打开包括文件...
  15. TAHOMA:Physical Representation-based Predicate Optimization for a Visual Analytics Database,IEEE2019
  16. 我是如何做研发人员内部培训的
  17. 花旗将老虎证券目标价上调365%、将富途控股目标价上调106%
  18. Python采集手机4K壁纸,又是一个练手小案例,也不用担心没壁纸换咯
  19. lightroom 闪退_【HTTP网球iOS】Lightroom解锁
  20. python unicode 标点范围_Unicode 编码 范围

热门文章

  1. 小米手机无法连接eclipse调试案例
  2. 嘿嘿 刚刚进来 记录下
  3. WinCE6.0 修改开机Logo方法集锦(二)
  4. 2010中国大陆×××指南,满足你的欲望!
  5. Apache服务器部署(2)
  6. led伏安特性实验误差分析_检测实验室误差分析知识汇编
  7. 标准功能模块组件 -- 内部联络单组件,内部邮件组件,提高多人异地协同办公效率...
  8. vue-cli3+typescript初体验
  9. 第二十章:异步和文件I/O.(十三)
  10. OpenStack 和 Cloud Foundry