协程

  • 概念
  • 协程的yield实现
  • greenlet模块
  • gevent
  • asyncio 异步模块
    • 基本使用
    • 手动封装报头
    • aiohttp模块封装报头
    • requests模块 asyncio

概念

无论是多进程还是多线程,在遇到IO阻塞时都会被操作系统强行剥夺走CPU的执行权限,程序的执行效率因此就降低了下来。解决这一问题的关键在于,我们自己从应用程序级别检测IO阻塞然后切换到我们自己程序的其他任务执行,这样把我们程序的IO降到最低,我们的程序处于就绪态就会增多,以此来迷惑操作系统,操作系统便以为我们的程序是IO比较少的程序,从而会尽可能多的分配CPU给我们,这样也就达到了提升程序执行效率的目的。这样,就用到了协程。

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

需要强调的是:

  1. python的线程属于内核级别的,即由操作系统控制调度(如单线程一旦遇到io就被迫交出cpu执行权限,切换其他线程运行)
  2. 单线程内开启协程,一旦遇到io,从应用程序级别(而非操作系统)控制切换对比操作系统控制线程的切换,用户在单线程内控制协程的切换,优点如下:
    1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
    2. 单线程内就可以实现并发的效果,最大限度地利用cpu

协程的yield实现

要实现协程,关键在于用户程序自己控制程序切换,切换之前必须由用户程序自己保存协程上一次调用时的状态,如此,每次重新调用时,能够从上次的位置继续执行。我们之前学的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跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产。这样也实现了并发,不依赖于多线程或多进程,并且几乎没有切换消耗。

greenlet模块

该模块封装了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

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)

asyncio 异步模块

基本使用

该模块是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模块封装报头

手动封装报头过于繁琐,借助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模块 + asyncio

将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)

你可能感兴趣的:(python)