使用 asyncio

使用 asyncio

官网对 asyncio 的描述

  1. Asynchronous I/O(异步 I/O)。当代码需要执行一个耗时的 I/O 操作的时候, 它只发出 I/O 的指令, 并不等待 I/O 的结果, 然后去执行其它的代码, 以提高效率。

  2. event loop(事件循环)。把基本的 I/O 操作转换为需要处理的事件, 通过事件循环做事件的监测和事件触发等处理工作。

  3. coroutines(协程)。线程是由操作系统控制切换的, 使用协程可以收回控制权, 并且将异步编程同步化, 注册到事件循环中的事件处理器就是协程对象, 它由事件循环来调用, 当程序阻塞等待读取或者写入数据的时候, 进行上下文的切换可以让效率最大化。

  4. tasks(任务)。asyncio 模块非常容易和方便的执行并发任务, 并且可以实现创建、取消等管理任务。

asyncio 在 Python 3.4 的时候被引入到标准库, 内置了对异步 IO 的支持。asyncio 的 API 在 Python 3.6 的时候稳定下来, 从 Python 3.6 开始, asyncio 模块已经可用于线上环境。

asyncio 的生态问题

asyncio 是 Python 3 官方的解决方案, 但是 asyncio 的生态还没有建立起来。

  1. 迁移成本太高。Python 2 到 Python 3 的迁移不是安装一个库或者升级 Python 就行了, 这是一个不兼容的升级, 而且现在大部分的公司在 Python 2 下的产品和服务运行都比较良好, 迁移不太值得。

  2. 使用后的效果不突出。asyncio 比以前的方案效率有所提升, 但是不明显。现有效率没有出现瓶颈, 一般是不会为了一点点的效率提升而去进行迁移和升级。

  3. 目前没有大公司使用。没有什么重要项目支持 asyncio 版本的驱动, 并且做到及时维护。

asyncio 的使用

asyncio 的事件循环有多种方法启动协程, 最简单的方案是 run_until_complete():

import asyncio


async def coroutine(): # 使用 async 创建一个协程
    print('in coroutine')
    return 'result'


if __name__ == '__main__':
    event_loop = asyncio.get_event_loop() # 创建一个默认的事件循环
    事件y:
        print('starting coroutine')
        coro = coroutine()
        print('entering event loop')
        result = event_loop.run_until_complete(coro) # 通过调用事件循环的 run_until_complete() 启动协程
        print(f'it returned: {result}')
    finally:
        print('closing event loop')
        event_loop.close() # 关闭事件循环

# 输出:
starting coroutine
entering event loop
in coroutine
it returned: result
closing event loop

协程可以启动另外的协程并等待结果, 这样可以让各个协程专注于自己的工作, 这也是实际开发中需要用到的模式:

import asyncio


async def main():
    print('waiting for chain1')
    result1 = await chain1()
    print('waiting for chain2')
    result2 = await chain2(result1)
    return (result1, result2)


async def chain1():
    print('chain1')
    return 'result1'


async def chain2(arg):
    print('chain2')
    return f'Derived from {arg}'


if __name__ == '__main__':
    event_loop = asyncio.get_event_loop()
    try:
        return_value = event_loop.run_until_complete(main())
        print(f'return value: {return_value}')
    finally:
        event_loop.close()

# 输出:
waiting for chain1
chain1
waiting for chain2
chain2
return value: ('result1', 'Derived from result1')

上面代码中的 asyncawait 两个关键字是 Python 3.5 开始添加的, 用来替换旧式写法:

import asyncio


@asyncio.coroutine
def main():
    print('waiting for chain1')
    result1 = yield from chain1()
    print('waiting for chain2')
    result2 = yield from chain2(result1)
    return (result1, result2)


@asyncio.coroutine
def chain1():
    print('in chain1')
    return 'result1'


@asyncio.coroutine
def chain2(arg):
    print('in chain2')
    return f'Derived from {arg}'


if __name__ == '__main__':
    event_loop = asyncio.get_event_loop()
    try:
        return_value = event_loop.run_until_complete(main())
        print(f'return value: {return_value}')
    finally:
        event_loop.close()

async 关键字替代了 @asyncio.coroutine 这个装饰器, await 替代了 yield from。至此, 协程成为了一种新的语法, 而不再是一种生成器类型。

async with

异步 with 语法:

# 需要先安装 aiohttp: pip install aiohttp
import asyncio
import aiohttp # 可以理解为一个支持异步 I/O 的 requests


async def fetch_page(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.json()


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    result = loop.run_until_complete(fetch_page('http://httpbin.org/get?a=2')) # httpbin 这个网站能测试 http 请求和响应的各种信息
    print(f"Args: {result.get('args')}")
    loop.close()

# 输出:
Args: {'a': '2'}

async for

除了 async with, 还有 async for, async for 是一个异步迭代器, 可以直接把一个协程进行循环, 而且支持列表解析的写法。async for 的语法在 Python 3.5 开始添加:

import asyncio


async def g1():
    yield 1
    yield 2


async def g2():
    async for v in g1():
        print(v)
    return [v * 2 async for v in g1()]


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(g2())
    finally:
        loop.close()

# 输出:
1
2

Future & Task

asyncio.future 的实例可以代表一个已经完成或者未完成的推迟的任务。它和协程都可以用 await 关键字从而将其传递给事件循环, 暂停协程的执行, 来等待某些事件的发生。当 future 完成自己的任务之后, 事件循环会察觉到暂停并等待, 协程会获取 future 对象的返回值并继续执行。

Task 对象是 Future 的子类, 它将协程和 future 联系起来, 将一个协程封装成一个 future 的对象。

import asyncio


async def func1():
    await asyncio.sleep(1) # 异步 I/O 里面的 sleep() 方法, 它也是一个协程, 异步 I/O 里面不能使用 time.sleep(), time.sleep() 会阻塞整个线程
    await func(1)


async def func2():
    await func(2)


async def func(num):
    print(num * 2)


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    tasks = asyncio.gather( # gather() 可以将一些 future 和协程封装成一个 future
        asyncio.ensure_future(func1()), # ensure_future() 可以将一个协程封装成一个 Task
        asyncio.ensure_future(func2())
    )
    loop.run_until_complete(tasks)

    tasks = [
        asyncio.ensure_future(func1()),
        asyncio.ensure_future(func2())
    ]

    loop.run_until_complete(asyncio.wait(tasks)) # loop.run_until_complete() 既可以接收一个协程对象, 也可以接收一个 future 对象
    loop.close()

# 输出:
4
2
4
2

同步机制

为了支持安全的并发, asyncio 模块也包含了在多进程和多线程模块里面相同的低级原语的实现:

Semaphore(信号量)

可以使用 Semaphore(信号量) 来控制并发访问的数量:

import aiohttp
import asyncio


NUMBERS = range(6)
URL = 'http://httpbin.org/get?a={}'
sema = asyncio.Semaphore(3)


async def fetch_async(a):
    async with aiohttp.request('GET', URL.format(a)) as r:
        data = await r.json()
    return data['args']['a']


async def print_result(a):
    with (await sema):
        r = await fetch_async(a)
        print(f'fetch({a}) = {r}')


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    f = asyncio.wait([print_result(num) for num in NUMBERS])
    loop.run_until_complete(f)
    loop.close()

# 输出:
fetch(1) = 1
fetch(4) = 4
fetch(5) = 5
fetch(2) = 2
fetch(0) = 0
fetch(3) = 3

Lock(锁)

import asyncio
import functools


def unlock(lock):
    print('callback releasing lock')
    lock.release()


async def test(locker, lock):
    print(f'{locker} waiting for the lock')
    with await lock:
        print(f'{locker} acquired lock')
    print(f'{locker} released lock')


async def main(loop):
    lock = asyncio.Lock()
    await lock.acquire()
    loop.call_later(0.1, functools.partial(unlock, lock)) # call_later() 表达推迟一段时间的回调, 第一个参数是以秒为单位的延迟, 第二个参数是回调函数
    await asyncio.wait([test('l1', lock), test('l2', lock)])


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main(loop))
    loop.close()

