[Twisted-Python] threadedselectreactor - shutting down the foreign event loop
Im looking at moving my PyQt app to threadedselectreactor. Previously I was keeping twisted and Qt in seperate threads. Avoiding the need to cross that thread boundary has been a great simplification, but Im having some problems relating to the shutdown of my foreign event loop. The examples such as http://svn.twistedmatrix.com/cvs/trunk/doc/core/examples/threadedselect/pygamedemo.py?view=auto&rev=13596 handle this by: a. Calling reactor.stop() when there is a GUI shutdown request, b. Using reactor.addSystemEventTrigger to shut down the foreign event loop after reactor shutdown. That approach seems untenable for me, for several reasons: 1. Too invasive. I dont want to pollute Qt code with Twisted details. 2. If my app is runing as a COM server then Pythoncom handles the main loop, not Qt. More pollution. 3. The reactor doesnt get shutdown if the foreign event loop shuts down for some other reason. This leads to deadlocks when the threading module tries to join the threads managed by the threadpool module, because the threadpool does know to close those threads. Im having moderate success with the following alternative structure which closes the reactor *after* whatever foreign event loop has exited, but it feels like I am relying on undocumented reactor implementation accidents here: from twisted.internet.threadedselectreactor import install as twisted_install reactor = twisted_install() reactor.interleave(trigger) try: my_event_loop_in_here() finally: # Request that the reactor stops itself. Internally the # reactor uses self.callLater, so shutdown is not complete # when this method returns. reactor.stop() # One call to reactor.iterate is sufficient to complete # All reactor shutdown events. reactor.iterate() # Reactor is now fully shutdown Is there a better approach? Should something like my finally block be included as a method of threadedselectreactor? Is it right for reactor.stop to return before the shutdown is complete? -- Toby Dickenson
On Jul 17, 2005, at 11:54 PM, Toby Dickenson wrote:
Im looking at moving my PyQt app to threadedselectreactor. Previously I was keeping twisted and Qt in seperate threads. Avoiding the need to cross that thread boundary has been a great simplification, but Im having some problems relating to the shutdown of my foreign event loop.
The examples such as http://svn.twistedmatrix.com/cvs/trunk/doc/core/examples/ threadedselect/pygamedemo.py?view=auto&rev=13596 handle this by: a. Calling reactor.stop() when there is a GUI shutdown request, b. Using reactor.addSystemEventTrigger to shut down the foreign event loop after reactor shutdown.
This is currently the only supported and recommended way.
That approach seems untenable for me, for several reasons: 1. Too invasive. I dont want to pollute Qt code with Twisted details. 2. If my app is runing as a COM server then Pythoncom handles the main loop, not Qt. More pollution. 3. The reactor doesnt get shutdown if the foreign event loop shuts down for some other reason. This leads to deadlocks when the threading module tries to join the threads managed by the threadpool module, because the threadpool does know to close those threads.
1. I guess that's too bad, then. It's less invasive than the alternatives (using an event-loop specific reactor, managing threads yourself). It's also really the only reliable and correct way to do it, see below. 2. Each different style of foreign event loop is going to need a different waker function and shutdown code. That's life. There is no happy magical cross-platform-toolkit-API to solve this problem. It's amazing that threadedselectreactor can do what it does so generically with just two minor integration hooks. In the same way your application can start up in two different modes, you're going to need different Twisted integration code. 3. The "pollution" is really quite minimal, you add a couple lines to defer shutdown of the foreign event loop until the reactor is has properly shut down. If the foreign event loop shuts down out of your control, you probably have other issues.
Im having moderate success with the following alternative structure which closes the reactor *after* whatever foreign event loop has exited, but it feels like I am relying on undocumented reactor implementation accidents here:
You are relying on exactly implementation accidents here. Even with the implementation accidents that make it appear to work, your proposed alternative has edge cases that will be rare and impossible to reproduce (yay threads).
from twisted.internet.threadedselectreactor import install as twisted_install reactor = twisted_install() reactor.interleave(trigger) try: my_event_loop_in_here() finally: # Request that the reactor stops itself. Internally the # reactor uses self.callLater, so shutdown is not complete # when this method returns. reactor.stop() # One call to reactor.iterate is sufficient to complete # All reactor shutdown events. reactor.iterate() # Reactor is now fully shutdown
Is there a better approach?
The problem is that the waker function (trigger in the above) can be called at any time -- and it's always called from another thread. In order to shut down the reactor properly, the waker function needs to continue to work for the duration of shutdown. reactor.stop() can take many runloop iterations to complete, as it can involve arbitrary deferreds and even new network I/O. If the foreign event loop has shut down already, it probably won't work, and you won't be able to do a proper shutdown. It's possible, I guess, use a proxy for the waker function that uses a thread lock so that you can reliably swap out the real waker at the right time. However, you must somehow acquire that lock before your foreign event loop has shut down (which is of the same level of invasiveness as the recommended solution!) so that it will block the proxy call until a new waker is installed. In the finally clause, you'd install a new trivial waker (the apply function would work) and release that lock so that any pending calls would complete. With any naive solution, it's possible for a message between the threadedselectreactor's worker thread and the main thread could get lost, and then you have a reactor that is in an undefined state. The waker function must work, or else the reactor won't. Note that your above code has another problem: it calls reactor.interleave too early. For the same reason you will have edge cases during shutdown, you can (in some circumstances) have edge cases during startup if the waker function does not work properly before you have entered the foreign event loop! Note that in all of the examples, reactor.interleave(...) is called while the foreign event loop has already begun (yay threads, again).
Should something like my finally block be included as a method of threadedselectreactor?
I doubt it, it's not really possible to make a correct one that's generic. At least, I can't think of any correct way. Correct patches accepted :)
Is it right for reactor.stop to return before the shutdown is complete?
Yes, always, even with the traditional reactors! The reactor can not nest itself, nor can multiple reactors be used in the same process, so it wouldn't be possible for reactor.stop() to work correctly if it blocked. -bob
(resend) On Monday 18 July 2005 11:43, Bob Ippolito wrote:
On Jul 17, 2005, at 11:54 PM, Toby Dickenson wrote:
a. Calling reactor.stop() when there is a GUI shutdown request, b. Using reactor.addSystemEventTrigger to shut down the foreign event loop after reactor shutdown.
This is currently the only supported and recommended way.
Thanks for that clear statement.
It's possible, I guess, use a proxy for the waker function that uses a thread lock so that you can reliably swap out the real waker at the right time. However, you must somehow acquire that lock before your foreign event loop has shut down (which is of the same level of invasiveness as the recommended solution!) so that it will block the proxy call until a new waker is installed. In the finally clause, you'd install a new trivial waker (the apply function would work) and release that lock so that any pending calls would complete.
Yes, that is much neater. My 'trigger' waker function is already queueing requests (it is used for more than just twisted integration) so I dont need to replace the waker - rather my finally clause needs to contain a new mini event loop that continues to processing my queue until the reactor is fully closed. As far as threadedselectreactor is concerned this looks no different to my normal event loop. Thanks again, -- Toby Dickenson
participants (2)
-
Bob Ippolito -
Toby Dickenson