美文网首页我爱编程
python 异步编程---curio

python 异步编程---curio

作者: hugoren | 来源:发表于2018-05-17 10:58 被阅读0次

    为什么使用协程

    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官方文档

    相关文章

      网友评论

        本文标题:python 异步编程---curio

        本文链接:https://www.haomeiwen.com/subject/egxbdftx.html