16 Tornado - 认识异步

简介: 16 Tornado - 认识异步

1. 同步

我们用两个函数来模拟两个客户端请求,并依次进行处理:

# coding:utf-8
def req_a():
    """模拟请求a"""
    print '开始处理请求req_a'
    print '完成处理请求req_a'
def req_b():
    """模拟请求b"""
    print '开始处理请求req_b'
    print '完成处理请求req_b'
def main():
    """模拟tornado框架,处理两个请求"""
    req_a()
    req_b()
if __name__ == "__main__":
    main()

执行结果:

开始处理请求req_a
完成处理请求req_a
开始处理请求req_b
完成处理请求req_b

同步是按部就班的依次执行,始终按照同一个步调执行,上一个步骤未执行完不会执行下一步。

想一想,如果在处理请求req_a时需要执行一个耗时的工作(如IO),其执行过程如何?

# coding:utf-8
import time
def long_io():
    """模拟耗时IO操作"""
    print "开始执行IO操作"
    time.sleep(5)
    print "完成IO操作"
    return "io result"
def req_a():
    print "开始处理请求req_a"
    ret = long_io()
    print "ret: %s" % ret
    print "完成处理请求req_a"
def req_b():
    print "开始处理请求req_b"
    print "完成处理请求req_b"
def main():
    req_a()
    req_b()
if __name__=="__main__":
    main()

执行过程:

开始处理请求req_a
开始执行IO操作
完成IO操作
完成处理请求req_a
开始处理请求req_b
完成处理请求req_b

在上面的测试中,我们看到耗时的操作会将代码执行阻塞住,即req_a未处理完req_b是无法执行的。

2. 异步

对于耗时的过程,我们将其交给别人(如其另外一个线程)去执行,而我们继续往下处理,当别人执行完耗时操作后再将结果反馈给我们,这就是我们所说的异步。

我们用容易理解的线程机制来实现异步。

2.1 回调写法实现原理
# coding:utf-8
import time
import thread
def long_io(callback):
    """将耗时的操作交给另一线程来处理"""
    def fun(cb): # 回调函数作为参数
        """耗时操作"""
        print "开始执行IO操作"
        time.sleep(5)
        print "完成IO操作,并执行回调函数"
        cb("io result")  # 执行回调函数
    thread.start_new_thread(fun, (callback,))  # 开启线程执行耗时操作
def on_finish(ret):
    """回调函数"""
    print "开始执行回调函数on_finish"
    print "ret: %s" % ret
    print "完成执行回调函数on_finish"
def req_a():
    print "开始处理请求req_a" 
    long_io(on_finish)
    print "离开处理请求req_a"
def req_b():
    print "开始处理请求req_b"
    time.sleep(2) # 添加此句来突出显示程序执行的过程
    print "完成处理请求req_b"
def main():
    req_a()
    req_b()
    while 1: # 添加此句防止程序退出,保证线程可以执行完
        pass
if __name__ == '__main__':
    main()

执行过程:

开始处理请求req_a
离开处理请求req_a
开始处理请求req_b
开始执行IO操作
完成处理请求req_b
完成IO操作,并执行回调函数
开始执行回调函数on_finish
ret: io result
完成执行回调函数on_finish

异步的特点是程序存在多个步调,即本属于同一个过程的代码可能在不同的步调上同时执行。

2.2 协程写法实现原理

在使用回调函数写异步程序时,需将本属于一个执行逻辑(处理请求a)的代码拆分成两个函数req_a和on_finish,这与同步程序的写法相差很大。而同步程序更便于理解业务逻辑,所以我们能否用同步代码的写法来编写异步程序?

初始版本

# coding:utf-8
import time
import thread
gen = None # 全局生成器,供long_io使用
def long_io():
    def fun():
        print "开始执行IO操作"
        global gen
        time.sleep(5)
        try:
            print "完成IO操作,并send结果唤醒挂起程序继续执行"
            gen.send("io result")  # 使用send返回结果并唤醒程序继续执行
        except StopIteration: # 捕获生成器完成迭代,防止程序退出
            pass
    thread.start_new_thread(fun, ())
def req_a():
    print "开始处理请求req_a"
    ret = yield long_io()
    print "ret: %s" % ret
    print "完成处理请求req_a"
def req_b():
    print "开始处理请求req_b"
    time.sleep(2)
    print "完成处理请求req_b"
def main():
    global gen
    gen = req_a()
    gen.next() # 开启生成器req_a的执行
    req_b()
    while 1:
        pass
if __name__ == '__main__':
    main()

执行过程:

开始处理请求req_a
开始处理请求req_b
开始执行IO操作
完成处理请求req_b
完成IO操作,并send结果唤醒挂起程序继续执行
ret: io result
完成处理请求req_a

升级版本

我们在上面编写出的版本虽然req_a的编写方式很类似与同步代码,但是在main中调用req_a的时候却不能将其简单的视为普通函数,而是需要作为生成器对待。

现在,我们试图尝试修改,让req_a与main的编写都类似与同步代码。

# coding:utf-8
import time
import thread
gen = None # 全局生成器,供long_io使用
def gen_coroutine(f):
    def wrapper(*args, **kwargs):
        global gen
        gen = f()
        gen.next()
    return wrapper
