Skip to content

Instantly share code, notes, and snippets.

@maartenbreddels
Created October 1, 2015 10:57
Show Gist options
  • Save maartenbreddels/9a31e4025ef9cc0ee68a to your computer and use it in GitHub Desktop.
Save maartenbreddels/9a31e4025ef9cc0ee68a to your computer and use it in GitHub Desktop.
Combining Qt and tornado, both which want to have their own event loop.
__author__ = 'breddels'
"""
Demonstrates combining Qt and tornado, both which want to have their own event loop.
The solution is to run tornado in a thread, the issue is that callbacks will then also be executed in this thread, and Qt doesn't like that.
To fix this, I show how to use execute the callback in the main thread, using a Qt signal/event in combination with Promises.
The output of the program is:
fetch page, we are in thread <_MainThread(MainThread, started 47200787479520)>
response is 191548 bytes, we are in thread <Thread(Thread-1, started daemon 47201018689280)>
the other thread should fulfil the result to this promise, we are in thread <Thread(Thread-1, started daemon 47201018689280)>
we received a promise, let us fulfill it, and are in thread <_MainThread(MainThread, started 47200787479520)>
let us set the background to black, we are in thread <_MainThread(MainThread, started 47200787479520)>
The magic happens in this line:
.then(self.move_to_gui_thread)
Without it, you'll see something like this:
fetch page, we are in thread <_MainThread(MainThread, started 47822588292064)>
response is 191604 bytes, we are in thread <Thread(Thread-1, started daemon 47822819497728)>
let us set the background to black, we are in thread <Thread(Thread-1, started daemon 47822819497728)>
QPixmap: It is not safe to use pixmaps outside the GUI thread
"""
from aplus import Promise # https://github.com/xogeny/aplus
import threading
import tornado
from tornado.httpclient import AsyncHTTPClient
from PyQt4 import QtGui
from PyQt4 import QtCore
import sys
# tornado works with futures, this wraps it in a promise
def wrap_future_with_promise(future):
promise = Promise()
def callback(future):
e = future.exception()
if e:
promise.reject(e)
else:
promise.fulfill(future.result())
future.add_done_callback(callback)
return promise
class Window(QtGui.QMainWindow):
signal_promise = QtCore.pyqtSignal(object, object)
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.button = QtGui.QPushButton("Async fetch using tornado", self)
self.button.resize(self.button.sizeHint())
self.button.clicked.connect(self.on_click)
self.signal_promise.connect(self.on_signal_promise)
def on_click(self, *args):
print "fetch page, we are in thread", threading.currentThread()
client = AsyncHTTPClient()
future = client.fetch("http://www.google.com/")
promise = wrap_future_with_promise(future)
# without .then(self.move_to_gui_thread), Qt will complain
promise.then(self.show_output)\
.then(self.move_to_gui_thread)\
.then(self.do_gui_stuff)\
.then(None, self.on_error)
def move_to_gui_thread(self, value):
promise = Promise()
print "the other thread should fulfil the result to this promise, we are in thread", threading.currentThread()
self.signal_promise.emit(promise, value)
return promise
def on_signal_promise(self, promise, value):
print "we received a promise, let us fulfill it, and are in thread", threading.currentThread()
promise.fulfill(value)
def on_error(self, error):
print "error", error
def show_output(self, response):
print "response is", len(response.body), "bytes, we are in thread", threading.currentThread()
def do_gui_stuff(self, response):
print "let us set the background to orange, we are in thread", threading.currentThread()
# this Qt call should only be done from the main thread
self.setStyleSheet("background-color: orange;")
# run the tornado loop in a seperate thread
thread = threading.Thread(target=lambda : tornado.ioloop.IOLoop.current().start())
thread.setDaemon(True)
thread.start()
app = QtGui.QApplication(sys.argv)
window = Window()
window.show()
app.exec_()
@maartenbreddels
Copy link
Author

Note that this solution:
https://gist.github.com/mivade/35cbd270f49e1ca7ba7d
doesn't demonstrate the issue, since setText doesn't trigger it, adding:
self.setStyleSheet("background-color: orange;")
to the update_label method does trigger the
"QPixmap: It is not safe to use pixmaps outside the GUI thread"
warning

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment