Python Asyncio

大纲

  1. 操作系统任务调度
  2. 进程、线程
  3. 协程
  4. Asyncio
    4.1 定义一个协程(Coroutine)
    4.2 定义一个任务(Task / Future)
    4.3 绑定回调 / 获取任务返回结果
    4.4 并发、并发控制
    4.5 协程停止
  5. 结语
  6. 参考

简书的锚点已经对我造成了成吨的伤害...

操作系统任务调度

操作系统执行的任务基本可以分为:CPU 密集型、I/O 密集型。CPU 密集型任务会消耗大量的 CPU 计算资源,因此让操作系统调度任务的执行即可。而 I/O 密集型任务一般会涉及到硬盘 I/O、网络传输,大部分的时间在等待 I/O 的完成,因此出现了基于多任务系统的 CPU 任务调度。参考:IBM/调整 Linux I/O 调度器优化系统性能

在多任务系统中,操作系统接管了所有硬件资源并持有对硬件控制的最高权限。在操作系统中执行的程序,都以进程的方式运行在低的权限中。所有的硬件资源,由操作系统根据进程的优先级以及进程的运行状况进行统一的调度。

常见 Linux 操作系统抢占式任务处理(现代操作系统都支持抢占式多任务,包括 Windows、macOS、Linux(包括Android)和 iOS)

进程、线程

程序是一组指令的集合,程序运行时操作系统会将程序载入内存空间,在逻辑上产生一个单独的实例叫做进程(Process)。

随着多核 CPU 的发展,为了充分利用多核资源,需要进程内能并行地执行任务,因此产生了线程(Thread)的概念。

线程是操作系统进行任务调度的最小单元,线程存活于进程之中;同一个进程中的线程,共享一个虚拟内存空间;线程之间各自持有自己的线程 ID、当前指令的指针(PC)、寄存器集合以及栈。

线程和进程均由操作系统调度。

image.png

多线程的优势:

  1. 充分利用多核 CPU 资源(在 Python 中是不存在的);
  2. 将等待 I/O 操作的时间,调度到其他线程执行,提高 CPU 利用率;
  3. 将计算密集型的操作留给工作线程,预留线程保持与用户的交互;
  4. 同进程内多线程之间更加容易实现内存共享;

多线程从一定程度上提升了 CPU 资源的利用率,然而类似 C10K 等问题又开始让程序员对内核级别的上下文切换开销重视起来。

协程

协程让用户可以自主调度协程的运行状态(运行,挂起),协程可以看做是用户态线程,协程的目的在于让阻塞的 I/O 操作异步化。

一般子程序/函数的调用是按照顺序执行的,一个入口,一次返回。而协程可以在子程序 A 的调用过程中中断执行,转而调用另外一个子程序 B,在适当的时机再切回到子程序 A 继续执行,因此协程节省了多线程切换带来的开销问题,实现了在单线程中多线程的效果(当然,前提是各个子程序都是非阻塞的)。

协程拥有自己的寄存器上下文和栈,协程调度切换时,将寄存器上下文和栈保存起来,在切回来的时候,恢复之前保存的寄存器上下文和栈,这种直接切换操作栈的方式(context上下文切换),避开了内核切换的开销,可以不加锁的访问全局变量,切换速度快。

协程的优势:

  1. 比线程开销小;
  2. 单线程模型,线程安全避免了资源竞争;
  3. 代码逻辑清晰,同步的方式编写异步逻辑代码;

Asyncio

Python 在 3.4 中引入了协程的概念3.5 确定了协程的语法,Asyncio 基本概念:

  • Event Loop 事件循环:程序开启一个 While True 循环,用户将一些函数注册到事件循环上,当满足事件执行条件时,调用的协程函数;
  • Coroutine 协程对象:使用 asnc关键字定义的函数,它的调用不会立即执行函数,而是返回一个协程对象,协程对象需要注册到事件循环中,由事件循环负责调用;
  • Task:对协程对象的进一步封装,包括任务的各种状态;
  • Future:代表将来执行或没有执行的任务的结果,和 Task 没有本质的区别;
  • async:定义一个协程对象;
  • await:挂起阻塞的异步调用接口;

tips : 使用 Cython + libuv 实现的 uvloop 可以提升事件循环更多的性能:

import asyncio
import uvloop
# 声明使用 uvloop 事件循环
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
...
...
loop = uvloop.new_event_loop()
asyncio.set_event_loop(loop)

协程示例

import asyncio

