异步IO

同步IO在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作。

在IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。因为一个IO操作就阻塞了当前线程,导致其他代码无法执行,所以我们必须使用多线程或者多进程来并发执行代码,为多个用户服务。每个用户都会分配一个线程,如果遇到IO导致线程被挂起,其他用户的线程则不不受影响。

多线程/多进程虽然解决了并发的问题,但是系统不能无上限地增加线程。由于系统切换线程的开销也很大,所以一旦线程数量过大,CPU就会花费很多时间在线程切换上,真正运行代码的时间就减少了。

由于我们要解决的问题是CPU高速执行能力和IO设备的低速严重不匹配,多线程/进程只是解决这一问题的一种方法。

另一种解决IO问题的方法是异步IO。当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。

同步IO,也就是我们按照普通顺序写出的代码是无法实现异步IO模型的。

异步IO需要一个消息循环,在消息循环中,主线程不断地重复“读取消息-处理消息”这一过程:

loop=get_event_loop()
while True:
event=loop.get_event()
process_event(event)

消息模型很早就应用在桌面应用程序中了。一个GUI程序的主线程负责不停地读取消息并且处理消息。所有的键盘、鼠标等消息都被发送到GUI程序的消息队列中,然后GUI程序主线程负责处理这些消息。

由于GUI线程处理键盘、鼠标等消息的速度很快,所以用户感觉不到延迟。某些时候,GUI线程在一个消息的处理过程中遇到问题导致一次消息处理时间过长,此时,用户会感觉整个GUI程序停止响应了,敲键盘、点鼠标都没有反应。这说明在消息模型中,处理一个消息必须非常迅速,否则主线程将无法及时处理消息队列中的其他消息,导致程序看上去停止响应。

那么,消息模型是如何解决同步IO必须等待IO操作这一问题的呢?当遇到IO操作时,代码只负责发出IO请求,不等待IO结果,然后直接结束本轮消息处理,进入下一轮消息处理过程。当IO操作完成后,将受到一条“IO完成”的消息,处理该消息就可以直接获取IO操作结果。

在“发出IO请求”到受到“IO完成”的这段时间里,同步IO模型下,主线程只能挂起,但是异步IO模型下,主线程并没有休息,而是在消息循环中继续处理其他消息。这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO会大大提升系统的多任务处理能力。

协程

协程,又称微线程,纤程,Coroutine。

子程序,或者说函数,在所有语言中都是层级调用,通过栈来实现,一个线程就是执行一个子程序。

子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

协程看上去也是子程序,但是在执行过程中,在子程序的内部可以中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。这不是函数调用,而类似CPU的中断。例如子程序A、B:

def A():
print ('1')
print ('2')
print ('3') def B():
print ('x')
print ('y')
print ('z')

假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行的过程中中断再去执行A,结果可能是:

1
2
x
y
3
z

但是在A中并不存在B的函数调用。看起来A、B的执行有点像多线程,但是协程的特点在于是一个线程执行,那么和多线程相比,协程有何优势呢?

最大的优势在于协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,所以没有线程切换的开销。和多线程相比,线程数量越多,协程的性能优势就越明显。

第二大优势是不需要多线程的锁机制。因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就可以,所以执行效率比多线程高很多。

因为协程是一个线程执行,那么如何利用多核CPU呢?最简单的方法是多线程+协程,既充分利用多核,又充分发挥协程的高效率,可以获得极高的性能。

Python对于协程的支持是通过Generator生成器实现的。

在Generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。但是Python的yield不仅可以返回一个值,还可以接收调用者发出的参数。

来看例子:

传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但是可能造成死锁。

如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,消费者执行完毕后,切换回生产者继续生产,效率极高:

def consumer():
r=''
while True:
n=yield r
if not n:
return
print('[CONSUMER] Consuming %s ...'%n)
r='200 OK'
def produce(c):
c.send(None)
n=0
while n<5:
n+=1
print('[PRODUCER] Producing %s ...'%n)
r=c.send(n)
print('[PROCUDER] Consumer return:%s'%r)
c.close() c=consumer()
produce(c)

执行结果:

