简介+异步+协程

tornado是异步的,通过使用非阻塞的io方式,可以处理c10k问题,可以处理长连接、 websockets。

安装

py2.7 py3.3+

concurrent.futures

推荐使用的线程池 py3自带,用于ThreadResolver

pycurl

用于tornado.curl_httpclient

Twisted

用于tornado.platform.twisted

pycares

非阻塞DNS解析

monotonic Monotime

在时钟一直改变的情况 在py3.3之后就不需要了

异步和非阻塞IO

实时的web需要每用户的长连接(大多数情况是空闲的),如果这样的话,传统的同步web服务器的解决方案是每个用户一个线程,这样耗费较高。tornado则使用单线程的事件循环,所以应用代码都应该是异步的、非阻塞的,因为同时只能执行一个操作。

阻塞

是等待返回(等待网络IO,磁盘IO,锁),当然实际上任何函数调用都是要被阻塞的(因为要使用CPU)。

tornado的httpclient的DNS解析默认是阻塞的,但是如果使用了ThreadResolver或tornado.curl_httpclient(配合libcurl)则不阻塞了。tornado关注的阻塞主要是在网络IO时的阻塞。

异步

异步函数是在它执行完之前就返回了,这样一般会造成一些工作在后台继续工作,直到达成某些条件。相比通常的同步函数则是在所有的东西都完成后才返回。有很多种异步的风格:

回调参数模式
返回一个占用符 (Future, Promise, Deferred)
分配到队列中
注册回调 (e.g. POSIX signals)

没有什么透明的方法使得同步函数异步,(gevent只是使用了轻量线程切换,并不是真正的异步)

同步函数

from tornado.httpclient import HTTPClient

def synchronous_fetch(url):
    http_client = HTTPClient()
    response = http_client.fetch(url)
    return response.body

异步函数

使用callback argument的:

from tornado.httpclient import AsyncHTTPClient

# 调用时需要给一个回调函数
def asynchronous_fetch(url, callback):
    http_client = AsyncHTTPClient()
    # 注册async回调函数 在其中调用给定的回调函数
    def handle_response(response):
        callback(response.body)
    http_client.fetch(url, callback=handle_response)  # callback  

使用Future的:

from tornado.concurrent import Future
def async_fetch_future(url):
    http_client = AsyncHTTPClient()
    my_future = Future()
    fetch_future = http_client.fetch(url)
    fetch_future.add_done_callback(
        lambda f: my_future.set_result(f.result()))
    return my_future

直接使用future的方式较为复杂,但是有优点:

错误处理可以更直观,因为Future.result方法可以直接raise异常
方便协程使用

协程版本(生成器)

from tornado import gen

@gen.coroutine
def fetch_coroutine(url):
    http_client = AsyncHTTPClient()
    response = yield http_client.fetch(url)
    raise gen.Return(response.body)  # 为了python2中生成器不能返回值所以这部是抛出异常来被捕获作为返回值 python3.3及更高会直接return

协程

推荐使用协程版本来写异步代码,在协程中通过使用yield来暂停并恢复执行(相比gevent,使用的是显式的切换并作为异步函数调用)

python3.5 async await

可以使用async来替换@gen.coroutine,await来替换yield,这样会更快

async def fetch_coroutine(url):
http_client = AsyncHTTPClient()
response = await http_client.fetch(url)
return response.body

yield可以抛出Futures的list,但是await需要使用tornado.gen.multi,可以通过:

async def f():
    executor = concurrent.futures.ThreadPoolExecutor()
    await tornado.gen.convert_yielded(executor.submit(g))

但是原生的协程是不需要绑定到特定框架的

工作机制

包含yield的就是一个生成器,所有的生成器都是异步的,当被调用时返回一个生成器而不是运行并直到完成。装饰器@gen.coroutine则通过yield来和生成器交流,返回给调用者一个Future。

装饰器的内部循环:

def run(self):
    future = self.gen.send(self.next)
    def callback(f):
        self.next = f.result()
        self.run()
    future.add_done_callback(callback)

装饰器收到生成器的Future,等待future完成(非阻塞),之后发送回给生成器作为yield的值。大多数的异步代码不涉及Future的直接使用,除了是立即发送Future给yield的值(通过异步函数返回的Future)

调用

协程不是按常规方式引发异常的,所有引发的异常都在Future里面直到被yield,所以需要注意使用的方式:

@gen.coroutine
def divide(x, y):
    return x / y

def bad_call():
    divide(1, 0)  # 常规来说是触发除零错误 但是并不会

@gen.coroutine
def good_call():
    yield divide(1, 0)

调用协程的必须自己也是一个协程,并使用yield语句来调用。在开发中,如果想重载基类的一个方法,需要查看文档来看它是否可用协程(文档中会说明该方法可以为协程或可以返回Future)。

如果不想要结果,可以使用IOLoop.spawn_callback来让IOLoop负责调用。如抛出了异常IOLoop会捕获并打印堆栈信息。

IOLoop.current().spawn_callback(divide, 1, 0)  # 在IOloop启动的情况下添加一个调用

IOLoop.current().run_sync(lambda: divide(1, 0))  # 启动IOloop 运行 然后结束

协程模式

应对使用callback的

@gen.coroutine
def call_task():
    yield gen.Task(some_function, other_args)
    # some_function(other_args, callback=callback)

这样可按some_function的定义模式(需要callback)来正常调用,但是会返回一个Future这样可在协程中使用。

调用阻塞函数

thread_pool = ThreadPoolExecutor(4)

@gen.coroutine
def call_blocking():
    yield thread_pool.submit(blocking_func, args)

并行

如果返回list或dict形式的Futures的,会等待所有的Future完成(并行的):

@gen.coroutine
def parallel_fetch(url1, url2):
    resp1, resp2 = yield [http_client.fetch(url1),
                        http_client.fetch(url2)]

@gen.coroutine
def parallel_fetch_many(urls):
    responses = yield [http_client.fetch(url) for url in urls]
    # responses is a list of HTTPResponses in the same order

@gen.coroutine
def parallel_fetch_dict(urls):
    responses = yield {url: http_client.fetch(url)
                        for url in urls}
    # responses is a dict {url: HTTPResponse}

插入

有时想先保存一个Future而不是立即yield出

@gen.coroutine
def get(self):
    fetch_future = self.fetch_next_chunk()  # 先保存的
    while True:
        chunk = yield fetch_future
        if chunk is None: break
        self.write(chunk)
        fetch_future = self.fetch_next_chunk()
        yield self.flush()

如果是使用了async def的形式,需要fetch_future = tornado.gen.convert_yielded(self.fetch_next_chunk())这样来调用

循环

import motor
db = motor.MotorClient().test

@gen.coroutine
def loop_example(collection):
    cursor = db.collection.find()
    while (yield cursor.fetch_next):
        doc = cursor.next_object()

定时调用的

@gen.coroutine
def minute_loop():
    while True:
        yield do_something()
        yield gen.sleep(60)

# Coroutines that loop forever are generally started with
# spawn_callback().
IOLoop.current().spawn_callback(minute_loop)

但是注意,前面的函数是每60+N秒执行一次,(N是执行do_something的时间),

@gen.coroutine
def minute_loop2():
    while True:
        nxt = gen.sleep(60)   # Start the clock.
        yield do_something()  # Run while the clock is ticking.
        yield nxt             # Wait for the timer to run out.

这种方式会先启动时钟,然后执行do_something,然后在睡眠剩余的时间,这样就是的确是每60s执行一次。

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

推荐阅读更多精彩内容