Python笔记14(异步IO)

21. 异步IO

CPU的速度远远快于磁盘、网络等IO。

同步IO:遇到IO操作,如读写文件、发送网络数据时,需要等待IO操作完成,才能继续进行下一步操作。

解决IO问题的方法

  • 多线程、多进程
  • 异步IO:代码需要执行一个耗时IO操作时,它只发出指令并不等待IO结果,然后去执行其他代码,等IO返回结果时再通知CPU进行处理

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

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

1. 协程

协程,又称微线程,纤程,英文名Coroutine。

子程序(或称为函数)调用是通过栈实现的,一个线程就是执行一个子程序。子程序就是协程的一种特例。

子程序调用总是一个入口,一次返回,调用顺序是明确的,而协程的调用和子程序不同。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而进行别的子程序,在适当的时候再返回来接着执行。

协程跟多线程比的优势

  • 协程极高的执行效率。子程序切换不是线程切换,而是由程序自身控制,没有线程切换的开销,和多线程比,线程数量越多协程的性能优势就越明显
  • 不需要多线程的锁机制。因为只有一个纤程,也不存在同时写变量冲突,在协程中控制共享资源不加锁只需判断状态即可

协程是一个线程执行,利用多核CPU最简单的方法是 多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

Python通过generator实现协程。可通过for循环迭代,也可不断调用next()函数获取由yield语句返回的下一个值。但yield不仅可返回一个值,还可接受调用者发出的参数。

生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高。

def consumer():
    '''generator生产者'''
    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)
        # 一旦产生了东西,通过c.send(n)切换到consumer()执行
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    # 关闭consumer
    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
