Why asyncio's run_in_executor blocks tornado&#

2019-08-01 04:21发布

问题:

I want to run a slow blocking method (actually from a 3rd-party library) in tornado's async GET request handler. Let the method be just:

def blocking_method(uid):
    print("slow method started: ", uid)
    time.sleep(10)
    print("slow method done: ", uid)
    return "slow method ({}) result".format(uid)

Moreover, I prefer running the tornado server in asyncio's event loop:

if __name__ == '__main__':
    tornado.platform.asyncio.AsyncIOMainLoop().install()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(make_app())
    loop.run_forever()

I knew about @run_in_executor decorator but it's not suitable for me, since I use asyncio. To run a blocking method in async coroutine I should use run_in_executor method of asyncio.get_event_loop(). Here is an example how to do it, from this answer:

import asyncio

async def main():
    loop = asyncio.get_event_loop()
    executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
    future1 = loop.run_in_executor(executor, blocking_method, 1)
    future2 = loop.run_in_executor(executor, blocking_method, 2)
    response1 = await future1
    response2 = await future2
    print(response1)
    print(response2)

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

And it works perfectly, here is output from the previous script:

slow method started:  1
slow method started:  2
slow method done:  2
slow method done:  1
slow method (1) result
slow method (2) result

But if I use the very same technique in async def get method of tornado's RequestHandler:

class AsyncHandler(tornado.web.RequestHandler):

    async def get(self):
        #  simple counter to distinguish requests
        self.application.counter += 1
        in_msg = "Registered request #{}, working...".format(self.application.counter)
        print(in_msg)
        loop = asyncio.get_event_loop()
        future = loop.run_in_executor(self.application.executor,
                                      blocking_method,
                                      self.application.counter)
        result = await future
        out_msg = "Request processed, result: {}".format(result)
        print(out_msg)
        self.write(out_msg)

it blocks the method of the handler. In other words, If I open http://localhost:8888/ in several browser tabs (let it be two), then I expect two requests working in parallel, with the following output:

Registered request #1, working...
slow method started:  1
Registered request #2, working...
slow method started:  2
slow method done:  1
Request processed, result: slow method (1) result
slow method done:  2
Request processed, result: slow method (2) result

But the requests are executed consequently:

Registered request #1, working...
slow method started:  1
slow method done:  1
Request processed, result: slow method (1) result
Registered request #2, working...
slow method started:  2
slow method done:  2
Request processed, result: slow method (2) result

So, where am I wrong? What should I do to allow execution the request handler in parallel?

Here is full script that describes my problem:

import asyncio
import concurrent.futures
import time

import tornado.web
import tornado.platform


def blocking_method(uid):
    print("slow method started: ", uid)
    time.sleep(10)
    print("slow method done: ", uid)
    return "slow method ({}) result".format(uid)


class AsyncHandler(tornado.web.RequestHandler):

    async def get(self):
        #  simple counter to distinguish requests
        self.application.counter += 1
        in_msg = "Registered request #{}, working...".format(self.application.counter)
        print(in_msg)
        loop = asyncio.get_event_loop()
        future = loop.run_in_executor(self.application.executor,
                                      blocking_method,
                                      self.application.counter)
        result = await future
        out_msg = "Request processed, result: {}".format(result)
        print(out_msg)
        self.write(out_msg)

async def make_app():
    handlers = [(r"/", AsyncHandler)]
    app = tornado.web.Application(handlers, debug=True)
    app.executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
    app.counter = 0
    app.listen(8888)

if __name__ == '__main__':
    tornado.platform.asyncio.AsyncIOMainLoop().install()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(make_app())
    loop.run_forever()

回答1:

Browsers will recognize that you are trying to load the same page in two different tabs and delay the second request until the first has finished.

http://www.tornadoweb.org/en/latest/faq.html#why-isn-t-this-example-with-time-sleep-running-in-parallel

  • Add something to your urls to make them unique. Instead of http://localhost:8888 in both tabs, load http://localhost:8888/?x=1 in one and http://localhost:8888/?x=2 in the other.
  • Use two different browsers. For example, Firefox will be able to load a url even while that same url is being loaded in a Chrome tab.