00001 #!/usr/bin/env python 00002 # 00003 # Copyright 2003 00004 # by 00005 # The Board of Trustees of the 00006 # Leland Stanford Junior University. 00007 # All rights reserved. 00008 # 00009 00010 __facility__ = "Online" 00011 __abstract__ = "Classes for launching Qt objects from non-GUI threads" 00012 __author__ = "S. Tuvi <stuvi@SLAC.Stanford.edu> SLAC - GLAST LAT I&T/Online" 00013 __date__ = "12/19/2003" 00014 __version__ = "$Revision: 2.8 $" 00015 __credits__ = "SLAC" 00016 00017 import LATTE.copyright_SLAC 00018 00019 import logging as log 00020 from qt import * 00021 from Queue import Queue 00022 import threading 00023 import sys 00024 00025 00026 class GUIbridge(object): 00027 """\brief Class for providing a bridge between the GUI thread and other threads. 00028 00029 This bridge is needed whenever a non-GUI thread needs to interact with the 00030 GUI. The GUI is handled by the 'GUI thread'. A 'GUI thread' is defined to 00031 be the one in which the Qt QApplication class's exec_loop() method is 00032 executed. There can be only one GUI thread in a program. 00033 00034 The solution to the problem is handled by sending a message (an 'event', in 00035 Qt terms) from the non-GUI thread to the GUI thread. The message contains 00036 information on how it is to be handled. In some cases, this handling produces 00037 a result that must be sent back to the initiating non-GUI thread. The 00038 requests and responses are passed around via queues. Separate queues are 00039 maintained for the different non-GUI threads so that responses don't end up 00040 handed off to the wrong thread. 00041 """ 00042 GUI_CMD_INST = QEvent.Type(QEvent.User + 1) 00043 GUI_CMD_FUNC = QEvent.Type(QEvent.User + 2) 00044 GUI_CMD_FUNC_NR = QEvent.Type(QEvent.User + 3) 00045 00046 def __init__(self, guiThread): 00047 """\brief This is the constructor of the GUIbridge class. 00048 00049 It must be called from the GUI thread. 00050 """ 00051 self.__guiThread = guiThread 00052 self.__cmdQueue = Queue() 00053 self.__respQueues = {} 00054 self.__qtBridge = QtThreadBridge("QtBridge", self.__cmdQueue) 00055 self.__qtBridge.start() 00056 self.__pyBridge = PyThreadBridge("PyBridge", self.__cmdQueue) 00057 00058 def shutdown(self): 00059 """\brief This method is used to shut the GUI bridge down. 00060 """ 00061 self.__qtBridge.quit() 00062 00063 def isGUIthread(self): 00064 """\brief Returns True if this method is being called from the GUI thread. 00065 """ 00066 return threading.currentThread() == self.__guiThread 00067 00068 def createGUI(self, qObject, obj, *args, **kwargs): 00069 """\brief Method used for instantiating a GUI from a non-GUI thread. 00070 00071 \param qObject - The Qt object that will receive the customEvent callback 00072 \param obj - The GUI object class type to create 00073 \param args - Arguments for the object class constructor 00074 \param kwargs - Keyword arguments for the object class constructor 00075 \return An instance of the requested GUI object class 00076 """ 00077 # Don't use the GUI bridge if we're being called from the GUI thread 00078 if self.isGUIthread(): return apply(obj, args, kwargs) 00079 00080 currentThread = threading.currentThread() 00081 if currentThread not in self.__respQueues: 00082 self.__respQueues[currentThread] = Queue() 00083 00084 respQ = self.__respQueues[currentThread] 00085 self.__pyBridge.pushEvent((GUIbridge.GUI_CMD_INST, qObject, 00086 (obj,) + args + (kwargs, respQ))) 00087 return self.__pyBridge.pullResponse(respQ) 00088 00089 def execGUImethodNR(self, qObject, func, *args, **kwargs): 00090 """\brief Method used for executing a GUI function from a non-GUI thread. 00091 00092 Use this method when no (useful) response is expected or 00093 when waiting for one could cause a deadlock. Any response from the 00094 function is lost. 00095 \param qObject - The Qt object that will receive the customEvent callback 00096 \param func - The GUI function or method to call 00097 \param args - Arguments for the function or method 00098 \param kwargs - Keyword arguments for the function or method 00099 """ 00100 if not self.isGUIthread(): 00101 respQ = None # No response expected or required 00102 self.__pyBridge.pushEvent((GUIbridge.GUI_CMD_FUNC_NR, qObject, 00103 (func,) + args + (kwargs, respQ))) 00104 else: 00105 apply(func, args, kwargs) 00106 00107 def execGUImethod(self, qObject, func, *args, **kwargs): 00108 """\brief Method used for executing a GUI function from a non-GUI thread. 00109 00110 Use this method when a response is expected from the function and 00111 when it is appropriate to wait for it (no deadlock arises) 00112 \param qObject - The Qt object that will receive the customEvent callback 00113 \param func - The GUI function or method to call 00114 \param args - Arguments for the function or method 00115 \param kwargs - Keyword arguments for the function or method 00116 \return The called GUI function's return value 00117 """ 00118 # Don't use the GUI bridge if we're being called from the GUI thread 00119 if self.isGUIthread(): return apply(func, args, kwargs) 00120 00121 currentThread = threading.currentThread() 00122 if currentThread not in self.__respQueues: 00123 self.__respQueues[currentThread] = Queue() 00124 00125 respQ = self.__respQueues[currentThread] 00126 self.__pyBridge.pushEvent((GUIbridge.GUI_CMD_FUNC, qObject, 00127 (func,) + args + (kwargs, respQ))) 00128 return self.__pyBridge.pullResponse(respQ) 00129 00130 if __name__ == "__main__": 00131 def execGUImethod2(self, qObject, func, *args, **kwargs): 00132 # Don't use the GUI bridge if we're being called from the GUI thread 00133 if self.isGUIthread(): return apply(func, args, kwargs) 00134 00135 currentThread = threading.currentThread() 00136 if currentThread not in self.__respQueues: 00137 if '_GUIbridge__respQueue' not in self.__dict__: 00138 self.__respQueue = Queue() 00139 self.__respQueues[currentThread] = self.__respQueue #Queue() 00140 00141 respQ = self.__respQueues[currentThread] 00142 self.__pyBridge.pushEvent((GUIbridge.GUI_CMD_FUNC, qObject, 00143 (func,) + args + (kwargs, respQ))) 00144 #time.sleep(0.01) # Delay to allow a task switch 00145 return self.__pyBridge.pullResponse(respQ) 00146 00147 def handleCustomEvent(self, e): 00148 try: 00149 # This method overrides the QObject base class's 00150 data = e.data() 00151 func = data[0] 00152 args = data[1:-3] 00153 kwargs = data[-3] 00154 respQ = data[-2] 00155 qtBridge = data[-1] 00156 if e.type() == GUIbridge.GUI_CMD_INST: 00157 qtBridge.pushResponse(respQ, func(*args, **kwargs)) 00158 elif e.type() == GUIbridge.GUI_CMD_FUNC: 00159 qtBridge.pushResponse(respQ, func(*args, **kwargs)) 00160 elif e.type() == GUIbridge.GUI_CMD_FUNC_NR: 00161 func(*args, **kwargs) 00162 except Exception, exc: 00163 log.exception("GUIbridge.handleCustomEvent(): Caught exception") 00164 if e.type() == GUIbridge.GUI_CMD_INST or \ 00165 e.type() == GUIbridge.GUI_CMD_FUNC: 00166 qtBridge.pushResponse(respQ, None, exc) 00167 00168 00169 class QtThreadBridge(QThread): 00170 def __init__(self, name, cmdQueue): 00171 QThread.__init__(self) 00172 self._cmdQueue = cmdQueue 00173 self.__quit = False 00174 00175 def quit(self): 00176 self.__quit = True 00177 self._cmdQueue.put( (None) ) 00178 00179 def pushResponse(self, respQueue, event, exception=None): 00180 respQueue.put((event, exception)) 00181 00182 def run(self): 00183 while not self.__quit: 00184 try: 00185 tplEvent = self._cmdQueue.get(True) 00186 if tplEvent is None: break # Quit signal is None 00187 tplEventType = tplEvent[0] 00188 tplEventDest = tplEvent[1] 00189 tplEventData = tplEvent[2] 00190 evt = QCustomEvent(tplEventType) 00191 evt.setData(tplEventData + (self,)) 00192 QApplication.postEvent(tplEventDest, evt) 00193 self.msleep(1) # RiC: Why's this here? 00194 except Exception, e: 00195 log.exception(e) 00196 00197 # Drop references to this object to allow it to be garbage collected 00198 tplEvent = None 00199 tplEventType = None 00200 tplEventDest = None 00201 tplEventData = None 00202 00203 # logging already shut down? 00204 #log.info(self.__class__.__name__ + ": terminating") 00205 00206 00207 class PyThreadBridge(object): 00208 def __init__(self,name, cmdQueue): 00209 self._cmdQueue = cmdQueue 00210 00211 def pushEvent(self, event): 00212 try: 00213 self._cmdQueue.put(event) 00214 except Exception, e: 00215 log.exception(e) 00216 00217 def pullResponse(self, respQueue, blnBlock = 1): 00218 try: 00219 response, exception = respQueue.get(blnBlock) 00220 if exception is None: return response 00221 raise exception 00222 except Exception, e: 00223 log.exception(e) 00224 # raise e 00225 00226 00227 if __name__ == "__main__": 00228 import time 00229 00230 class GUIbridgeTest(threading.Thread): 00231 def __init__(self, qObject, guiBridge, id, label): 00232 threading.Thread.__init__(self) 00233 00234 self.__qObject = qObject 00235 self.__guiBridge = guiBridge 00236 self.__id = id 00237 self.__label = label 00238 self.__quit = False 00239 00240 def shutdown(self): 00241 self.__quit = True 00242 00243 def run(self): 00244 qo = self.__qObject 00245 gb = self.__guiBridge 00246 tf = self.__label 00247 id = self.__id 00248 count = 0 00249 start = time.time() 00250 while not self.__quit: 00251 string = "Thread %d: %12d" % (id, count) 00252 00253 # Write value 00254 gb.execGUImethodNR(qo, tf.setText, string) 00255 00256 # Old method: 00257 #v = gb.execGUImethod2(qo, tf.text) 00258 # New method: 00259 v = gb.execGUImethod(qo, tf.text) 00260 00261 # Bypass GUI thread: 00262 #v = tf.text("Thread %d: %d" % (id, count)) # This doesn't reliably work 00263 00264 # Compare result 00265 if str(v) != string: print "Thread %d: '%s' != '%s'" % (id, string, v) 00266 00267 #time.sleep(.001) 00268 if id == 1: 00269 count += 1 00270 time.sleep(0.123) # To get some beating betwee the two tasks 00271 else: count -= 1 00272 end = time.time() 00273 if id == 2: count = -count 00274 rate = count / (end - start) 00275 print "Exiting thread %d, %d calls = %.1f setText()s per second" % \ 00276 (self.__id, count, rate) 00277 00278 00279 class TestWindow(QMainWindow): 00280 def __init__(self, parent = None, name = None, fl = 0): 00281 QMainWindow.__init__(self, parent, name, fl) 00282 00283 self.setName("TestWindow") 00284 00285 self.mainWidget = QWidget(self) # dummy widget to containt the layout mgr 00286 self.setCentralWidget(self.mainWidget) 00287 layout = QVBoxLayout(self.centralWidget(), 11, 6, "Test_Window_Layout") 00288 00289 self.__label1 = QLabel("Thread 1:", self.mainWidget) 00290 self.__label2 = QLabel("Thread 2:", self.mainWidget) 00291 self.__quit = QPushButton("Quit", self.mainWidget, "quit") 00292 self.connect(self.__quit, SIGNAL("clicked()"), self.quit) 00293 00294 layout.addWidget(self.__label1) 00295 layout.addWidget(self.__label2) 00296 layout.addWidget(self.__quit) 00297 00298 self.__guiBridge = GUIbridge(threading.currentThread()) 00299 00300 self.__gbt1 = GUIbridgeTest(self, self.__guiBridge, 1, self.__label1) 00301 self.__gbt2 = GUIbridgeTest(self, self.__guiBridge, 2, self.__label2) 00302 00303 self.__gbt1.start() 00304 self.__gbt2.start() 00305 00306 def customEvent(self, e): 00307 self.__guiBridge.handleCustomEvent(e) 00308 00309 def quit(self): 00310 self.__gbt1.shutdown() 00311 self.__gbt2.shutdown() 00312 time.sleep(0.1) # Seems to be required to yield and allow things to exit 00313 self.__guiBridge.shutdown() 00314 self.close() 00315 00316 def startTest(args): 00317 app = QApplication(args) 00318 app.connect(app, SIGNAL("lastWindowClosed()"), app, SLOT("quit()")) 00319 00320 win = TestWindow() 00321 app.setMainWidget(win) 00322 win.show() 00323 00324 app.exec_loop() 00325 00326 00327 # Start the test 00328 startTest(sys.argv)