async def compute(x, y):
    print("Compute %s + %s ..." % (x, y))
    # 阻塞
    await asyncio.sleep(1.0)
    return x + y

async def print_sum(x, y):
    result = await compute(x, y)
    print("%s + %s = %s" % (x, y, result))

loop = asyncio.get_event_loop()
loop.run_until_complete(print_sum(1, 2))
loop.close()

流程

image.png

使用 async 关键字定义一个协程(coroutine),协程是一个对象,直接调用并不会运行。可以通过在协程内部 await coroutine 或 yield from coroutine 运行,或者将协程加入到事件循环中让 EventLoop 调度执行。

Calling a coroutine does not start its code running – the coroutine object returned by the call doesn’t do anything until you schedule its execution. There are two basic ways to start it running: call await coroutine
or yield from coroutine
from another coroutine (assuming the other coroutine is already running!), or schedule its execution using the ensure_future()
function or the AbstractEventLoop.create_task()
method.
Coroutines (and tasks) can only run when the event loop is running.

定义一个协程(Coroutine)
import time
import asyncio

# 定义协程
async def test(x):
    print("wait:", x)
    await asyncio.sleep(x)
    
start = time.time()

coroutine = test(1)

# 获取事件循环
loop = asyncio.get_event_loop()
loop.run_until_complete(coroutine)

print("time:", time.time() - start)

# 输出:
# wait: 1
# time: 1.0050649642944336
定义一个任务(Task / Future)

Future 对象保存了协程的状态,可以用来获取协程的执行返回结果。
asyncio.ensure_future(coroutine)loop.create_task(coroutine) 都可以创建任务,run_until_complete 的参数是一个 futrue 对象。当传入一个协程方法时,其内部会自动封装成task,task是Future的子类。

import time
import asyncio

# 定义协程
async def test(x):
    print("wait:", x)
    await asyncio.sleep(x)

start = time.time()

coroutine = test(1)

loop = asyncio.get_event_loop()
# future
# task = asyncio.ensure_future(coroutine)
# 显式创建任务:task 是 future 的子类
task = loop.create_task(coroutine)

print(task)
loop.run_until_complete(task)
print(task)
print("time:", time.time() - start)

# <Task pending coro=<test() running at xxx>>
# wait: 1
# <Task finished coro=<test() done, defined at xxx> result=None>
# time: 1.006286859512329
绑定回调 / 获取任务返回结果
import time
import asyncio

# 定义协程
async def test(x):
    print("wait:", x)
    await asyncio.sleep(x)
    return "done of {}".format(x)

def callback(future):
    print("callback:", future.result())
    
start = time.time()    

coroutine = test(1)
loop = asyncio.get_event_loop()
task = loop.create_task(coroutine)
# 回调
task.add_done_callback(callback)
loop.run_until_complete(task)

# 直接获取
print("result:", task.result())
print("time:", time.time() - start)

# wait: 1
# callback: done of 1
# result: done of 1
# time: 1.0015690326690674
并发、并发控制

多个协程注册到事件循环中,当执行某一个协程时在任务阻塞的时候用 await 挂起,其他协程继续工作。

import time
import asyncio

async def test(x):
    print("wait:", x)
    await asyncio.sleep(x)
    return "done of {}".format(x)

start = time.time()

# sleep 1s 2s 3s
coroutine1 = test(1)
coroutine2 = test(2)
coroutine3 = test(3)

loop = asyncio.get_event_loop()

task = [
    loop.create_task(coroutine1),
    loop.create_task(coroutine2),
    loop.create_task(coroutine3)
]

# wait方式
# run_task = asyncio.wait(task)
# gather 能保证有序的结果返回
run_task = asyncio.gather(*task)

loop.run_until_complete(run_task)

for t in task:
    print("task result:", t.result())

print("time:", time.time() - start)

# 输出:
wait: 1
wait: 2
wait: 3
task result: done of 1
task result: done of 2
task result: done of 3
time: 3.0037271976470947

通过 Semaphore 信号量机制控制并发数量
通过 await 再调用另外一个协程,这样可以实现协程的嵌套

  • await asyncio.gather(*task)
  • await asyncio.wait(task)
  • asyncio.as_completed(task)
import time
import asyncio
import aiohttp

URL = "https://www.baidu.com"

# 设置并发数:3
sema = asyncio.Semaphore(3)

cookie_jar = aiohttp.CookieJar(unsafe=True)
session = None