'''

代码解释:

  • 函数consumer()是一个 generator,把一个consumer传入produce后:
    • 首先调用c.send(None)启动生成器
    • 然后,一旦产生了东西,通过s.send(n)切换到consumer执行
    • consumer通过yield拿到消息处理,又通过yield把结果传回
    • produce拿到consumer处理的结果,继续生产下一条消息
    • produce决定不生产了,通过c.close()关闭consumer,整个过程结束
  • 整个流程无锁,由一个线程执行,produceconsumer协作完成任务,所以称为"协程",而非线程的抢占式多任务

2. asyncio

asyncio是Python3.4版本引入的标准库,直接内置了对异步IO的支持。

asyncio的编程模型就是一个消息循环,从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。

asyncio实现Hello world代码:

import asyncio

@asyncio.coroutine
def hello():
    print("Hello, World!")
    # 异步调用asyncio.sleep(1)
    r = yield from asyncio.sleep(1)
    print("Hello again")

# 获取EventLoop
loop = asyncio.get_event_loop()
# 执行coroutine
loop.run_until_complete(hello())
loop.close()

# 打印
Hello, World!
Hello again!

代码解释:

  • @asyncio.coroutine把一个generator标记为coroutine类型,然后把这个coroutine扔到EventLoop中执行
  • hello()会首先打印出Hello, World!
  • yield from语句让我们方便的调用另一个generator。由于asyncio.sleep()也是一个coroutine,所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环
  • asyncio.sleep()返回时,线程就可以从yield from拿到返回值(此处是None),然后接着执行下一行语句

用Task封装两个coroutine:

import threading
import asyncio

@asyncio.coroutine
def hello():
    print('Hello, World! (%s)' % threading.currentThread())
    yield from asyncio.sleep(1)
    print('Hello, again! (%s)' % threading.currentThread())

loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

# 第二行和第三行中间暂停1秒
'''
Hello, World! (<_MainThread(MainThread, started 4749815296)>)
Hello, World! (<_MainThread(MainThread, started 4749815296)>)
Hello, again! (<_MainThread(MainThread, started 4749815296)>)
Hello, again! (<_MainThread(MainThread, started 4749815296)>)
'''

代码解释:

  • 由打印的当前线程名称可看出:两个coroutine是由一个线程并发执行的
  • 如果把asyncio.sleep()换成真正的IO操作,则多个coroutine就可由一个线程并发执行

asyncio的异步网络连接来获取sina、Sohu和163的网站首页:

import asyncio

@asyncio.coroutine
def wget(host):
    print('wget %s...' % host)
    connect = asyncio.open_connection(host, 80)
    reader, writer = yield from connect
    header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host
    writer.write(header.encode('utf-8'))
    yield from writer.drain()
    while True:
        line = yield from reader.readline()
        if line == b'\r\n':
            break
        print('%s header > %s' % (host, line.decode('utf-8').rstrip()))
    # Ignore the body, close the socket
    writer.close()

loop = asyncio.get_event_loop()
tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

'''
wget www.sohu.com...
wget www.163.com...
wget www.sina.com.cn...
(等待一段时间,很短很短)
(打印sina的header)
www.sina.com.cn header > HTTP/1.1 302 Moved Temporarily
www.sina.com.cn header > Server: nginx
...
(打印Sohu的header)
www.sohu.com header > Content-Type: text/html
www.sohu.com header > Content-Length: 180
...
(打印163的header)
www.163.com header > HTTP/1.1 301 Moved Permanently
www.163.com header > Date: Tue, 22 Jun 2021 15:17:27 GMT
'''

可见3个连接由一个线程通过coroutine并发完成。

知识点:

  • asyncio提供了完善的异步IO支持
  • 异步操作需要在coroutine中通过yield from完成
  • 多个coroutine可封装成一组Task然后并发执行

3. asyncio/await

asyncio提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异步操作。

为了简化更好的标识异步IO,从Python3.5开始引入新的语法asyncawait可以让coroutine的代码更简洁易读。

asyncawait是针对coroutine的新语法。使用该语法有两步简单替换:

  • @asyncio.coroutine替换为async
  • yield from替换为await

导入模块:

import asyncio
import threading

上一节代码:

@asyncio.coroutine
def hello():
    print('Hello, World! (%s)' % threading.currentThread())
    yield from asyncio.sleep(1)
    print('Hello, World! (%s)' % threading.currentThread())

loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

'''
Hello, World! (<_MainThread(MainThread, started 13052)>)
Hello, World! (<_MainThread(MainThread, started 13052)>)
'''
# 停顿1秒后输出下方信息
'''
Hello, World! (<_MainThread(MainThread, started 13052)>)
Hello, World! (<_MainThread(MainThread, started 13052)>)
'''

新语法:

async def hello():
    print('Hello, World! (%s)' % threading.currentThread())
    r = await asyncio.sleep(1)
    print('Hello again! (%s)' % threading.currentThread())

# 剩下的代码不变
loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

'''
Hello, World! (<_MainThread(MainThread, started 5300)>)
Hello, World! (<_MainThread(MainThread, started 5300)>)
'''
# 停顿1秒后打印下面信息
'''
Hello again! (<_MainThread(MainThread, started 5300)>)
Hello again! (<_MainThread(MainThread, started 5300)>)
'''

4. aiohttp

asyncio可实现单线程并发IO操作。如果仅用在客户端,发挥的作用不大。如果把asyncio用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可用单线程+coroutine实现多用户的高并发支持。

asyncio实现了TCP、UDP、SSL等协议,aiohttp则是基于asyncio实现的HTTP框架。

使用:

  • 安装aiohttp

pip install aiohttp

  • 编写一个HTTP服务器,分别处理一下URL:

/ - 首页返回b'<h1>Index</h1>'
/hello/{name} - 根据URL参数返回文本hello,%s!

  • 代码如下:
import asyncio
from aiohttp import web

async def index(request):
    await asyncio.sleep(0.5)
    return web.Response(body=b'<h1>Index</h1>')

async def hello(request):
    await asyncio.sleep(0.5)
    text = '<h1>hello, %s!</h1>' % request.match_info['name']
    return web.Response(body=text.encode('utf-8'))

async def init(loop):
    app = web.Application(loop=loop)
    app.router.add_route('GET', '/', index)
    app.router.add_route('GET', '/hello/{name}', hello)
    srv = await loop.create_server(app.make_handler(), '127.0.0.1', 8000)
    print('Server started at http://127.0.0.1:8000...')
    return srv

loop = asyncio.get_event_loop()
loop.run_until_complete(init(loop))
loop.run_forever()

# 试了下不行,下载了个文件什么鬼。。。可能是Windows的锅

注意aiohttp的初始化函数init()也是一个coroutine,loop.create_server()则利用asyncio创建TCP服务。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员老五

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值