Related
So basically I created a script called worker which contain 2 classes:
Worker
CreateThread
this script is supposed to handle unexpected exception occurs, while also provide threading solution to running heavy function to prevent GUI freezing.
My problem is that I get unexpected behavior sometimes, for example one time I can run function with the worker class and I will get a good response and everything works fine, and second time things go wrong and I get odd errors and also GUI crashes.
I tried to change the order I manage the thread handling, and I tried addepting the code to the errors but there is a hidden problem that keeps hunting me...
that is my worker code:
# Create a worker class #
class Worker(QObject):
my_started = pyqtSignal()
my_finished = pyqtSignal()
my_error = pyqtSignal(str, str)
my_success = pyqtSignal()
def __init__(self):
super(Worker, self).__init__()
def run(self, func):
try:
self.my_started.emit()
time.sleep(0.1)
x = func()
if x is None:
self.my_success.emit()
self.my_finished.emit()
return
if x[0] == -1:
function_error = x[1]
trace_back = x[2]
print(f'\nFunction error: {function_error}')
print(trace_back)
self.my_error.emit(str(function_error), trace_back,
str(func.__name__))
self.my_finished.emit()
return
except Exception as e:
self.trace_back = str(traceback.format_exc())
print(f'\nFunction error: {e}')
print(self.trace_back)
self.my_error.emit(str(e), self.trace_back)
self.my_finished.emit()
# Creating thread manger class #
class CreateThread(Worker, QObject):
def __init__(self, atp_name):
super(CreateThread, self).__init__()
self.stop_flag = False
self.atp_name = atp_name
self.thread = QThread()
self.worker = Worker()
def start_thread(self, func, **kwargs):
self.kwargs = kwargs
self.func_name = func.__name__
if self.stop_flag:
return
if self.func_name == '<lambda>':
self.func_name = self.kwargs['func_name']
# Thread and Worker init #
self.thread = QThread()
self.worker = Worker()
act = partial(self.worker.run, func)
self.worker.moveToThread(self.thread)
self.thread.started.connect(act)
# Signal to Function connection #
self.worker.my_started.connect(self.started_signal_connection)
self.worker.my_finished.connect(self.finished_signal_connection)
self.worker.my_error.connect(self.error_signal_connection)
self.worker.my_success.connect(self.success_signal_connection)
# Handling the worker and the thread deletion #
self.worker.my_finished.connect(self.thread.quit)
self.worker.my_finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.thread.start()
# Connect signals from worker #
def started_signal_connection(self):
self.stop_flag = True
print(f"\nFunction {self.func_name} started\n")
def finished_signal_connection(self):
self.stop_flag = False
print("\nWorker finished\n")
def error_signal_connection(self, error_name, trace_back):
self.today = date.today()
self.now = datetime.now()
self.current_date = str(
self.now.strftime("%d/%m/%Y %H:%M:%S")).replace(' ',
'_').replace(
'/', '_').replace(':', '_')
self.header_name = f'{self.atp_name}_{self.func_name}_Date_{self.current_date}'
self.target_folder = 'target_folder'
if os.path.isfile(self.header_name) == False:
file_name = f'{self.target_folder}{self.header_name}.txt'
try:
with open(file_name, 'w', encoding='utf-8') as f:
f.write(f'''ATP: {self.atp_name}\nFunction name: {self.func_name}\nError: {error_name}\nDate: {self.current_date}\n\n{trace_back}''')
except Exception as e:
print(e)
def success_signal_connection(self):
self.stop_flag = False
print("\nFunction success")
Another thing I noticed is that every time I use the worker with function that use Tk - askopenfilename()
I get troubles, here is a traceback example:
line 57, in run
**x = func()**
line 64, in open_main_terminal
**self.path1, self.file_name = upload_item()**
line 62, in upload_item
**filename = askopenfilename()**
line 375, in askopenfilename
**return Open(**options).show()**
line 39, in show
**w = Frame(self.master)**
line 2741, in __init__
**Widget.__init__(self, master, 'frame', cnf, {}, extra)**
line 2296, in __init__ (widgetName, self._w) + extra + self._options(cnf))
**RuntimeError: main thread is not in main loop**
I'm trying to extend this example on multithreading in PyQt5:
Multithreading PyQt applications with QThreadPool (MWE below)
to allow for two different threaded functions, one that uses a callback and the other that does not. In the example above, the progress_callback is hard-coded in the Worker() class __init__, which means that any threaded function must have a signature that accommodates that callback:
def execute_this_fn(self, progress_callback):
which means that if there is a second threaded process, that function's signature would also be required to accommodate the callback in its signature even though the callback is not used.
So instead of hard-coding progress_callback into the __init__ of Worker(), I'd like to pass in the callback when instantiating Worker().
My MWE is below -- I include two commented-out two lines from the original example for reference. When I run it, and press the "DANGER!" button on the GUI, I get:
$ python threadtest.py
Multithreading with maximum 16 threads
progress_callback = <bound PYQT_SIGNAL progress of WorkerSignals object at 0x113bbd160>
n = 0
n = 1
n = 2
n = 3
n = 4
Done.
THREAD COMPLETE!
So the code runs, but the callback function (slot) progress_fn is never called, and I'm not sure why...
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import time
import traceback, sys
class WorkerSignals(QObject):
''' Defines the signals available from a running worker thread.
Supported signals are:
finished
No data
error
`tuple` (exctype, value, traceback.format_exc() )
result
`object` data returned from processing, anything
progress
`int` indicating % progress
'''
finished = pyqtSignal()
error = pyqtSignal(tuple)
result = pyqtSignal(object)
progress = pyqtSignal(int)
class Worker(QRunnable):
''' Worker thread
Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
:param callback: The function callback to run on this worker thread. Supplied args and
kwargs will be passed through to the runner.
:type callback: function
:param args: Arguments to pass to the callback function
:param kwargs: Keywords to pass to the callback function
'''
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
# The old way (hard-code the callback to kwargs)
#self.kwargs['progress_callback'] = self.signals.progress
#pyqtSlot()
def run(self):
'''
Initialise the runner function with passed args, kwargs.
'''
# Retrieve args/kwargs here; and fire processing using them
try:
result = self.fn(*self.args, **self.kwargs)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() # Done
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.counter = 0
self.signals = WorkerSignals()
layout = QVBoxLayout()
self.l = QLabel("Start")
b = QPushButton("DANGER!")
b.pressed.connect(self.oh_no)
layout.addWidget(self.l)
layout.addWidget(b)
w = QWidget()
w.setLayout(layout)
self.setCentralWidget(w)
self.show()
self.threadpool = QThreadPool()
print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount())
self.timer = QTimer()
self.timer.setInterval(1000)
self.timer.timeout.connect(self.recurring_timer)
self.timer.start()
def progress_fn(self, n):
print("%d%% done" % n) # this does not execute...
def execute_this_fn(self, progress_callback):
print(f'progress_callback = {progress_callback}')
for n in range(0, 5):
time.sleep(1)
print(f'n = {n}')
progress_callback.emit(int(n*100/4))
return "Done."
def print_output(self, s):
print(s)
def thread_complete(self):
print("THREAD COMPLETE!")
def oh_no(self):
# Pass the function to execute
# the old way (callback hardcoded in __init__ of Worker class)
#worker = Worker(self.execute_this_fn)
# the desired new way
worker = Worker(self.execute_this_fn, progress_callback=self.signals.progress)
worker.signals.result.connect(self.print_output)
worker.signals.finished.connect(self.thread_complete)
worker.signals.progress.connect(self.progress_fn)
# Execute
self.threadpool.start(worker)
def recurring_timer(self):
self.counter +=1
self.l.setText("Counter: %d" % self.counter)
app = QApplication([])
window = MainWindow()
app.exec_()
The problem is simple: You have 2 WorkerSignals objects, and in one of them you make the connection and with the other you emit the signal. The solution is to use the same object for the connection and for the emission:
worker = Worker(self.execute_this_fn, progress_callback=self.signals.progress)
worker.signals.result.connect(self.print_output)
worker.signals.finished.connect(self.thread_complete)
self.signals.progress.connect(self.progress_fn)
Although I prefer to create a QObject that implements that logic:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import time
import traceback, sys
class WorkerSignals(QObject):
"""Defines the signals available from a running worker thread.
Supported signals are:
finished
No data
error
`tuple` (exctype, value, traceback.format_exc() )
result
`object` data returned from processing, anything
progress
`int` indicating % progress
"""
finished = pyqtSignal()
error = pyqtSignal(tuple)
result = pyqtSignal(object)
progress = pyqtSignal(int)
class Worker(QRunnable):
"""Worker thread
Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
:param callback: The function callback to run on this worker thread. Supplied args and
kwargs will be passed through to the runner.
:type callback: function
:param args: Arguments to pass to the callback function
:param kwargs: Keywords to pass to the callback function
"""
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
# The old way (hard-code the callback to kwargs)
# self.kwargs['progress_callback'] = self.signals.progress
#pyqtSlot()
def run(self):
"""
Initialise the runner function with passed args, kwargs.
"""
# Retrieve args/kwargs here; and fire processing using them
try:
result = self.fn(*self.args, **self.kwargs)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() # Done
class ProgressCallback(QObject):
progressChanged = pyqtSignal(int)
def __call__(self, value):
self.progressChanged.emit(value)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.counter = 0
layout = QVBoxLayout()
self.l = QLabel("Start")
b = QPushButton("DANGER!")
b.pressed.connect(self.oh_no)
layout.addWidget(self.l)
layout.addWidget(b)
w = QWidget()
w.setLayout(layout)
self.setCentralWidget(w)
self.show()
self.threadpool = QThreadPool()
print(
"Multithreading with maximum %d threads" % self.threadpool.maxThreadCount()
)
self.timer = QTimer()
self.timer.setInterval(1000)
self.timer.timeout.connect(self.recurring_timer)
self.timer.start()
def progress_fn(self, n):
print("%d%% done" % n) # this does not execute...
def execute_this_fn(self, progress_callback):
print(f"progress_callback = {progress_callback}")
for n in range(0, 5):
time.sleep(1)
print(f"n = {n}")
progress_callback(int(n * 100 / 4))
return "Done."
def print_output(self, s):
print(s)
def thread_complete(self):
print("THREAD COMPLETE!")
def oh_no(self):
callback = ProgressCallback()
worker = Worker(self.execute_this_fn, progress_callback=callback)
worker.signals.result.connect(self.print_output)
worker.signals.finished.connect(self.thread_complete)
callback.progressChanged.connect(self.progress_fn)
# Execute
self.threadpool.start(worker)
def recurring_timer(self):
self.counter += 1
self.l.setText("Counter: %d" % self.counter)
app = QApplication([])
window = MainWindow()
app.exec_()
I have a problem with an application on CentOs / RH to show download statuses from the API. Everything works fine using the PyCharm IDE, but after compiling with PyInstaller (one folder) the application is very unstable and I can't find an error. I run the thread and download API statuses every 10 seconds and if there is a change, I update the icon and send a notification. After left-clicking on the icon, the statuses are displayed in Gtk.ApplicationWindow.
Sometimes Gtk.StatusIcon can not change status or be inactive - left / right clicking doesn't work (but notifications of status changes come)
the application may end unexpectedly
I suspect the problem is in threads, but I can't find a proper solution.
Code (main.py)
class ExampleSystemTrayInit(Gtk.StatusIcon):
def __init__(self):
super().__init__()
app.tray = Gtk.StatusIcon()
app.tray.set_from_file(ico_start)
app.tray.connect('popup-menu', self.on_right_click)
app.tray.connect('activate', self.on_left_click, app)
def on_right_click(self, icon, event_button, event_time):
self.menu = Gtk.Menu()
quit = Gtk.MenuItem("Quit")
quit.connect('activate', self.quitApp)
self.menu.append(quit)
self.menu.show_all()
self.menu.popup(None, None, Gtk.StatusIcon.position_menu, app.tray, event_button, event_time)
def on_left_click(self, icon, app):
try:
self.app = app
if app.all_status:
data = self.app.all_status
Controller.show_window(self, data, self.app, refresh=False)
#call to class MainWindow(Gtk.ApplicationWindow) and show some data
else:
pass;
except Exception as e:
print(e)
app.tray.set_from_file(ico_disconnect)
def quitApp(self, par):
app.quit()
class ExampleSystemTray(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(
application_id="example-system-tray2.app"
)
self.tray = None
self.mainWindow = None
def do_activate(self):
if not hasattr(self, "my_app"):
self.hold()
self.my_app_settings = "Primary application instance."
self.systray = ExampleSystemTrayInit()
TrayController(app)
else:
print("Already running!")
def do_startup(self):
Gdk.threads_init()
Gdk.threads_enter()
Gtk.Application.do_startup(self)
Gdk.threads_leave()
if __name__ == "__main__":
GObject.threads_init()
app = ExampleSystemTray()
app.run()
TrayController (traycontroller.py)
class TrayController(threading.Thread):
def __init__(self, app):
self.app = app
self.cache = None
threading.Thread.__init__(self, name='TrayController', )
self.interval = 10
self.finished = threading.Event()
self.daemon = True
self.start()
def run(self):
while True:
try:
self.finished.wait(self.interval)
if not self.finished.is_set():
self.connect(self.app)
except Exception as e:
print(e)
def cancel(self):
"""Stop the timer if it hasn't finished yet."""
self.finished.set()
def connect(self, app):
try:
self.result = GetJson.get_json(self, api_view)
if self.result == None:
self.cache = None
self.show_status(2)
app.all_status = None
elif (self.cache != self.result):
self.cache = self.result
app.all_status = AllStatusInstance(self.result)
self.show_status(app.all_status.status)
Controller.show_main_window(self, app.all_staus, app, refresh=True)
#call to class MainWindow(Gtk.ApplicationWindow) if window is visble, then refresh
except Exception as e:
print(e)
self.show_status(2)
app.all_status = None
def show_status(self, status):
self.status = status
if self.status == 0:
return self.change_tray_icon(ico_red, notify_red, tag_red, widget=Gtk.StatusIcon)
elif self.status == 1:
return self.change_tray_icon(ico_gray, notify_gray, tag_gray, widget=Gtk.StatusIcon)
elif self.status == 2:
return self.change_tray_icon(ico_disconnect, notify_disconnect, tag_disconnect,
widget=Gtk.StatusIcon)
def change_tray_icon(self, icon, notification, tag, widget):
if self.app.tray.get_title() != tag:
self.app.tray.set_from_file(icon)
self.app.tray.set_title(tag)
self.notify("Notification:", notification, icon)
def notify(self, title, body, link):
notify2.init("Alert", mainloop=None)
icon = GdkPixbuf.Pixbuf.new_from_file(link)
n = notify2.Notification(title, body)
n.set_icon_from_pixbuf(icon)
n.set_urgency(notify2.URGENCY_CRITICAL)
n.show()
class AllStatusInstance(object):
__instance = None
def __new__(cls, val):
if AllStatusInstance.__instance is None:
AllStatusInstance.__instance = object.__new__(cls)
AllStatusInstance.__instance.val = val
return AllStatusInstance.__instance
Indeed, the problem is threads, in the sense that you cannot use GTK API from multiple threads, ever, as the documentation clearly states.
You should use Gio.Task if you have a blocking, synchronous operation and you wish to update some UI state at the end of it; or, if you're using a thread object in Python, always use GLib.MainContext.invoke_full() to invoke a function within the same thread that is running the GTK main loop.
What you should not do, is this:
def do_startup(self):
Gdk.threads_init()
Gdk.threads_enter()
Gtk.Application.do_startup(self)
Gdk.threads_leave()
and then call GTK API from a separate thread than the one that is running GTK's event loop; that is entirely undefined, non-portable behaviour.
Below I have an example program. When the button is pressed, it takes a second before it can calculate the value to show. If the user presses the button in rapid succession they end up waiting a long time to see the last answer, which is the only answer they care about. In the code, you can see that the _dataCruncher function needs to know self._count, but self._count does not depend on the output of _dataCruncher.
My question, therefore, is how can I interrupt the normal execution of _dataCruncher on subsequent calls in order to keep the GUI free to do other stuff, and to not waste processing time when it is not needed? I realize that I will likely need to use a thread to run _dataCruncher and some sort of Queue to get the appropriate val to display, but I do not understand how to put this all together.
from PyQt4 import QtGui, QtCore
import sys
import time
import random
import random
class MainWindow(QtGui.QMainWindow):
def __init__(self):
self.app = QtGui.QApplication(sys.argv)
super(MainWindow, self).__init__()
self.count = 0
self.initUI()
def initUI(self):
# Layouts
central = QtGui.QWidget()
layout = QtGui.QVBoxLayout()
self.button = QtGui.QPushButton('Press Me')
self.text = QtGui.QLabel('?')
layout.addWidget(self.button)
layout.addWidget(self.text)
central.setLayout(layout)
self.setCentralWidget(central)
self.button.clicked.connect(self._buttonClicked)
def _dataCruncher(self, val):
time.sleep(1) # takes a long time to process data using val
return val * random.randint(1,10)
def _buttonClicked(self):
self.count += 1
val = self._dataCruncher(self.count)
self.text.setText('Value {}'.format(val))
def startup(self):
self.show()
result = self.app.exec_()
sys.exit(result)
if __name__ == '__main__':
random.seed()
myWindow = MainWindow()
myWindow.startup()
So, finding an answer to this was more complicated than I thought. As #MTset mentions in one of the comments, python does not offer any means by which to cancel the execution of a Thread. So, what I did was create a 'threadHandler' class that, well, handles thread. It keeps track of the last thread that was created and offers a means by which to get the result from the execution of the last thread.
I am posting an modified version of the test code from the original post as well as the threadHandler code in full in case anyone has use for it.
File 1 here
# tester.py, run this file
from PyQt4 import QtGui, QtCore
import random, sys, time
from threadHandler import MyThreadHandler
class MyModel(object):
def dataCruncher(self, val):
delay = random.randint(1,5)
print('{} sleeping for {}'.format(val, delay))
time.sleep(delay) # takes a long time to process data using val
print('{} done sleeping'.format(val))
return val
class MainWindow(QtGui.QMainWindow):
def __init__(self, threadHandler):
self.app = QtGui.QApplication(sys.argv)
super(MainWindow, self).__init__()
self.count = 0
self.initUI()
self.button_clicked_events = Event()
self.threadHandler = threadHandler
def initUI(self):
# Layouts
central = QtGui.QWidget()
layout = QtGui.QVBoxLayout()
self.button = QtGui.QPushButton('Press Me')
self.text = QtGui.QLabel('?')
layout.addWidget(self.button)
layout.addWidget(self.text)
central.setLayout(layout)
self.setCentralWidget(central)
self.button.clicked.connect(self._buttonClicked)
def _buttonClicked(self):
self.count += 1
self.button_clicked_events(self.count)
def setLabel(self, val):
self.text.setText(str(val))
def startup(self):
self.show()
result = self.app.exec_()
return result
class Event(list):
"""Event subscription.
A list of callable objects. Calling an instance of this will cause a
call to each item in the list in ascending order by index.
Example Usage:
>>> def f(x):
... print 'f(%s)' % x
>>> def g(x):
... print 'g(%s)' % x
>>> e = Event()
>>> e()
>>> e.append(f)
>>> e(123)
f(123)
>>> e.remove(f)
>>> e()
>>> e += (f, g)
>>> e(10)
f(10)
g(10)
>>> del e[0]
>>> e(2)
g(2)
"""
def __init__(self):
self.output = {}
def __call__(self, *args, **kwargs):
for f,key in self:
output = f(*args, **kwargs)
self.output[key] = output
return self.output
def __repr__(self):
return "Event({})".format(list.__repr__(self))
if __name__ == '__main__':
def checker(handler, window):
if handler.isLastDone():
val = handler.getLastResult()
window.setLabel(val)
else:
window.setLabel('calculating...')
random.seed()
model = MyModel()
threadHandler = MyThreadHandler()
myWindow = MainWindow(threadHandler)
threadHandler.createTimer(1, checker, threadHandler, myWindow)
def getData(count):
threadHandler.createOneShot(model.dataCruncher, count)
myWindow.button_clicked_events.append((getData, 'dt'))
result = myWindow.startup()
print('ending')
threadHandler.end()
print('ended')
sys.exit(result)
File 2 below
#threadHandler.py, save this file in the same folder as tester.py
import threading, time
class MyThreadHandler(object):
def __init__(self):
self.oneShots = []
self.timers = []
self.oldOneShots = []
self.latest = None
self.cleaning = False
self._startCleaner()
def _startCleaner(self):
print('-'*20+'Starting cleaner'+'-'*20)
self.cleaner = self.createTimer(1, self._cleanupThreads)
def _stopCleaner(self):
print('-'*20+'Stopping cleaner'+'-'*20)
self.cleaner.stop()
def getNumThreads(self):
return len(self.oneShots)
def getNumOldThreads(self):
return len(self.oldOneShots)
def end(self):
for i,timer in enumerate(self.timers):
timer.stop()
self.timers.pop(i)
def createTimer(self, interval, func, *args, **kwargs):
timer = myTimer(interval, func, args, kwargs)
self.timers.append(timer)
return timer
def createOneShot(self, func, *args, **kwargs):
oneshot = myOneShot(func, args, kwargs)
self.oneShots.append(oneshot)
self.latest = oneshot
def isLastDone(self):
if not self.latest is None:
return not self.latest.running()
else:
return None
def getLastResult(self):
if self.latest is None:
raise ValueError('There have not been any oneshots created.')
while self.latest.running():
pass
result = self.latest.getResult()
if len(self.oneShots) > 0:
self.oldOneShots.append(myOneShot(self._cleanAll, (self.oneShots,)))
self.oneShots = []
return result
def _cleanAll(self, toClean):
# loop through toClean and pop up anything that's done. this DOES lock
while len(toClean) > 0:
toClean = self._cleanup(toClean)
def _cleanup(self, toCleanup):
while not self.cleaning:
self.cleaning = True
for i, thread in enumerate(toCleanup):
if not thread.running():
toCleanup.pop(i)
self.cleaning = False
return toCleanup
def _cleanupThreads(self):
# check each of these lists and pop out any threads that are done. This
# does not lock. This function should really only be called by the
# cleaner, which is set up in __init__
self.oneShots = self._cleanup(self.oneShots)
self.timers = self._cleanup(self.timers)
self.oldOneShots = self._cleanup(self.oldOneShots)
class myTimer(object):
def __init__(self, delay, func, args=tuple(), kwargs={}):
self.delay = delay
self.func = func
self.loop = True
self.args = args
self.kwargs = kwargs
self.thread = threading.Thread(target=self.run, daemon=True)
self.thread.start()
self.output = None
def run(self):
while self.loop:
self.output = self.func(*self.args, **self.kwargs)
if self.delay > 0.1:
count = 0
while count <= self.delay:
count += 0.1
time.sleep(0.1)
else:
time.sleep(self.delay)
def stop(self):
self.loop = False
def running(self):
return self.loop
def getResult(self):
return self.output
class myOneShot(object):
def __init__(self, func, args=tuple(), kwargs={}):
self.func = func
self.args = args
self.kwargs = kwargs
self.thread = threading.Thread(target=self.run, daemon=True)
self.thread.start()
self.output = None
def run(self):
self.output = self.func(*self.args, **self.kwargs)
def running(self):
return self.thread.is_alive()
def getResult(self):
return self.output
if __name__ == '__main__':
import random
random.seed()
def longFunc(num):
delay = random.randint(5,8)
if num in (3, 6):
delay = 2
print('-'*30+'func {} has sleep {}'.format(num, delay))
time.sleep(delay)
print('-'*30+'func {} is done'.format(num))
return num
def checker(handler):
if handler.isLastDone():
return handler.getLastResult()
else:
return None
myHandler = MyThreadHandler()
# The 'checker' function simulates something in my program that uses the
# data generated by the 'longFunc'. It waits until there are no more threads
# in the threadHandler, as that would indicate that the user is done
# switching back-and-forth between different values
checkTimer = myHandler.createTimer(1, checker, myHandler)
# create 10 one-shot threads that take a 'long' time. The delay is to keep
# them in order, as this loop is meant to simulate a user switching between
# items using a keyboard or mouse, which I imagine they couldn't do any
# faster than every 1/10th of a second
start = time.time()
for i in range(4):
myHandler.createOneShot(longFunc, i)
time.sleep(0.1)
# wait until there are no more threads executing
last = myHandler.getLastResult()
print('result from last = {}'.format(last))
for i in range(4, 7):
myHandler.createOneShot(longFunc, i)
time.sleep(0.1)
last = myHandler.getLastResult()
print('result from last = {}'.format(last))
while myHandler.getNumOldThreads() >0 or myHandler.getNumThreads() > 0:
pass
myHandler.end()
print('done ending')
You could disable the button after it's pressed until an answer is ready using:
setEnabled(False)
Then reset it just before providing the result.
I'm boggled over why a function called in a thread always returns the same value. I've confirmed that the parameters are different for each call. If I call the function after acquiring a lock then the function returns the correct value. This obviously defeats the purpose of using threads, because then this function is just called sequentially, one thread after another. Here is what I have. The function is called "get_related_properties" and I've made a note of it in the code:
class ThreadedGetMultipleRelatedProperties():
def __init__(self, property_values, **kwargs):
self.property_values = property_values
self.kwargs = kwargs
self.timeout = kwargs.get('timeout', 20)
self.lock = threading.RLock()
def get_result_dict(self):
queue = QueueWithTimeout()
result_dictionary = {}
num_threads = len(self.property_values)
threads = []
for i in range(num_threads):
t = GetMultipleRelatedPropertiesThread(queue,
result_dictionary,
self.lock)
t.setDaemon(True)
try:
threads.append(t)
t.start()
except:
return {"Error": "Unable to process results at this time." }
for property_value in self.property_values:
kwargs_copy = dict.copy(kwargs)
kwargs_copy['property_value'] = property_value
queue.put(self.kwargs_copy)
queue.join_with_timeout(self.timeout)
# cleanup threads
for i in range(num_threads):
queue.put(None)
for t in threads: t.join()
return result_dictionary
class GetMultipleRelatedPropertiesThread(threading.Thread):
def __init__(self, queue, result_dictionary, lock):
threading.Thread.__init__(self)
self.queue = queue
self.result_dictionary = result_dictionary
self.lock = lock
def run(self):
from mixpanel_helpers import get_related_properties
while True:
kwargs = self.queue.get()
if kwargs == None:
break
current_property_value = kwargs.get('property_value')
self.lock.acquire()
# The function call below always returns the same value if called before acquire
result = get_related_properties(**kwargs)
try:
self.result_dictionary[current_property_value] = result
finally:
self.lock.release()
#signals to queue job is done
self.queue.task_done()
Here is get_related_properties, although it makes other calls, so I'm not sure the problem lives in here:
def get_related_properties(property_name,
property_value,
related_properties,
properties={},
**kwargs):
kwargs['exclude_detailed_data'] = True
properties[property_name] = property_value
result = get_multiple_mixpanel_results(properties=properties,
filter_on_values=related_properties,
**kwargs)
result_dictionary = {}
for related_property in related_properties:
try:
# grab the last result here, because it'll more likely have the most up to date properties
current_result = result[related_property][0]['__results'][0]['label']
except Exception as e:
current_result = None
try:
related_property = int(related_property)
except:
pass
result_dictionary[related_property] = current_result
return result_dictionary
An additional note, I've also tried to copy the function using Python's copy module, both a deep and shallow copy and call the function copy, but neither of those worked.