[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return:200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return:200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return:200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return:200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return:200 OK

补充几个知识点,有助于我们理解这段代码运行

1、生成器Generator的运行

对于Generator,

①每次调用next()或send()方法时执行,

②遇到yield语句返回,

③再次执行时,从上次返回的yield语句后一句继续执行。

2、send

send(None)

c.send(None)
#等价于
next(c)

而对于send(n),则用于传输变量,相当于把Generator中yield那一句话变成了变量n,再进行send(None)(或者说next(c)),即:

c.send(n)
#等价于,先把n传入Generator中替换掉yield表达式,再
next(c)

这就是在正文最后几段中提到的yield接收调用者发出参数的例子

3、空字符串在参与条件判断时,被认为是False

4、赋值运算符=,如果右边是一个表达式,那么赋值运算=会分为两步进行:①先运行右边表达式;②用右边表达式运行结果赋值

因此如果右边是yield 语句的话,一个普通的send()或者next()并不会完成赋值,赋值语句在下一次send()或者next()后首先进行。(这里的逻辑对于理解整个代码非常重要!)

接下来是对于代码的解读,序号表示运行步骤:

c=consumer()

生成了一个Generator,用变量c承接

produce(c)

把Generator c作为参数传入函数produce中运行

接下来就是produce中的运行:

c.send(None)
#等价于
next(c)

运行Generator c,直到yield返回,此时

r=''
n = yield r

相当于只执行了

r=''
yield r

变量n并没有赋值为r,原因见上文补充知识点4。

n=0
while n<5:
n = n + 1 #n=1
print('[PRODUCER] Producing %s...' % n)

r=c.send(n)

碰到send(),在Generator中继续上次断掉的地方n = yield r运行,这次要执行赋值语句了。

由于是send(n),所以把n传入Generator,替换掉yield表达式,相当于

n = yield r
#c.send(n) 传入了n,因此赋值语句实际为
n = n #注意第二个n为c.send传入的n,这两个n只是名字一样,实际上并不相关

        if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
  #接着第二次while循环,同样是运行到yield停止
while True:
n = yield r

由于在主程序语句⑤中,用r=c.send(n),所以第二次yield的值,会赋值给主程序⑤中的r

r='200 OK' #此即Generator中通过yield传回来的值

接着

print('[PRODUCER] Consumer return: %s' % r) #r='200 OK'

这样主程序中一次while循环完成。

输出为:

[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK

剩下的while循环逻辑与这一次的相同,不再详述。

c.close()

主程序中的循环完毕,通过c.close()关闭Generator c。

读到这里,你可能会发现Generator中的return语句并没有用到,实际上是这样的。这里的return,其实就是用来检验你对执行顺序的了解情况的。

此即用yield通过接收调用者传入参数的方式实现协程的一个例子。

总结:

1、异步IO是通过消息循环实现的,在消息循环中,主线程不断重复“读取消息-处理消息”这一过程。

2、消息模型解决同步IO必须等待IO操作问题的方式:当遇到IO操作时,代码只负责发出IO请求,不等待IO结果。然后直接结束本轮消息处理,进入下一轮消息处理过程。当IO操作完成后,将收到一条“IO完成”的消息,处理该消息就可直接获取IO的结果
3、协程看上去也是子函数,但是区别在于协程执行过程中可以在子函数内部中断,转而执行别的子函数,在适当的时候再回来执行。
4、协程是由一个线程执行的。子程序切换时候不需要切换线程,没有了线程切换的开销。
5、Python对于协程的实现是通过Generator生成器完成的,yield不仅可以返回一个值,还可以接收调用者传入的参数。
两个子程序间的通信通过send()和yield n进行

2020.11.2 异步IO 协程的更多相关文章

  1. 异步IO/协程/数据库/队列/缓存(转)

    原文:Python之路,Day9 - 异步IO\数据库\队列\缓存 作者:金角大王Alex add by zhj: 文章很长 引子 到目前为止,我们已经学了网络并发编程的2个套路, 多进程,多线程,这 ...

  2. python -- 异步IO 协程

    python 3.4 >>> import asyncio >>> from datetime import datetime >>> @asyn ...

  3. python异步加协程获取比特币市场信息

    目标 选取几个比特币交易量大的几个交易平台,查看对应的API,获取该市场下货币对的ticker和depth信息.我们从网站上选取4个交易平台:bitfinex.okex.binance.gdax.对应 ...

  4. Tornado异步之-协程与回调

    回调处理异步请求 回调 callback 处理异步官方例子 # 导入所需库 from tornado.httpclient import AsyncHTTPClient def asynchronou ...

  5. python并发编程之多进程、多线程、异步、协程、通信队列Queue和池Pool的实现和应用

    什么是多任务? 简单地说,就是操作系统可以同时运行多个任务.实现多任务有多种方式,线程.进程.协程. 并行和并发的区别? 并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任 ...

  6. python并发编程之多进程、多线程、异步和协程

    一.多线程 多线程就是允许一个进程内存在多个控制权,以便让多个函数同时处于激活状态,从而让多个函数的操作同时运行.即使是单CPU的计算机,也可以通过不停地在不同线程的指令间切换,从而造成多线程同时运行 ...

  7. 潭州课堂25班:Ph201805201 tornado 项目 第十课 深入应用异步和协程(课堂笔记)

    tornado 相关说明 需求: 增加 /save 的 handler,实现异步保存指定 URL 图片的功能 从网页上得到一张图片地址,由这个地址将图片保存到服务器,并将相关数据保存到数据库 impo ...

  8. python笔记-10(socket提升、paramiko、线程、进程、协程、同步IO、异步IO)

    一.socket提升 1.熟悉socket.socket()中的省略部分 socket.socket(AF.INET,socket.SOCK_STREAM) 2.send与recv发送大文件时对于黏包 ...

  9. 进程&线程(三):外部子进程subprocess、异步IO、协程、分布式进程

    1.外部子进程subprocess python之subprocess模块详解--小白博客 - 夜风2019 - 博客园 python subprocess模块 - lincappu - 博客园 之前 ...

随机推荐

  1. 深度评测丨 GaussDB(for Redis) 大 Key 操作的影响

    本文分享自华为云社区<墨天轮评测:GaussDB(for Redis)大Key操作的影响>,作者: 高斯 Redis 官方博客. 在前一篇文章<墨天轮评测:GaussDB(for R ...

  2. javascript 判断对像是否相等

    在Javascript中相等运算包括"==","==="全等,两者不同之处,不必多数,本篇文章我们将来讲述如何判断两个对象是否相等? 你可能会认为,如果两个对象 ...

  3. Java反射机制及原理

    一.概念 java程序运行时动态的创建类并调用类的方法和属性   二.原理简介 Class<?> clz = Class.forName("java.util.ArrayList ...

  4. CSS之 sass、less、stylus 预处理器的使用方式

    更详细区别参考文档:https://blog.csdn.net/pedrojuliet/article/details/72887490 使用变量: sass:  使用 $ 符号定义变量,如: $ba ...

  5. 女朋友让我深夜十二点催她睡觉,我有Python我就不干

    事情是这样的:今天晚上,女朋友让我十二点催她睡觉. 不过,可是我实在太困了,熬不下去-- 是吧?女朋友哪有睡觉重要? 但,女朋友的命令,我是不敢违抗的-- 但是睡觉也不能缺! 这时候我们该怎么办呢?是 ...

  6. Entity Framework 在OrderBy排序中使用字符串

    public static class LinqExtensions { private static PropertyInfo GetPropertyInfo(Type objType, strin ...

  7. laravel操作Redis排序/删除/列表/随机/Hash/集合等方法全解

    Song • 3563 次浏览 • 0 个回复 • 2017年10月简介 Redis模块负责与Redis数据库交互,并提供Redis的相关API支持: Redis模块提供redis与redis.con ...

  8. js 对象的深克隆

    前端笔试或者面试的时候,很喜欢问的一个问题就是对象的深度克隆,或者说是对象的深度复制.其实这个问题说容易很容易,但是要说全面也挺不易. 要弄明白对象的克隆,首先要明白js中对象的组成.在js中一切实例 ...

  9. json中传递数组和list

    json的数据类型:List,数组,数字,字符串,逻辑值,对象,null 1.如果json传递的是数组,格式: { "name":"网站", "num ...

  10. Springcloud-微服务

    1.什么是微服务? 通过阅读马丁弗勒关于描述微服务的文章(https://martinfowler.com/articles/microservices.html),在此特作仅用作个人理解的关于微服务 ...