无论是多进程还是多线程,在遇到IO阻塞时都会被操作系统强行剥夺走CPU的执行权限,程序的执行效率因此就降低了下来。解决这一问题的关键在于,我们自己从应用程序级别检测IO阻塞然后切换到我们自己程序的其他任务执行,这样把我们程序的IO降到最低,我们的程序处于就绪态就会增多,以此来迷惑操作系统,操作系统便以为我们的程序是IO比较少的程序,从而会尽可能多的分配CPU给我们,这样也就达到了提升程序执行效率的目的。这样,就用到了协程。
协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
需要强调的是:
要实现协程,关键在于用户程序自己控制程序切换,切换之前必须由用户程序自己保存协程上一次调用时的状态,如此,每次重新调用时,能够从上次的位置继续执行。我们之前学的yield生成器函数其实就可以实现协程的效果。
下面我们用yield来写一个生产者消费者模型:
import time
def consumer():
res = ''
while True:
n = yield res
if not n:
return
print('[顾客] <-- 消费:',n)
time.sleep(1)
res = '好吃,再来...'
def produce(c):
next(c) # 初始化生成器
n = 0
while n < 5:
n += 1
print('[厨师] --> 生产:','包子%s'% n)
res = c.send('包子%s'% n)
print('[厨师] 顾客反馈:',res)
c.close() # 关闭生成器
if __name__ == '__main__':
c = consumer()
produce(c)
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产。这样也实现了并发,不依赖于多线程或多进程,并且几乎没有切换消耗。
该模块封装了yield,两个函数的切换也更方便。
from greenlet import greenlet
import time
def foo():
print('foo,step1')
time.sleep(2) # 程序将阻塞在这里,然后再切换到bar函数
gr2.switch()
print('foo,step2')
def bar():
print('bar,step1')
gr1.switch() # 切换到foo函数,从foo上次switch()暂停处继续执行
print('bar,step2')
gr1 = greenlet(foo) # 创建对象
gr2 = greenlet(bar)
gr1.switch() # 哪个函数先运行就启动对应的对象
'''
foo,step1
bar,step1
foo,step2
bar,step2
'''
协程的切换不会节省时间。和顺序执行是一样的。必须解决I/O密集型任务的性能问题。gevent模块可以解决这个问题。
gevent是一个第三方库,基于greenlet实现协程,可以实现并发:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。遇到I/O自动切换,其实是基于I/O模型的IO multiplexing
用法如下:
在导入模块的最上面打补丁:
from gevent import monkey; monkey.patch_all()
import gevent
说明:切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch_all()完成。
创建对象:
obj1 = gevent.spawn(func)
obj2 = gevent.spawn(func)
…
放入对象:
gevent.joinall([obj1, obj2])
from gevent import monkey; monkey.patch_all()
import gevent
import time
def eat():
print('吃饼1')
time.sleep(3) # 等待上饼
print('吃饼2')
def work():
print('搬第一车砖')
time.sleep(4) # 等第二车砖
print('搬第二车砖')
if __name__ == '__main__':
start = time.time()
g1 = gevent.spawn(eat)
g2 = gevent.spawn(work)
gevent.joinall([g1, g2])
print('一共耗时:',time.time()-start)
''' 执行结果
吃饼1
搬第一车砖
吃饼2
搬第二车砖
一共耗时: 4.004263162612915'''
from gevent import monkey; monkey.patch_all()
import gevent
import requests
import time
def get_page(url):
print('GET: %s' % url)
resp = requests.get(url)
print('%d bytes received from %s.' % (len(resp.text), url))
return resp.text
start=time.time()
g1=gevent.spawn(get_page,'https://www.python.org/doc')
g2=gevent.spawn(get_page,'https://www.github.com/')
g3=gevent.spawn(get_page,'http://www.csdn.com/')
gevent.joinall([g1,g2,g3,])
print(g3.value) # .value拿到返回值
print(time.time()-start)
# gevent 协程池
from gevent import monkey; monkey.patch_all()
import gevent
import requests
import time
def get_page(url):
print('GET: %s' % url)
resp = requests.get(url)
print('%d bytes received from %s.' % (len(resp.text), url))
return resp.text
from gevent.pool import Pool
start = time.time()
pool = Pool(3) # 实例化协程池;测试发现如果池大小为1,那么执行完一个任务就结束了,其它没问题
tasks = [
'https://www.python.org/doc',
'https://www.github.com/',
'http://www.csdn.com/',
'http://www.cnblog.com',
'http://www.douban.com',
]
for url in tasks:
pool.apply_async(get_page, args=(url,))
pool.join()
print(time.time() - start)
该模块是python3.3之后新增的,可以帮我们检测IO(只能是网络IO),实现应用程序级别的切换。下面我们看一下基本使用:
import asyncio
import time
@asyncio.coroutine
def get_html(url):
print('开始请求%s' % url)
yield from asyncio.sleep(random.randint(1, 3)) # yield from 检测IO, 保存状态;这里用asyncio.sleep模拟网络IO
return '%s 返回 html......' % url
start = time.time()
urls = ['url_1', 'url_2', 'url_3', 'url_4', 'url_5', 'url_6']
tasks = []
for url in urls:
tasks.append(get_html(url))
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(*tasks))
loop.close()
end = time.time()
print('一共耗时:', end - start)
不过asyncio模块只能发送tcp包(只封装到这一层),不能直接发http包,因此,发送http请求时,需要额外封装http协议头。
import asyncio
import time
@asyncio.coroutine
def get_html(host, post=80, url='/'):
print('开始请求:',host)
# step1: 建立tcp连接,IO阻塞
conn_recv, conn_send = yield from asyncio.open_connection(host, post)
# step2: 封装http报头
header = """GET {url} HTTP/1.0\r\nHOST:{host}\r\n...\r\n\r\n"""\
.format(url=url, host=host).encode('utf-8')
# step3: 发送http请求,IO阻塞
conn_send.write(header)
yield from conn_send.drain() # Flush the write buffer
# step4 接收http请求, IO阻塞
html = yield from conn_recv.read()
# 关闭通道
conn_send.close()
print(host, url, len(html.decode('utf-8')))
start = time.time()
urls = ['url_1', 'url_2', 'url_3', 'url_4', 'url_5', 'url_6']
tasks = [
get_html(host='www.zhihu.com'),
get_html(host='blog.csdn.net',url='//ayhan_huang'),
get_html(host='www.sogou.com')
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(*tasks))
loop.close()
end = time.time()
print('一共耗时:', end - start)
"""
开始请求: www.zhihu.com
开始请求: www.sogou.com
开始请求: blog.csdn.net
www.sogou.com / 558
www.zhihu.com / 488
blog.csdn.net //ayhan_huang 41957
一共耗时: 1.0431559085845947
"""
手动封装报头过于繁琐,借助aiohttp模块,可以简化我们的工作:
import asyncio
import aiohttp
import time
@asyncio.coroutine
def get_html(url):
print('开始请求:',url)
# IO http请求
response = yield from aiohttp.request('GET',url)
# IO http响应
data = yield from response.read()
print(url, len(data))
start = time.time()
tasks = [
get_html('http://www.zhihu.com'),
get_html('http://blog.csdn.net/ayhan_huang'),
get_html('http://www.sogou.com')
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(*tasks))
loop.close()
end = time.time()
print('一共耗时:', end - start)
将requests模块的函数作为参数传入,可以以我们熟悉的方式发起http请求
import asyncio
import requests
import time
@asyncio.coroutine
def get_html(func, *args):
print('开始请求:',*args[0])
loop = asyncio.get_event_loop()
future = loop.run_in_executor(None, func, *args)
response = yield from future
print(response.url, len(response.text))
start = time.time()
tasks = [
get_html(requests.get, 'http://www.chouti.com'),
get_html(requests.get, 'http://blog.csdn.net/ayhan_huang'),
get_html(requests.get, 'http://www.sogou.com')
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(*tasks))
loop.close()
end = time.time()
print('一共耗时:', end - start)