前言
建立前文提到的yield基本应用,协程概念之后,在续一些协程衍生出的异步应用。经常会听到并发和并行,同步和异步那么就简单梳理下这几个到底应该怎样理解。
并发和并行
并发:是程序结构上的概念,指你实现的程序是否支持多个动作。所以并发是指:你实现的程序是并发的,或者说你实现的系统是并发的,实质就是你实现的系统支持多个动作。例如你这个人既支持画图,也支持写文章这个两个动作,说明你“人”这个系统是并发的。
并行:指在同一时刻,同时执行多个操作。这个从直观感觉上说,就是并行指程序运行过程中,否同时执行多个事件。直观感受,单核操作系统是不能达到并行目的,因为一个cpu一个时间只能执行一个东西,多核才是执行并行的基础。既有两个人,同时执行画图,写文章这两件事。同一时间点,这两件事都在执行。
同步和异步
同步:发起一个调用,没得到调用结果,就不返回。
异步:发起一个调用结果,没等调用结束,就已经返回。
阻塞和非阻塞
阻塞:线程和进程在访问数据时遇到需要等待返回时执行等待。
非阻塞:线程和进程访问数据时遇到等待返回,挂起当前线程或进程,转而执行其他线程或进程。
一个很好的示例说明同步阻塞,同步非阻塞,异步阻塞,异步费阻塞的示例:
同步阻塞:小明一直盯着下载进度条,到 100% 的时候就完成。
同步非阻塞:小明提交下载任务后就去干别的,每过一段时间就去瞄一眼进度条,看到 100% 就完成。
异步阻塞:小明换了个有下载完成通知功能的软件,下载完成就“叮”一声。不过小明仍然一直等待“叮”的声音(看起来很傻,不是吗)
异步非阻塞:仍然是那个会“叮”一声的下载软件,小明提交下载任务后就去干别的,听到“叮”的一声就知道完成了。
实际使用
结合我们之前描述的协程概念可以知道,我们使用协程实现的程序就是异步非阻塞。这一套实用性,可以说非常广泛。举个例子:
用过mongodb都知道,如果要是在海量数据中match一些复合条件的数据出来,一定会有一个等待的时间,且假定你的程序是单进程,单线程的。那么现在要求运行5个match数据的操作,每个操作都是一秒。通常阻塞的就会按部就班一个一个运行,最终至少5秒得到结果。可是如果你运用异步机制,每个match操作在读取停顿,立马返回主调函数,继续执行下一个match,一次类推,这样相当于5个match在同时等待,最终耗时1秒多。
如果换多线程使用:看似并行,别忘了python中GIL限制,实际上也是单线程执行的,只是系统会自动切换。那么增加了操作系统线程间切换的开销,一定不会快于上面的协程实现。因为这个程序主要耗时是等待从mongodb中读取数据。
如果换做多进程:这个是真实并行,但是效率同协程比起来,基本相同,可是这个会增加系统额外的内存开销,因为每个进程都要占据自己的独立运行空间。
所以对于IO(数据库读写,网络访问,文件读写等)密集型的,协程能够极大提高效率。因为这是我们人为知道在等待耗时的地方调出继续执行。
Gevent
概述
Gevent是一个python第三方库来支持协程,并不是简单的让编者自己用yield实现。其以greenlet为核心,pip在安装gevent时会自动安装greenlet。Gevent框架实现异,也是利用了linux epoll事件监听机制。
用法
具体用法我们通过一个示例了解:
import gevent
def foo():
print('Running in foo')
gevent.sleep(0) #不同于time.sleep(n)
print('Explicit context switch to foo again')
def bar():
print('Explicit context to bar')
gevent.sleep(0)
print('Implicit context switch back to bar')
gevent.joinall([
gevent.Greenlet(foo).start()
gevent.spawn(bar),
])
执行结果:
这里的gevent.sleep(n)和我们通常理解的time.sleep(n)不同源码注释中解释说:让当前greenlet休眠至少n秒,但是当n<=0时,立马将执行权交给其他的能运行的greenlet。说白了n置0,立马调出当前函数,下次轮到他执行的时候继续从gevent.sleep(n)后执行。可以去看一下gevent.sleep(seconds=0, ref=True)源码,很简单。
怎么样,foo和bar两个函数并没有yield,但是实现了协程。这个有点僵硬,通常写东西经常要访问别人写好的一些基础服务的API,示例如下:
import gevent
import time
import requests
tic = lambda start : 'at %1.1f seconds' % (time.time() - start)
urls = [
('google', 'https://www.google.com.hk/'),
('github', 'https://github.com/'),
('baidu', 'https://www.baidu.com/'),
]
def fetch(key, url):
start = time.time()
response = requests.get(url)
print('Process {0}: {1} -- consuming: {2}'.format(
key, response.status_code, tic(start)))
def synchronous():
start = time.time()
for obj in urls:
fetch(obj[0], obj[1])
print('End synchronous: consuming: {}'.format(tic(start)))
def asynchronous():
start = time.time()
coroutine = []
for obj in urls:
coroutine.append(gevent.spawn(fetch, obj[0], obj[1]))
gevent.joinall(coroutine)
print('End synchronous: consuming: {}'.format(tic(start)))
print('Synchronous:')
synchronous()
print('Asynchronous:')
asynchronous()
该段程序运行结果:
仔细观察上图运行结果,思考几个问题:
1, 重复运行,synchronous返回结果的顺序是不是一定是google, github, baidu? 相对应Asynchronous呢?
2,synchronous总用时?Asynchronous总共用时?
不妨自己尝试运行一下。
在学习Gevent过程中,http://xlambda.com/gevent-tutorial/#_4
让我收获颇丰,其中示例第一个源于此链接。
除此之外,异步在python中真正一个大的框架是Tornado,是在牛的一个框架,很轻,但是却能够解决c10k,后续有机会再行分享。