协程

来源:互联网 发布:汽车行业制图软件 编辑:程序博客网 时间:2024/06/05 10:04

  • 概念
  • 协程的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 timedef 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 timedef 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,step1bar,step1foo,step2bar,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 geventimport timedef 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 geventimport requestsimport timedef get_page(url):    print('GET: %s' % url)    resp = requests.get(url)    print('%d bytes received from %s.' % (len(resp.text), url))    return resp.textstart=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 geventimport requestsimport timedef get_page(url):    print('GET: %s' % url)    resp = requests.get(url)    print('%d bytes received from %s.' % (len(resp.text), url))    return resp.textfrom gevent.pool import Poolstart = 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 asyncioimport time@asyncio.coroutinedef get_html(url):    print('开始请求%s' % url)    yield from asyncio.sleep(random.randint(1, 3)) # yield from 检测IO, 保存状态;这里用asyncio.sleep模拟网络IO    return '%s 返回 html......' % urlstart = 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 asyncioimport time@asyncio.coroutinedef 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.netwww.sogou.com / 558www.zhihu.com / 488blog.csdn.net //ayhan_huang 41957一共耗时: 1.0431559085845947"""

aiohttp模块封装报头

手动封装报头过于繁琐,借助aiohttp模块,可以简化我们的工作:

import asyncioimport aiohttpimport time@asyncio.coroutinedef 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 asyncioimport requestsimport time@asyncio.coroutinedef 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)
原创粉丝点击