python 异步编程---curio

为什么使用协程

C10K问题

在互联网开始的早期,使用互联网的人较少,一台服务器同时在线的连接也不是很多,所以最初的服务器设计的时候使用进程或者是线程的方式分配一个TCP连接,这个时候不存在C10K的难题。

当到了Web2.0的时代,互联网不再是单纯的浏览网页了,它开始需要进行交互,随着互联网的进一步发展,用户界面和界面交互都变得非常复杂起来,应用程序的逻辑也随之变的更加复杂,即时通信和在线的实时互动已经变的非常普遍了,假设每个用户都必须要与服务器保持一个或者多个TCP连接,而且每一个TCP连接需要占用一个进程(线程)的资源,这样的话,一个服务器的并发连接数是非常高的,一个普通的大一点网页服务的连接可能就过亿了。进程是操作系统最宝贵的资源,一台机器创建不了这么多进程,如果是C10k就要创建1万个进程,这个是操作系统无法承受的。就算是分布式系统,维持1亿用户在线也需要10万台服务器,成本是巨大的,只有FLAG、BAT这样的公司才有财力购买如此多的服务器。

怎么样解决C10K问题

既然有了C10K问题,程序员们就开始行动去解决它。为了解决这一问题,出现了「用同一进程/线程来同时处理若干连接」的思路,也就是I/O多路复用。于是FreeBSD推出了kqueue,Linux推出了epoll,Windows推出了IOCP。这些操作系统提供的功能就是为了解决C10K问题。因为Linux是互联网企业中使用率最高的操作系统,Epoll就成为C10K killer、高并发、高性能、异步非阻塞这些技术的代名词了。

epoll技术的编程模型就是异步非阻塞回调,也可以叫做Reactor,事件驱动,事件轮循(EventLoop)。Epoll就是为了解决C10K问题而生。使用Epoll技术,使得小公司也可以玩高并发。不需要购买很多服务器,有几台服务器就可以服务大量用户。Nginx,libevent,node.js这些就是Epoll时代的产物。

就这样C10K问题解决了,然后又来更高的问题,也就是C100K,C1M等。Epoll既然能解决C10K,解决什么C100K,C1M也是可以的。秘诀就是使用epoll模型,然后多买一些服务器就可以了。但是问题又来了

异步嵌套回调太TM难写了。尤其是Node.js层层回调,缩进了几十层,要把程序员逼疯了。于是一个新的技术被提出来了,那就是协程(coroutine)。这个技术本质上也是异步非阻塞技术,它是将事件回调进行了包装,让程序员看不到里面的事件循环。程序员就像写阻塞代码一样简单。比如调用 client->recv() 等待接收数据时,就像阻塞代码一样写。实际上是底层库在执行recv时悄悄保存了一个状态,比如代码行数,局部变量的值。然后就跳回到EventLoop中了。什么时候真的数据到来时,它再把刚才保存的代码行数,局部变量值取出来,又开始继续执行。

这个就像时间禁止的游戏一样,国王对巫师说“我必须马上得到宝物,不然就砍了你的脑袋”,巫师念了一句时间停止的咒语,直到过了1年后勇士们才把宝物送来。这时候巫师解开咒语,把宝物交给国王。这里国王就可以理解成协程,他根本没感觉到时间停止,在他停止到醒来期间发生了什么他不知道,也不关心。

这就是协程的本质。协程是异步非阻塞的另外一种展现形式。Golang,Erlang,Lua协程都是这个模型。

说的有点远了,关于协程和epoll模型,你可能需要到网上找一些更加详细的资料看看,现在开始我们今天的主题 – curio库使用指南

curio-一个用同步写法进行异步编程的库

如何把同步的代码改成异步的

首先看一个同步的例子:

def handle(id):

    subject = get_subject_from_db(id)

    buyinfo = get_buyinfo(id)

    change = process(subject, buyinfo)

    notify_change(change)

    flush_cache(id)
import curio

async def handle(id):

    async with TaskGroup() as g:

        subject = await g.spawn(get_subject_from_db, id)

        buyinfo = await g.spawn(get_buyinfo, id)

    change = await process(subjetc.result, buginfo)

    await change.join()

    await notifu_change(change.result)

    await flush_cache(id)

其实就是把函数包装成一个Task对象或者说future对象,使用spawn可以把函数包装为Task,然后等待函数完成后,从Task的result属性获取返回值。

下篇我们来聊一聊curio具体有哪些东西和怎么样去使用他们进行异步编程。

一些基本概念

  • event_loop 事件循环:程序开启一个无限的循环,程序员会把一些函数注册到事件循环上。当满足事件发生的时候,调用相应的协程函数。

  • coroutine 协程:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。

  • task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含任务的各种状态。

  • future: 代表将来执行或没有执行的任务的结果。它和task上没有本质的区别

定义一个协程