async def fetcher(url, index):
    """
    通过 aiohttp 非阻塞的方式访问 URL 资源
    """
    async with session.get(url) as resp:
        print("start fetch index:{}".format(index))
        # 假装多卡1秒
        await asyncio.sleep(1)
        return await resp.text()
    
async def worker(url, index):
    """
    Semaphore信号量机制控制并发
    """
    with (await sema):
        resp = await fetcher(url, index)
        return ("index:", index, len(resp), time.time())

async def dispatch(task_list):
    """
    派发下载任务
    """
    # init session
    global session
    session = aiohttp.ClientSession(cookie_jar=cookie_jar)
    # send task
    tasks = [asyncio.ensure_future(worker(URL, t)) for t in task_list]
    for task in asyncio.as_completed(tasks):
        resp = await task
        print(resp)
    # release session
    session.close()

start = time.time()
        
loop = asyncio.get_event_loop()
coroutine = dispatch(range(5))
loop.run_until_complete(coroutine)
print("total time:", time.time() - start)

# 输出:
start fetch index:2
start fetch index:1
start fetch index:0
('index:', 2, 227, 1508508870.628295)
('index:', 1, 227, 1508508870.642124)
('index:', 0, 227, 1508508870.6424)
start fetch index:4
start fetch index:3
('index:', 4, 227, 1508508871.736131)
('index:', 3, 227, 1508508871.737195)
total time: 2.2324538230895996
协程停止

Future 对象状态:

  • pending
  • running
  • waiting (瞎蒙的)
  • done
  • canceled

Future 对象在协程创建之后状态为 pending,事件循环调度执行协程时状态变为 running,想要停止协程,调用 future.cancel() 即可。

import time
import asyncio

async def test(x):
    print("wait:", x)
    await asyncio.sleep(x)
    return "done of {}".format(x)

coroutine1 = test(1)
coroutine2 = test(10)
coroutine3 = test(15)

loop = asyncio.get_event_loop()

task = [
    loop.create_task(coroutine1),
    loop.create_task(coroutine2),
    loop.create_task(coroutine3)
]

start = time.time()

try:
    loop.run_until_complete(asyncio.wait(task))
except KeyboardInterrupt:
    print(asyncio.Task.all_tasks())
    print(asyncio.gather(*asyncio.Task.all_tasks()).cancel())
    loop.stop()
    loop.run_forever()
finally:
    loop.close()

print("time:", time.time() - start)

# 输出:
wait: 1
wait: 10
wait: 15
^C{<Task pending coro=<test() running at a.py:6> wait_for=<Future pending cb=[Task._wakeup()]> cb=[_wait.<locals>._on_completion() at /usr/local/var/pyenv/versions/3.5.3/Python.framework/Versions/3.5/lib/python3.5/asyncio/tasks.py:422]>, <Task pending coro=<test() running at a.py:6> wait_for=<Future pending cb=[Task._wakeup()]> cb=[_wait.<locals>._on_completion() at /usr/local/var/pyenv/versions/3.5.3/Python.framework/Versions/3.5/lib/python3.5/asyncio/tasks.py:422]>, <Task pending coro=<wait() running at /usr/local/var/pyenv/versions/3.5.3/Python.framework/Versions/3.5/lib/python3.5/asyncio/tasks.py:355> wait_for=<Future pending cb=[Task._wakeup()]> cb=[_run_until_complete_cb() at /usr/local/var/pyenv/versions/3.5.3/Python.framework/Versions/3.5/lib/python3.5/asyncio/base_events.py:176]>, <Task finished coro=<test() done, defined at a.py:4> result='done of 1'>}
True
time: 1.4758961200714111
结语

Asyncio 对于熟悉 Tornado 或 Twisted 等异步框架的同学上手起来会很快,编程风格也可以很"同步化"。目前我们仅在生产环境尝试了 asyncio + aiohttp 作为网络采集的解决方案,初步使用下来感觉还是挺稳定的,并且避免了之前使用 Gevent Monkey Patch 的侵入式改动,Aysncio 还有更多的场景等待我们去发掘(比如 aiohttp 作为 Web 服务)。

目前 Github 开源的部分支持异步非阻塞的 aio 库,链接:https://github.com/aio-libs

对于新事物,永远保持一颗探索的心,共勉。

参考

https://docs.python.org/3/library/asyncio.html
https://liam0205.me/2017/01/17/layers-and-operation-system/
https://segmentfault.com/a/1190000003063859

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

推荐阅读更多精彩内容