def long_io():
    def fun():
        print "开始执行IO操作"
        global gen
        time.sleep(5)
        try:
            print "完成IO操作,并send结果唤醒挂起程序继续执行"
            gen.send("io result")  # 使用send返回结果并唤醒程序继续执行
        except StopIteration: # 捕获生成器完成迭代,防止程序退出
            pass
    thread.start_new_thread(fun, ())
@gen_coroutine
def req_a():
    print "开始处理请求req_a"
    ret = yield long_io()
    print "ret: %s" % ret
    print "完成处理请求req_a"
def req_b():
    print "开始处理请求req_b"
    time.sleep(2)
    print "完成处理请求req_b"
def main():
    req_a()
    req_b()
    while 1:
        pass
if __name__ == '__main__':
    main()

执行过程:

开始处理请求req_a
开始处理请求req_b
开始执行IO操作
完成处理请求req_b
完成IO操作,并send结果唤醒挂起程序继续执行
ret: io result
完成处理请求req_a

最终版本

刚刚完成的版本依然不理想,因为存在一个全局变量gen来供long_io使用。我们现在再次改写程序,消除全局变量gen。

# coding:utf-8
import time
import thread
def gen_coroutine(f):
    def wrapper(*args, **kwargs):
        gen_f = f()  # gen_f为生成器req_a
        r = gen_f.next()  # r为生成器long_io
        def fun(g):
            ret = g.next() # 执行生成器long_io
            try:
                gen_f.send(ret) # 将结果返回给req_a并使其继续执行
            except StopIteration:
                pass
        thread.start_new_thread(fun, (r,))
    return wrapper
def long_io():
    print "开始执行IO操作"
    time.sleep(5)
    print "完成IO操作,yield回操作结果"
    yield "io result"
@gen_coroutine
def req_a():
    print "开始处理请求req_a"
    ret = yield long_io()
    print "ret: %s" % ret
    print "完成处理请求req_a"
def req_b():
    print "开始处理请求req_b"
    time.sleep(2)
    print "完成处理请求req_b"
def main():
    req_a()
    req_b()
    while 1:
        pass
if __name__ == '__main__':
    main()

执行过程:

开始处理请求req_a
开始处理请求req_b
开始执行IO操作
完成处理请求req_b
完成IO操作,yield回操作结果
ret: io result
完成处理请求req_a

这个最终版本就是理解Tornado异步编程原理的最简易模型,但是,Tornado实现异步的机制不是线程,而是epoll,即将异步过程交给epoll执行并进行监视回调。

需要注意的一点是,我们实现的版本严格意义上来说不能算是协程,因为两个程序的挂起与唤醒是在两个线程上实现的,而Tornado利用epoll来实现异步,程序的挂起与唤醒始终在一个线程上,由Tornado自己来调度,属于真正意义上的协程。虽如此,并不妨碍我们理解Tornado异步编程的原理。

目录
打赏
0
0
1
0
237
分享
相关文章
Python异步: 什么时候使用异步?
Asyncio 是 Python 中用于异步编程的库,适用于协程、非阻塞 I/O 和异步任务。使用 Asyncio 的原因包括:1) 使用协程实现轻量级并发;2) 采用异步编程范式提高效率;3) 实现非阻塞 I/O 提升 I/O 密集型应用性能。然而,Asyncio 并不适合所有场景,特别是在 CPU 密集型任务或已有线程/进程方案的情况下。选择 Asyncio 应基于项目需求和技术优势。
爬虫之协程异步 asyncio和aiohttp
爬虫之协程异步 asyncio和aiohttp
Python中的异步编程:使用asyncio和aiohttp构建高效的异步应用
在当今高度并发和响应式的应用程序中,异步编程成为处理大量并发请求的一种重要方式。Python通过`asyncio`和`aiohttp`库提供了强大的异步编程支持,使得开发者能够构建高效的异步应用。本文将深入探讨如何利用这两个库来构建强大的异步应用程序。
【Python】asyncio+aiohttp——使用协程异步paqu数据
【Python】asyncio+aiohttp——使用协程异步paqu数据
119 0
Python异步: 什么时候使用异步?(3)
从广义上讲,Asyncio 是新的、流行的、讨论广泛的和令人兴奋的。然而,对于何时应该在项目中采用它存在很多困惑。
120 0
Python3的原生协程(Async/Await)和Tornado异步非阻塞
我们知道在程序在执行 IO 密集型任务的时候,程序会因为等待 IO 而阻塞,而协程作为一种用户态的轻量级线程,可以帮我们解决这个问题。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存,在调度回来的时候,恢复先前保存的寄存器上下文和栈。因此协程能保留上一次调用时的状态,即所有局部状态的一个特定组合
Python3的原生协程(Async/Await)和Tornado异步非阻塞
python3.7+Tornado5.1.1+Celery3.1+Rabbitmq3.7.16实现异步队列任务
在之前的一篇文章中提到了用[Django+Celery+Redis实现了异步任务队列](https://v3u.cn/a_id_54),只不过消息中间件使用了redis,redis作为消息中间件可谓是差强人意,功能和性能上都不如Rabbitmq,所以本次使用tornado框架结合celery,同时消息中间件使用Rabbitmq来实现异步发邮件,并且使用flower来监控任务队列。
python3.7+Tornado5.1.1+Celery3.1+Rabbitmq3.7.16实现异步队列任务
【Tornado】协程队列和异步DNS解析器在Tornado项目里的实战表现已经运用详解
【Tornado】协程队列和异步DNS解析器在Tornado项目里的实战表现已经运用详解
190 2