定义协程很简单,使用python3.5的关键字async,可以像定义普通的函数一样:

import curio

async def countdown(n):

    while n > 0:

        print('T-minus', n)

        await curio.sleep(1)

        n -= 1

if __name__ == '__main__':

    curio.run(countdown, 10)

使用async定义一个协程(coroutine),协程也是一种对象。协程不能直接运行,需要把协程加入到事件循环(loop),由后者在适当的时候调用协程。curio使用curio kernel来运行协程,run()方法可以开始kernel并且初始化Task。

创建一个task

协程对象不能直接运行,在运行kernel的时候,可以curio.spawn方法将协程包装成为了一个任务(task)对象。所谓task对象是Future类的子类。保存了协程运行后的状态,用于未来获取协程的结果。

import curio

async def countdown(n):

    while n > 0:

        print('T-minus', n)

        await curio.sleep(1)

        n -= 1

async def kid():

    print('Building the Millenium Falcon in Minecraft')

    await curio.sleep(1000)

async def parent():

    kid_task = await curio.spawn(kid)

    await curio.sleep(5)

    print("Let's go")

    count_task = await curio.spawn(countdown, 10)

    await count_task.join()

    print("We're leaving!")

    await kid_task.join()

    print('Leaving')

if __name__ == '__main__':

    curio.run(parent)

在当前程序中,parent()使用curio.spawn()创建新的子任务,当sleep一段时间后,countdown开始运行,join()方法会等待这个Task运行结束,在首先等待countdown()完成后,然后程序等待kid()完成,在你运行这个程序时,可以得到下面的结果:

curio monitor

在上个程序中的kid()将会阻塞1000秒,而parent的join方法会等待kid()的完成后才会结束。你可以将代码改成下面的样子来开启monitor:


if __name__ == '__main__':

    curio.run(parent, with_monitor=True)

运行程序,当程序阻塞在kid()的时候,打开monitor工具:

curio > ps

Task State Cycles Timeout Task


1 FUTURE_WAIT 1 None Monitor.monitor_task

2 READ_WAIT 1 None Kernel._run_coro.<locals>._kernel_task

3 TASK_JOIN 3 None parent

4 TIME_SLEEP 1 None kid

curio >

还可以使用where查看追踪task:

curio > w 3

这样手动取消task会抛出TaskCancelled异常,表示程序没有正常运行。因此你需要结束task的时候需要在程序中手动取消:

当然,你在parent取消kid的时候,kid可以捕捉到这个消除请求并且清除它:

同步机制

curio模块包含多种同步机制,它提供和线程一样的同步机制(Event, Lock, Semaphore, and Condition)。看下面使用Event的例子:

start_evt = curio.Event()

async def kid():

print('Can I play?')

await start_evt.wait()

print('Building the Millenium Falcon in Minecraft')

async with curio.TaskGroup() as f:

    await f.spawn(friend, 'Max')

    await f.spawn(friend, 'Lillian')

    await f.spawn(friend, 'Thomas')

    try:

        await curio.sleep(1000)

    except curio.CancelledError:

        print('Fine. Saving my work.')

        raise

async def parent():

kid_task = await curio.spawn(kid)

await curio.sleep(5)

print('Yes, go play')

await start_evt.set()

await curio.sleep(5)

print("Let's go")

count_task = await curio.spawn(countdown, 10)

await count_task.join()

print("We're leaving!")

try:

    await curio.timeout_after(10, kid_task.join)

except curio.TaskTimeout:

    print('I warned you!')

    await kid_task.cancel()

print('Leaving!')

在程序运行kid()的时候,await start_evt.wait()会等待,直到await start_evt.set()运行。

转自:
https://heshangbuxitou.github.io/2017/11/04/curio%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97%EF%BC%88%E4%BA%8C%EF%BC%89/

参考:
聊聊C10K问题及解决方案

Curio官方文档

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

推荐阅读更多精彩内容

  • 前言 很多朋友对异步编程都处于“听说很强大”的认知状态。鲜有在生产项目中使用它。而使用它的同学,则大多数都停留在知...
    星星在线阅读 2,848评论 2 39
  • 1 什么是异步编程 通过学习相关概念,我们逐步解释异步编程是什么。 1.1 阻塞 程序未得到所需计算资源时被挂起的...
    hugoren阅读 2,643评论 2 10
  • 上篇 中篇 下篇 1 什么是异步编程 1.1 阻塞 程序未得到所需计算资源时被挂起的状态。 程序在等待某个操作完成...
    秦时明星阅读 990评论 0 3
  • title标题: A Web Crawler With asyncio Coroutinesauthor作者: A...
    彰乐乐乐乐阅读 2,025评论 0 8
  • 使用JunitGeneratorV2.0生成基于Mockito框架的单元测试类模板 1.安装插件 Setting ...
    JAVA觅音阁阅读 654评论 0 0