【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>
非堵塞和异步有什么区别?
非堵塞
在tornado的框架中非堵塞一般指得是网络I/O层面的socket数据接收模式(select或者epoll),不论用哪个模式,最终程序都会收到数据并处理数据(这个数据要么被转发、要么被解析和处理)。
非堵塞的弊端: 如果处理一个密集计算的请求需要花费10秒钟(就是堵塞了10秒钟),当两个或多个请求同时到达时,只要第一个被接受处理没结束,其他全部请求都要等,并且挨个挨个等到被轮询结束。这就是单线程事件还回机制(非堵塞机制), 对堵塞零容忍, 任何一个地方堵住了还回线程,其他全部请求都被堵住。
也就是说采用了非堵塞模式之后,最好不要用堵塞(常规解析数据的函数)的代码块来解析数据。
异步
异步的作用是将堵塞代码错开来,不放在当前接受数据的线程中处理,
要么丢到rabbitmq/zeromq/activemq中交给另外一个进程去处理,要么用其他线程或进程来处理。
让监听数据的这个socket收到数据后直接抛给其他程序来处理,然后立马保持监听状态,这样子程序的循环能力就非常强。
再就是要提的一点,tornado本身的ioloop就采用epool/select/kqueue来完成非堵塞动作,咱们使用tornado只要把异步的代码写好就可以很好的发挥出tornado的优势了。
堵塞模式编程流程:
传统的I/O(socket)堵塞编程模式流程:
while True:
1. socket accept (等待)
2. socket receive (接受数据)
3. handle data (处理数据)
4. socket send (返回结果)
非堵塞模式编程流程:
while True:
1. events = epoll poll (主动拉取列表)
2. for file_descriptor, event in events: (查找是否有新的请求)
3. async handle data
3.1 标注状态(running)
3.2 异步丢给其他函数通过线程的方式执行.
3.3 线程执行完毕后修改状态为(finish), 并且通过回掉的方式注册进 ioloop中(ioloop.add_done_callback或者ioloop.add_future)
4. socket send (返回结果)
Python环境准备
1. python >= 2.7 < 3.x
2. pip install requests tornado futures
Server环境准备
centos 7 | blockingServer.py | 192.168.1.100 | 构建用于测试的堵塞环节 |
windows 8 | blockingClient.py nbAsync.py nbFuture.py nbCoroutine.py nbGenTask.py |
192.168.1.101 | 验证常用异步非堵塞写法 |
centos 7 | siege、ab | 192.168.1.102 | 并发环境 |
启动Server
在192.168.1.100服务器上运行用于测试的堵塞服务器(实际上是非堵塞模式,只不过是每个连接都要等待5秒钟).
# 目的是提供一个堵塞的环境用来证明tornado结合常用的异步写法都是非堵塞高效模式.
python blockingServer.py
# -.- coding:utf-8 -.-
import tornado.web
import tornado.gen
import tornado.ioloop
import tornado.options
import tornado.httpserver
class BlockingHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self, *args, **kwargs):
# 如果这条命令没看懂的话,请参考这个链接: http://www.tornadoweb.org/en/stable/faq.html
yield tornado.gen.sleep(5)
self.write('ok')
class Application(tornado.web.Application):
def __init__(self):
handlers = [
('/blocking', BlockingHandler),
]
super(Application, self).__init__(handlers)
if __name__ == "__main__":
tornado.options.define("port", default=88, help="run on the given port", type=int)
tornado.options.parse_command_line()
http_server = tornado.httpserver.HTTPServer(Application())
http_server.listen(tornado.options.options.port)
tornado.ioloop.IOLoop.current().start()
1. tornado + 非异步代码(堵塞的代码)
代码:
# 文件名: blockingClient.py
# -.- coding:utf-8 -.-
# __author__ = 'zhengtong'
import tornado.ioloop
import tornado.web
import tornado.options
import tornado.httpserver
import requests
class Application(tornado.web.Application):
def __init__(self):
handlers = [
('/blocking', BlockHandler),
('/non_blocking', NonBlockHandler),
]
super(Application, self).__init__(handlers)
class BlockHandler(tornado.web.RequestHandler):
def get(self, *args, **kwargs):
response = requests.get('http://192.168.1.100:88/blocking') # blocked here.
result = dict(response.headers)
result.update({'content': response.content})
self.write(result)
class NonBlockHandler(tornado.web.RequestHandler):
def get(self, *args, **kwargs):
self.write('non_blocking')
if __name__ == "__main__":
tornado.options.define("port", default=80, help="run on the given port", type=int)
tornado.options.parse_command_line()
http_server = tornado.httpserver.HTTPServer(Application())
http_server.listen(tornado.options.options.port)
tornado.ioloop.IOLoop.current().start()
测试方法:
1. 在192.168.1.102压力测试服务器上运行如下并发测试命令.
# 发起10个并发,持续60秒钟.
[root@localhost ~]# siege http://192.168.1.101/blocking -c10 -t60s
2. 在 windows 8 (192.168.1.101)上用浏览器来访问如下链接.
http://192.168.1.101/non_blocking
测试结果:
siege:
** SIEGE 4.0.2
** Preparing 10 concurrent users for battle.
The server is now under siege...
HTTP/1.1 200 5.07 secs: 212 bytes ==> GET /blocking
HTTP/1.1 200 10.15 secs: 212 bytes ==> GET /blocking
HTTP/1.1 200 15.23 secs: 212 bytes ==> GET /blocking
HTTP/1.1 200 20.31 secs: 212 bytes ==> GET /blocking
HTTP/1.1 200 25.38 secs: 212 bytes ==> GET /blocking
HTTP/1.1 200 30.47 secs: 212 bytes ==> GET /blocking
HTTP/1.1 200 35.55 secs: 212 bytes ==> GET /blocking
HTTP/1.1 200 40.63 secs: 212 bytes ==> GET /blocking
HTTP/1.1 200 45.71 secs: 212 bytes ==> GET /blocking
HTTP/1.1 200 45.53 secs: 212 bytes ==> GET /blocking
HTTP/1.1 200 45.51 secs: 212 bytes ==> GET /blocking
Lifting the server siege...
Transactions: 11 hits
Availability: 100.00 %
Elapsed time: 59.65 secs
Data transferred: 0.00 MB
Response time: 29.05 secs
Transaction rate: 0.18 trans/sec
Throughput: 0.00 MB/sec
Concurrency: 5.36
Successful transactions: 11
Failed transactions: 0
Longest transaction: 45.71
Shortest transaction: 5.07
浏览器:
non_block也是等待状态,必须要等block执行完成后,才会执行non_block.
结论:
siege在60秒钟内,只得到了11个结果,证明堵塞非常严重, 并且浏览器也是出于一直等待的状态.
也就是说在tornado中如果写堵塞代码,只有单线程在运行的tornado,会死的很难看,刚接触tornado的同学甚至都不知道为什么会这样,根本没有像听说那样tornado是一个极其高效的web框架。
通过结果可以看出,不采用异步的方式就无法发挥出它的能力。
2. tornado.web.asynchronous
代码:
# 文件名: nbAysnc.py
# -.- coding:utf-8 -.-
# __author__ = 'zhengtong'
import tornado.ioloop
import tornado.web
import tornado.options
import tornado.httpserver
# import requests # 不用requests, 后面再讨论用requests也能异步非堵塞.
import tornado.httpclient # 采用tornado自带的异步httpclient客户端
class Application(tornado.web.Application):
def __init__(self):
handlers = [
('/blocking', BlockHandler),
('/non_blocking', NonBlockHandler),
]
super(Application, self).__init__(handlers)
class BlockHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self, *args, **kwargs):
client = tornado.httpclient.AsyncHTTPClient()
client.fetch('http://192.168.1.100:88/blocking', callback=self.on_response)
def on_response(self, content):
result = dict(content.headers)
result.update({'content': content.body})
self.write(result)
self.finish()
class NonBlockHandler(tornado.web.RequestHandler):
def get(self, *args, **kwargs):
self.write('non_blocking')
if __name__ == "__main__":
tornado.options.define("port", default=80, help="run on the given port", type=int)
tornado.options.parse_command_line()
http_server = tornado.httpserver.HTTPServer(Application())
http_server.listen(tornado.options.options.port)
tornado.ioloop.IOLoop.current().start()
# 这里提供一个不采用任何装饰器的写法, 比较raw ioloop, 运行结果是一致的,效率也是一致的.
# 文件名: nbAsync_NoAsyncDecorator.py
# -.- coding:utf-8 -.-
# __author__ = 'zhengtong'
import tornado.ioloop
import tornado.web
import tornado.options
import tornado.httpserver
import tornado.concurrent
# import requests # 不只用requests
import tornado.httpclient # 采用tornado自带的异步httpclient客户端
class Application(tornado.web.Application):
def __init__(self):
handlers = [
('/blocking', BlockHandler),
('/non_blocking', NonBlockHandler),
]
super(Application, self).__init__(handlers)
class BlockHandler(tornado.web.RequestHandler):
def get(self, *args, **kwargs): # def get上方移除了tornado.web.asynchonous装饰器
self._auto_finish = False
client = tornado.httpclient.AsyncHTTPClient()
future = client.fetch('http://192.168.1.100:88/blocking') # 在这里添加callback也行
tornado.ioloop.IOLoop.current().add_future(future, callback=self.on_response)
def on_response(self, content):
result = dict(content.headers)
result.update({'content': content.body})
self.write(result)
self.finish()
class NonBlockHandler(tornado.web.RequestHandler):
def get(self, *args, **kwargs):
self.write('non_blocking')
if __name__ == "__main__":
# 通过define 可以为options增加变量.
tornado.options.define("port", default=80, help="run on the given port", type=int)
tornado.options.parse_command_line()
http_server = tornado.httpserver.HTTPServer(Application())
http_server.listen(tornado.options.options.port)
tornado.ioloop.IOLoop.current().start()
测试方法:
参考<1. tornado + 非异步代码(堵塞的代码) >章节的测试方法.
测试结果:
siege:
Lifting the server siege...
Transactions: 100 hits
Availability: 100.00 %
Elapsed time: 59.13 secs
Data transferred: 0.02 MB
Response time: 5.61 secs
Transaction rate: 1.69 trans/sec
Throughput: 0.00 MB/sec
Concurrency: 9.48
Successful transactions: 100
Failed transactions: 0
Longest transaction: 10.62
Shortest transaction: 5.07
浏览器:
访问non_blocking页面正常而且响应很快。
结论
siege一直在持续并发请求的同时用浏览器来访问non_blocking和blocking页面都能够得到响应,也就证明tornado已经开始发挥它的功效了。
采用了异步非堵塞模式后,被命中只有110次,落差很大,心理非常不平衡。其实这并不是问题,这里面有多重限制所以才会导致这个结果。
1. AsyncHttpClient本身的限制(默认情况下只允许同时发起10个客户端). 详情请参考tornado源码的 simple_httpclient.py文件
2. ioloop本身的限制(为了保证线程的稳定性,默认只开启了10个线程来支持并发). 详情请参考tornado源码的 netutil.py文件
可以通过设定参数来提高并发能力(将 tornado.httpclient.AsyncHTTPClient() 改为 tornado.httpclient.AsyncHTTPClient(max_clients=100)).
max_clients由默认的10改为100后,测试结果的hits也随之增加了十倍.
Lifting the server siege...
Transactions: 1099 hits
Availability: 100.00 %
Elapsed time: 59.71 secs
Data transferred: 0.22 MB
Response time: 5.10 secs
Transaction rate: 18.41 trans/sec
Throughput: 0.00 MB/sec
Concurrency: 93.81
Successful transactions: 1099
Failed transactions: 0
Longest transaction: 6.34
Shortest transaction: 5.06
3. tornado.concurrent.futures
代码:
# 文件名: nbFuture.py
# 备注: 在第二章节中的移除tornado.web.asynchonous装饰器的写法同样适合futures. 详情请参考源码文件: nbFuture_NoAsyncDecorator.py
# -.- coding:utf-8 -.-
# __author__ = 'zhengtong'
import tornado.ioloop
import tornado.web
import tornado.options
import tornado.httpserver
import tornado.concurrent
# import requests # 不只用requests
import tornado.httpclient # 采用tornado自带的异步httpclient客户端
class Application(tornado.web.Application):
def __init__(self):
handlers = [
('/blocking', BlockHandler),
('/non_blocking', NonBlockHandler),
]
super(Application, self).__init__(handlers)
class BlockHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self, *args, **kwargs):
client = tornado.httpclient.AsyncHTTPClient()
future = tornado.concurrent.Future()
fetch_future = client.fetch('http://192.168.1.100:88/blocking', callback=self.on_response)
fetch_future.add_done_callback(lambda x: future.set_result(x.result()))
def on_response(self, content):
result = dict(content.headers)
result.update({'content': content.body})
self.write(result)
self.finish()
class NonBlockHandler(tornado.web.RequestHandler):
def get(self, *args, **kwargs):
self.write('non_blocking')
if __name__ == "__main__":
# 通过define 可以为options增加变量.
tornado.options.define("port", default=80, help="run on the given port", type=int)
tornado.options.parse_command_line()
http_server = tornado.httpserver.HTTPServer(Application())
http_server.listen(tornado.options.options.port)
tornado.ioloop.IOLoop.current().start()
测试方法:
参考<1. tornado + 非异步代码(堵塞的代码) >章节的测试方法.
测试结果:
于<2. tornado.web.asynchronous >的测试结果基本一致.
结论
future是官方特别推荐用来练习的一种编码方式,因为这样会比较深入的了解tornado的运作原理。
future的add_done_callback方法,是告诉ioloop当future的状态变更为完成的时候,就调用包裹在add_done_callback中的函数(或匿名函数).
future还提供了一组produce方法和consumer方法, 用于管理future的状态.
4. tornado.gen.Task
代码:
# 文件名: nbGenTask.py
# -.- coding:utf-8 -.-
# __author__ = 'zhengtong'
import tornado.ioloop
import tornado.web
import tornado.options
import tornado.httpserver
import tornado.concurrent
import tornado.gen # 导入tornado.gen模块
# import requests # 不只用requests
import tornado.httpclient # 采用tornado自带的异步httpclient客户端
class Application(tornado.web.Application):
def __init__(self):
handlers = [
('/blocking', BlockHandler),
('/non_blocking', NonBlockHandler),
]
super(Application, self).__init__(handlers)
class BlockHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self, *args, **kwargs):
client = tornado.httpclient.AsyncHTTPClient()
content = yield tornado.gen.Task(client.fetch, ('http://192.168.1.100:88/blocking'))
result = dict(content.headers)
result.update({'content': content.body})
self.write(result)
self.finish()
class NonBlockHandler(tornado.web.RequestHandler):
def get(self, *args, **kwargs):
self.write('non_blocking')
if __name__ == "__main__":
tornado.options.define("port", default=80, help="run on the given port", type=int)
tornado.options.parse_command_line()
http_server = tornado.httpserver.HTTPServer(Application())
http_server.listen(tornado.options.options.port)
tornado.ioloop.IOLoop.current().start()
测试方法:
参考<1. tornado + 非异步代码(堵塞的代码) >章节的测试方法.
测试结果:
于<2. tornado.web.asynchronous >的测试结果基本一致.
结论
tornado.gen.Task需要配合tornado.gen.coroutine装饰器来完成代码的运行,因为Task利用了yield,它的隐藏方法run()利用了gen.send()方法,所以gen模块必须要用coroutine装饰器.
利用coroutine的方式比较明显的一个地方是,代码不用再分开了, 这个是Python语言的一个特性,yield关键字可以赋值给一个变量, 因此就不需要callback了.
这样有什么好处? 本地变量和全局变量不用传递了,默认就是共享的,这个算不算很爽?
5. tornado.gen.coroutine + ThreadPool/ProcessPool
代码:
# 文件名: nbFuture.py
# -.- coding:utf-8 -.-
# __author__ = 'zhengtong'
import tornado.ioloop
import tornado.web
import tornado.options
import tornado.httpserver
import tornado.concurrent
import tornado.gen
import requests
import tornado.concurrent # 导入 tornado.concurrent 并发模块
class Application(tornado.web.Application):
def __init__(self):
handlers = [
('/blocking', BlockHandler),
('/non_blocking', NonBlockHandler),
]
super(Application, self).__init__(handlers)
# 建议设定为CPU核心数量 * 4或8或16也是可以接受的, 取决于计算量,计算量越大设定的值应该越小.
self.executor = tornado.concurrent.futures.ThreadPoolExecutor(16)
class BlockHandler(tornado.web.RequestHandler):
@property
def executor(self):
return self.application.executor
@tornado.gen.coroutine
def get(self, *args, **kwargs):
print dir(self)
content = yield self.executor.submit(requests.get, ('http://192.168.1.100:88/blocking'))
result = dict(content.headers)
result.update({'content': content.content})
self.write(result)
class NonBlockHandler(tornado.web.RequestHandler):
def get(self, *args, **kwargs):
self.write('non_blocking')
if __name__ == "__main__":
# 通过define 可以为options增加变量.
tornado.options.define("port", default=80, help="run on the given port", type=int)
tornado.options.parse_command_line()
http_server = tornado.httpserver.HTTPServer(Application())
http_server.listen(tornado.options.options.port)
tornado.ioloop.IOLoop.current().start()
测试方法:
参考<1. tornado + 非异步代码(堵塞的代码) >章节的测试方法.
测试结果:
于<2. tornado.web.asynchronous >的测试结果基本一致.
结论
自从了coroutine、threadpool、processpool之后,tornado算是一个里程碑式的解放了对异步的要求, 原因是tornado的异步库只针对httpclient, 没有针对mysql或者其他数据库的异步库(自己写一个异步库难度太高,因为辗转十几个源码文件的重度调用以及每个类中的状态控制)。
coroutine结合threadpool让编写异步代码不再拆成多个函数,变量能够共享,堵塞的代码(例如 requests、mysql.connect、密集计算)可以不影响ioloop,形成真正的闭合.
参考:
https://github.com/tornadoweb/tornado/wiki
http://scotdoyle.com/python-epoll-howto.html
tornado源码
来源:oschina
链接:https://my.oschina.net/u/2452965/blog/715615