Tornado: IOLoop.current() doesn't work in non-main thread with AsyncIOLoop configured

Created on 2 Nov 2017  路  10Comments  路  Source: tornadoweb/tornado

>>> loop = IOLoop.current()
>>> loop
<tornado.platform.asyncio.AsyncIOMainLoop at 0x7fba6ca64550>
>>> def f():
...:    l = IOLoop.current()
...:    print(l, l is loop)
...:    
>>> threading.Thread(target=f).start()
>>> Exception in thread Thread-572:
Traceback (most recent call last):
  File "/home/antoine/miniconda3/envs/dask36/lib/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/home/antoine/miniconda3/envs/dask36/lib/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "<ipython-input-6-23395fe1f1eb>", line 2, in f
    l = IOLoop.current()
  File "/home/antoine/tornado/tornado/ioloop.py", line 254, in current
    current = AsyncIOMainLoop()
  File "/home/antoine/tornado/tornado/util.py", line 306, in __new__
    instance.initialize(*args, **init_kwargs)
  File "/home/antoine/tornado/tornado/platform/asyncio.py", line 167, in initialize
    super(AsyncIOMainLoop, self).initialize(asyncio.get_event_loop(),
  File "/home/antoine/miniconda3/envs/dask36/lib/python3.6/asyncio/events.py", line 676, in get_event_loop
    return get_event_loop_policy().get_event_loop()
  File "/home/antoine/miniconda3/envs/dask36/lib/python3.6/asyncio/events.py", line 584, in get_event_loop
    % threading.current_thread().name)
RuntimeError: There is no current event loop in thread 'Thread-572'.

This is because the default asyncio event loop policy only automatically creates an event loop for the main thread.

Most helpful comment

It didn't appear "again"; this is how it was always going to be in Tornado 5.0 because that's how asyncio works. If you need to run event loops in multiple threads, you can either create one explicitly in each thread (asyncio.set_event_loop(asyncio.new_event_loop())) or install Tornado's AnyThreadEventLoopPolicy.

All 10 comments

I'm not sure about this. We generally want to be converging on behavior with asyncio, so it seems to me that applications that want to use event loops on non-main threads should probably be responsible for setting up an appropriate event loop policy just as they would if they were using asyncio without tornado. It would be weird if event loops for other threads were created automatically or not depending on whether you hit IOLoop.current() or asyncio.get_event_loop() first.

It's admittedly annoying for applications that don't care about asyncio (especially if they're trying to span python 2 and 3), but it's only for the small minority that use atypical event loop arrangements.

Well, IOLoop.current() doesn't need any special care if using Tornado's PollIOLoop. The main annoyance here is that it's not trivial to write an asyncio loop policy (you have to subclass the default policy - which is itself platform-specific, and which the asyncio docs don't give any guidance about). Note that we already did so here: https://github.com/dask/distributed/blob/master/distributed/utils.py#L1189, so we could live without this PR being merged.

I was also surprised by asyncio's behaviour and didn't expect it to require such handling, especially since the official docs only state that "the default policy defines context as the current thread, and manages an event loop per thread that interacts with asyncio" (https://docs.python.org/3/library/asyncio-eventloops.html#asyncio.set_event_loop_policy).

Yeah, that policy interface is annoying. There ought to be an easy way to enable the creation of event loops on every thread. Maybe Tornado should include a policy like this.

Also note that if you're able to run some code in each thread before you need the event loop, you can just do asyncio.set_event_loop(asyncio.new_event_loop()), without touching the policy.

I think this policy is reasonable as a sub-thread cannot decide automatically if a event loop is needed. If asyncio create new event loop for every sub-thread but most of time they are unused, there will be a lot of wastes.

Further, main thread is unique so just create one unused event loop is OK. But sub-thread could be unlimited.

No one's suggesting that an event loop be created for every thread, only for those threads that do something involving asyncio. This won't lead to waste unless you have a lot of threads that sometimes do something asynchronous but spend most of their time without an event loop, but that would be a very strange pattern. The asyncio "main thread only" policy is not so much about avoiding waste as steering new users towards the pattern that should almost always be used (only one event loop, probably in the main thread).

this bug appeared again in 5.0.0?

code:

import threading

from tornado.ioloop import IOLoop


loop = IOLoop.current()

def f():
    l = IOLoop.current()
    print(l, l is loop)

threading.Thread(target=f).start()

run tornado 4.5.3

<tornado.platform.epoll.EPollIOLoop object at 0x7f1499226978> True

run tornado 5.0.0

Exception in thread Thread-8:
Traceback (most recent call last):
  File "/usr/lib/python3.5/threading.py", line 914, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.5/threading.py", line 862, in run
    self._target(*self._args, **self._kwargs)
  File "<ipython-input-1-c60044c05b63>", line 9, in f
    l = IOLoop.current()
  File "/usr/local/lib/python3.5/dist-packages/tornado/ioloop.py", line 283, in current
    loop = asyncio.get_event_loop()
  File "/usr/lib/python3.5/asyncio/events.py", line 632, in get_event_loop
    return get_event_loop_policy().get_event_loop()
  File "/usr/lib/python3.5/asyncio/events.py", line 578, in get_event_loop
    % threading.current_thread().name)
RuntimeError: There is no current event loop in thread 'Thread-8'.

It didn't appear "again"; this is how it was always going to be in Tornado 5.0 because that's how asyncio works. If you need to run event loops in multiple threads, you can either create one explicitly in each thread (asyncio.set_event_loop(asyncio.new_event_loop())) or install Tornado's AnyThreadEventLoopPolicy.

@bdarnell when I updated to version 5.0.0 I did not realize this change, I was not very familiar with asyncio either. I'm sorry if the message might sound wrong.

The solution that comments works perfectly :) thanks

Just wanted to say thanks @bdarnell for responding. Your answer helped me.

Was this page helpful?
0 / 5 - 0 ratings