# 输出:
l1 waiting for the lock
l2 waiting for the lock
callback releasing lock
l1 acquired lock
l1 released lock
l2 acquired lock
l2 released lock

Condition(条件)

import asyncio
import functools


async def consumer(cond, name, second):
    await asyncio.sleep(second)
    with await cond:
        await cond.wait()
        print('{}: Resource is available to consumer'.format(name))


async def producer(cond):
    await asyncio.sleep(2)
    for n in range(1, 3):
        with await cond:
            print('notifying consumer {}'.format(n))
            cond.notify(n=n) # 挨个通知单个消费者
        await asyncio.sleep(0.1)


async def producer2(cond):
    await asyncio.sleep(2)
    with await cond:
        print('Making resource available')
        cond.notify_all() # 一次性通知全部的消费者


async def main(loop):
    condition = asyncio.Condition()
    task = loop.create_task(producer(condition)) # producer 和 producer2 是两个协程, 不能使用 call_later(), 需要用到 create_task() 把它们创建成一个 task
    consumers = [consumer(condition, name, index) for index, name in enumerate(('c1', 'c2'))]
    await asyncio.wait(consumers)
    task.cancel()
    task = loop.create_task(producer2(condition))
    consumers = [consumer(condition, name, index) for index, name in enumerate(('c1', 'c2'))]
    await asyncio.wait(consumers)
    task.cancel() # 取消任务


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main(loop))
    loop.close()

# 输出:
notifying consumer 1
c1: Resource is available to consumer
notifying consumer 2
c2: Resource is available to consumer
Making resource available
c1: Resource is available to consumer
c2: Resource is available to consumer

Event(事件)

下面代码是模仿 Lock(锁) 的例子实现的一个事件, 与 Lock(锁) 不同的是, 事件被触发的时候, 两个消费者不用获取锁, 就要尽快地执行下去了:

import asyncio
import functools


def set_event(event):
    print('setting event in callback')
    event.set()


async def test(name, event):
    print('{} waiting for event'.format(name))
    await event.wait()
    print('{} triggered'.format(name))


async def main(loop):
    event = asyncio.Event()
    print('event start state: {}'.format(event.is_set()))
    loop.call_later(0.1, functools.partial(set_event, event))
    await asyncio.wait([test('e1', event), test('e2', event)])
    print('event end state: {}'.format(event.is_set()))


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main(loop))
    loop.close()

# 输出:
event start state: False
e1 waiting for event
e2 waiting for event
setting event in callback
e1 triggered
e2 triggered
event end state: True

队列(Queue)

下面是一个 aiohttp + 优先级队列的用:

import asyncio
import random
import aiohttp


NUMBERS = random.sample(range(100), 7)
URL = 'http://httpbin.org/get?a={}'
sema = asyncio.Semaphore(3)


async def fetch_async(a):
    async with aiohttp.request('GET', URL.format(a)) as r:
        data = await r.json()
    return data['args']['a']


async def collect_result(a):
    with (await sema):
        return await fetch_async(a)

async def produce(queue):
    for num in NUMBERS:
        print(f'producing {num}')
        item = (num, num)
        await queue.put(item)


async def consume(queue):
    while 1:
        item = await queue.get()
        num = item[0]
        rs = await collect_result(num)
        print(f'consuming {rs}...')
        queue.task_done()


async def run():
    queue = asyncio.PriorityQueue()
    consumer = asyncio.ensure_future(consume(queue))
    await produce(queue)
    await queue.join()
    consumer.cancel()


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(run())
    loop.close()

# 输出:
producing 87
producing 27
producing 98
producing 25
producing 1
producing 50
producing 35
consuming 1...
consuming 25...
consuming 27...
consuming 35...
consuming 50...
consuming 87...
consuming 98...
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容