tkinter gui stops responding while 2 other threads open - python

from pythoncom import PumpWaitingMessages
import pyHook, threading
import tkinter as tk
threadsRun = 1
token = 0
def pas():
while threadsRun:
pass
def listen(startButton):
"""Listens for keystrokes"""
def OnKeyboardEvent(event):
"""A key was pressed"""
global threadsRun
if event.Key == "R":
startButton.config(relief=tk.RAISED, state=tk.NORMAL, text="Start")
threadsRun = 0
return True
hm = pyHook.HookManager()
hm.KeyDown = OnKeyboardEvent
hm.HookKeyboard()
while threadsRun:
PumpWaitingMessages()
else:
hm.UnhookKeyboard()
def simplegui():
def threadmaker():
"""Starts threads running listen() and pas()"""
startButton.config(relief=tk.SUNKEN, state=tk.DISABLED, text="r=stop listening")
global token, threadsRun
threadsRun = 1
token += 1
t1 = threading.Thread(target=pas, name='pas{}'.format(token))
t2 = threading.Thread(target=listen, args=(startButton,), name='listen{}'.format(token))
t1.start()
t2.start()
def destroy():
"""exit program"""
global threadsRun
threadsRun = 0
root.destroy()
startButton = tk.Button(root, text="Start", command=threadmaker, height=10, width=20)
startButton.grid(row=1, column=0)
quitButton = tk.Button(root, text="Quit", command=destroy, height=10, width=20)
quitButton.grid(row=1, column=1)
root = tk.Tk()
simplegui()
root.mainloop()
Code description:
simplegui() creates two threads to run
pas() and
listen()
simultaneously.
listen() waits for keyboard presses(only r does anything: exits both threads/functions).
pas() does nothing but is needed to reproduce bug.
Problem description:
After clicking start, pressing any button on the keyboard can cause tkinter to stop responding.
~2/3rd of the time r will behave as intended.
I'm using Spyder IDE (python 3.5).
Some observations:
Using print statements, the program will go into while threadsRun loop, in listen(), before the crash, but didn't reach OnKeyboardEvent() print statement.
Can wait a long time before pressing a key and it may freeze.
Can press a key instantly after pressing start and it may function as intended.
Removing t1 = ... and t1.start() lines allows program to run bug free.
Alternatively, removing all tkinter code allows program to run bug free.
Mashing a bunch of keys all at once freezes it.
If I place a print statement inside the while threadsRun loop, r will rarely work.
I've read in other posts, tkinter is not thread safe, and to use a queue. But I don't understand how. I also think maybe something else is wrong because it works sometimes. https://www.reddit.com/r/learnpython/comments/1v5v3r/tkinter_uis_toplevel_freezes_on_windows_machine/
Thanks very much for reading.

One attempt I managed to use for threads and queues is the following (replaced used code with multiple pseudo-code entries)
The Class works as a session watchdog and uses sql commands gather logged in users, then uses threads for their location detection (geoip)
class SessionWatchdog
import Tkinter as tk
import ttk
import Queue
import Locator
class SessionWatchdog(ttk.Frame):
"""
Class to monitor active Sessions including Location in a threaded environment
"""
__queue = None
__sql = None
def __init__(self, *args, **kwargs):
#...
# Create the Queue
self.__queue = Queue.Queue()
def inqueue(self):
""" Handle Input from watchdog worker Thread """
if self.__queue.empty():
return
while self.__queue.qsize():
"""
Use
try:
self.__queue.get()
finally:
self.__queue.task_done()
to retrieve data from the queue
"""
pass
def gather_data(self, queue):
"""
Retrieve online users and locate them
"""
if self.__sql:
threads = []
# gather data via sql
# ....
# ....
for data in sql_result:
thread = Locator(queue, data)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
Locator to fill the queue:
class Locator
import threading
import Queue
class Locator(threading.Thread):
"""
docstring
"""
__base_url = "http://freegeoip.net/json/{}"
def __init__(self, queue, user_information):
threading.Thread.__init__(self)
self.queue = queue
self.user_information = user_information
def run(self):
""" add location information to data (self.user_information)
to improve performance, we put the localization in single threads.
"""
located_user = []
# locate the user in a function, NOT method!
self.queue.put(located_user, False)

Related

Thread hangs when trying to write to tkinter IntVar when user closes the window

I'm using threading to run a long task, but I ran into an issue. The thread just hung while trying to set an IntVar after I clicked the close button. It doesn't even error. I don't want to use a daemon thread because the function is a critical part of the program, which might have consequences if it stops midway through (it deals with a bunch of files).
Here's an oversimplified version of my program, meant to demonstrate my issue.
import tkinter as tk
import tkinter.ttk as ttk
import threading
class Window(tk.Tk):
def __init__(this, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
this.threads = []
this.var = tk.IntVar(value=0)
this.label = ttk.Label(textvariable=this.var)
this.button = ttk.Button(text='Start counter', command=this.startCounter)
this.label.pack()
this.button.pack()
this.stop = False
this.protocol("WM_DELETE_WINDOW", this.close)
def startCounter(this):
thread = threading.Thread(target=this.counter)
this.threads.append(thread)
thread.start()
def counter(this):
while True:
if this.stop:
print(f'self.stop = ')
break
this.var.set(this.var.get() + 1)
def close(this):
print('Stopping threads')
this.stop = True
this.waitThreads()
print('Stopped threads')
this.destroy()
def waitThreads(this):
for thread in this.threads:
thread.join()
Window().mainloop()
My program is using an InVar for a progress bar, not a counter, this was just the best way I could demonstrate the issue.
I tried a bunch of different methods to stop all threads, but none of them worked (that was before I knew what the issue was). For some reason in my actual program, if I log the var and the value of the var before the stop check, it's able to stop. I could not reproduce that with my test script.
I'm expecting the set var line to move on, or error instead of just hang.
Why is it hanging, and how can I fix this? I want to be able to safely stop the thread(s), and I don't want to use a daemon.
you have a race condition, a deadlock, and an undefined behavior in your application ... that's how simple it is to mess up a small code snippet when multithreading.
the tk interpreter isn't threadsafe, and shouldn't be called from different threads, notably the event_generate function is threadsafe and should be used for instructing GUI changes, incrementing the variable from another thread is likely going to crash the interpreter, it's also a race condition, and the results will be wrong, increments should only happen in the main thread, by generating an event from the other thread.
lastly you need to make your threads drop the GIL momentarily, this can be done by a small sleep time.sleep(0.0000001).
import tkinter as tk
import tkinter.ttk as ttk
import threading
import time
class Window(tk.Tk):
def __init__(this, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
this.threads = []
this.var = tk.IntVar(value=0)
this.label = ttk.Label(textvariable=this.var)
this.button = ttk.Button(text='Start counter', command=this.startCounter)
this.bind("<<increment_counter>>",this.increment_var)
this.label.pack()
this.button.pack()
this.stop = False
this.protocol("WM_DELETE_WINDOW", this.close)
def startCounter(this):
thread = threading.Thread(target=this.counter)
this.threads.append(thread)
thread.start()
def increment_var(this, event):
this.var.set(this.var.get() + 1)
def counter(this):
while True:
time.sleep(0.0000001) # drop the GIL momentarily
if this.stop:
print(f'self.stop = ')
break
this.event_generate("<<increment_counter>>") # all increments happen in main thread
def close(this):
print('Stopping threads')
this.stop = True
this.waitThreads()
print('Stopped threads')
this.destroy()
def waitThreads(this):
for thread in this.threads:
thread.join()
Window().mainloop()
note that the first argument of a method is by convention called self in python, calling it this will confuse a lot of linters, parsers, other coders, autocorrect IDEs and documentation generators. please don't do that to everyone and use self instead of this.
I have a few suggestions to make your code work safer.
Use a costume defined event. This will place an event in the event queue of tkinters event-loop.
Have a thread limit, too many threads might mess things up an become unreachable.
Use a threading primitive to signal the termination of your threads, like threading.Event()
Instead of joining the threads within the event-loop of tkinter, make sure they are done after your app has been terminated. This will lead to a better user-experience.
In addition I would propose:
Using self instead of this because of convention
Using a set() instead of a list()
import tkinter as tk
import tkinter.ttk as ttk
import threading
import time
class Window(tk.Tk):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.threads = set()
self.var = tk.IntVar(value=0)
self.label = ttk.Label(self, textvariable=self.var)
self.button = ttk.Button(
self, text='Start counter', command=self.startCounter)
self.label.pack()
self.button.pack()
self.stop = threading.Event()
self.event_generate('<<UpdateLabel>>')
self.bind('<<UpdateLabel>>', self.update_counter)
self.protocol("WM_DELETE_WINDOW", self.close)
def update_counter(self, event):
self.var.set(self.var.get() + 1)
def startCounter(self):
if threading.active_count()+1 <= THREAD_LIMIT:
t = threading.Thread(target=self.counter)
self.threads.add(t)
t.start()
else:
print('too many threads might messing things up')
def counter(self):
current = threading.current_thread()
while not self.stop.is_set():
time.sleep(1) #simulate task
if not self.stop.is_set():
#if application still exists
self.event_generate('<<UpdateLabel>>')
else:
self.threads.discard(current)
break
self.threads.discard(current)
def close(self):
print('Stopping threads')
self.stop.set()
print('Stop event is set')
self.destroy()
if __name__ == '__main__':
COUNT = threading.active_count()
THREAD_LIMIT = COUNT + 7
window = Window()
window.mainloop()
print('mainloop ended')
while threading.active_count() > COUNT: #simulate join
time.sleep(0.1)
print(threading.active_count())
print('all threads ended, mainthread ends now')

Threading 2 functions

I'm trying to read a very slow sensor (1-wire) while still operating other functions. I've replaced the read sensors with a sleep of 3 sec. I'd like to understand if I can have below time concurrently print accurate time every second while the sensor (wait 3sec.) occurs. This threading concept is new to me.
import time
import threading
from tkinter import Tk
def events_every_second(): #update clock every second
right_now = time.strftime("%I:%M:%S %p")#("%H:%M:%S")
print("time is now",right_now)
root.after(1000, events_every_second)
def one_wire():
time.sleep(3)
print("one_wire loop")
root.after(3010, one_wire)
root = Tk()
t_one_wire = one_wire()
thread_one_wire = threading.Thread(target = t_one_wire)
thread_one_wire.start()
t_ees = events_every_second
thread_ees = threading.Thread(target = t_ees)
thread_ees.start()
root.mainloop()
Just the function that does the lenghty sensor read needs to be
in a separate thread.
You can use a queue.Queue to obtain data from it -
in the example bellow I inserted the value fetching in the
every_one_sec function.
The every_one_sec function is handled by Tkinter scheduling events -
no need to creat a thread for it.
Other than that, the most incorrect part in your code was doing a
full call to one_wire before creating the thread in
t_one_wire = one_wire() - and the fact that it would run also require
tkinter to call it back. Tkinter wants to run all its events in the same
thread - so this would lead to problems.
I hope the comments bellow are enough for a better comprehension
import time
import threading
from queue import Queue, Empty
from tkinter import Tk
def some_ui_code():
global stop_reading
stop_reading = True
def events_every_second(): #update clock every second
right_now = time.strftime("%I:%M:%S %p")#("%H:%M:%S")
print("time is now",right_now)
try:
result = queue.get(block=False)
except Empty:
result = None
if result is not None:
# code to display the result read from the sensor in tkinter interface goes here
...
# tkinter, not a separate thread, handles this
root.after(1000, events_every_second)
def one_wire(queue):
# this is handled in a separate thread, and "does not know" about tkinter at all
while not stop_reading:
result = call_to_read_sensor() # takes 3 seconds
queue.put(result)
print("one_wire loop")
time.sleep(0.1) # actual call to time.sleep to space sensor readings, if needed.
root = Tk()
stop_reading = False
queue = Queue()
# start events_every_second - tkinter will handle the subsequent calls with the `root.after` scheduling
events_every_second()
thread_one_wire = threading.Thread(target=t_one_wire, args=(queue,))
thread_one_wire.start()
root.mainloop()
You should not use root.after(3010,one_wire) this will cause your Tk GUI to hang for 3 seconds and don't update.
You want to create a new Thread for this function so it does not stop your Tk app.
def one_wire():
time.sleep(3)
print("one_wire_loop")
create a new thread for this function.
import threading
one_wire_thread = threading.Thread(target = one_wire, args = ())
one_wire_thread.start()
Note that the code above will only run your function once. you could create a wrapper function with a while True: in there to keep it running, in the wrapper function you could also check for condition to stop and break the function and thus stop the one_wire function. you could also do this in the one_wire function:
def one_wire():
while True:
time.sleep(3)
print("one_wire_loop")
if {"condition to stop reading sensor"}:
break

TkInter disable buttons while thread is running

I have an app that tries to generate text when a button is pressed. Most of the time the text generation is fast but there is one function that takes about 20 seconds to execute (depending on the amount of text). During that process the GUI used to freeze so I transferred that function on a separate thread and everything is fine on that field.
Now I have an issue with the buttons. When the function that takes some time to execute is running, the user can still click the button and that function will be executed several times while the first call is still being processed.
I would like to prevent that by disabling all the buttons while that function is running but I can't get the threading to work properly.
Here is the code that I have:
def generate_text():
choice = dropdown_choice.get()
if context_obj.context_text.get() == '':
if choice == 'OpenAI':
context = 'Some random context text'
else:
context = ' '
else:
context = context_obj.context_text.get()
if choice == 'OpenAI':
progress.start(50)
progress_bar_text = Label(text='Please wait while the text is being generated',
background='#E5F2FF',
font=("Helvetica", 12))
progress_bar_text.place(relx=.2,
rely=.66,
anchor="c")
# multithreading for the OpenAI text generation
q = queue.Queue()
thread1 = Thread(target=openAI_generator.sample, args=[text_amount.get(), temperature.get(), context, q])
thread1.start()
def display_AI_text(q):
openAI_text = q.get()
generated_text.configure(state='normal')
generated_text.delete(1.0,END)
generated_text.insert(tk.END, openAI_text)
generated_text.configure(state='disabled')
progress.stop()
progress_bar_text.place_forget()
thread2 = Thread(target=display_AI_text, args=[q])
thread2.start()
In this code, thread1 is executing the function and the thread2 is taking the input from that function and displaying it.
What I would like to do is, while thread2 is being executed, all the buttons to be disabled and when the thread finishes, the buttons to become enabled again.
I have tried adding:
thread2 = Thread(target=display_AI_text, args=[q])
generate_button.config(state="disabled")
thread2.start()
and then:
thread2.join()
generate_button.config(state="normal")
but this code freezes the app. I assume that the main thread is waiting for thread2 to finish and that is why it is not responding.
Does anyone know a way to overcome this issue?
In many GUIs you can't change GUI in thread - you have to do it in main process.
You can use queue to send information to main process which will update GUI.
In Tkinter you can use
root.after(time_in_milliseconds, function_name)
to run periodically function which can check message from this queue.
Or it can periodically check
thread2.is_alive()
instead of using thread2.join() because is_alive() doesn't block code.
import tkinter as tk
from threading import Thread
import time
def long_running_function():
print('start sleep')
time.sleep(3)
print('end sleep')
def start_thread():
global t
global counter
b['state'] = 'disable'
counter = 0
t = Thread(target=long_running_function)
t.start()
check_thread()
# or check after 100ms
# root.after(100, check_thread)
def check_thread():
global counter
if not t.is_alive():
b['state'] = 'normal'
l['text'] = ''
else:
l['text'] = str(counter)
counter += 0.1
# check again after 100ms
root.after(100, check_thread)
#-----------------------------------------------------
# counter displayed when thread is running
counter = 0
root = tk.Tk()
l = tk.Label(root)
l.pack()
b = tk.Button(root, text="Start", command=start_thread)
b.pack()
root.mainloop()

GUI not responding after child processes finishes due to redirection of Stdout

I've spent the past few days reading various threads about making tkinter thread-safe and running children without blocking the main thread. I thought I had arrived at a solution that allowed my code to run as I wanted it to, but now my main thread becomes non-responsive when my child process finishes. I can move the window around but the GUI part shows a loading cursor, whites out, and says "Not Responding" in the title of the window. I can let it sit like that forever and nothing will happen. I know what part of the code is causing the problem but I am not sure why it's causing the GUI to freeze. I'm using Windows.
I want my GUI to run another process using multiprocess. I have sys.stdout and sys.stderr routed to a queue and I use threading to create a thread that holds an automatic queue checker that updates the GUI every 100 ms so my GUI updates in "real time". I have tried every way of sending the child's stdout/stderr to the GUI and this is the only way that works the way I want it to (except for the freezing bit), so I would like to find out why it's freezing. Or I would like help setting up a proper way of sending the child's output to the GUI. I have tried every method I could find and I could not get them to work.
My main thread:
#### _______________IMPORT MODULES_________________###
import Tkinter
import multiprocessing
import sys
from threading import Thread
import qBMPchugger
###____________Widgets__________________###
class InputBox(Tkinter.Tk):
def __init__(self,parent):
Tkinter.Tk.__init__(self, parent)
self.parent = parent
self.initialize()
def initialize(self):
# Styles
self.grid()
# Approval
self.OKbutton = Tkinter.Button(self, text=u"OK", command=self.OKgo, anchor="e")
self.OKbutton.pack(side="right")
self.view = Tkinter.Text(self)
self.view.pack(side="left")
self.scroll = Tkinter.Scrollbar(self, orient=Tkinter.VERTICAL)
self.scroll.config(command=self.view.yview)
self.view.config(yscrollcommand=self.scroll.set)
self.scroll.pack(side="left")
def write(self, text):
self.view.insert("end", text)
def OKgo(self):
sys.stdout = self
sys.stderr = self
checker = Thread(target=self._update)
checker.daemon = True
checker.start()
self.view.delete(1.0, "end")
self.update_idletasks()
print("Loading user-specified inputs...")
path = "C:/"
inarg = (q, path)
print("Creating the program environment and importing modules...")
# Starts the text monitor to read output from the child process, BMPchugger
p = multiprocessing.Process(target=qBMPchugger.BMPcode, args=inarg)
p.daemon = 1
p.start()
def _update(self):
msg = q.get()
self.write(msg)
self.update_idletasks()
self.after(100, self._update)
if __name__ == "__main__":
app = InputBox(None)
app.title("File Inputs and Program Settings")
q = multiprocessing.Queue()
app.mainloop()
My child process (qBMPchugger):
#### _______________INITIALIZE_________________###
import os
import sys
import tkMessageBox
import Tkinter
class BadInput(Exception):
pass
def BMPcode(q, path):
# Create root for message boxes
boxRoot = Tkinter.Tk()
boxRoot.withdraw()
# Send outputs to the queue
class output:
def __init__(self, name, queue):
self.name = name
self.queue = queue
def write(self, msg):
self.queue.put(msg)
def flush(self):
sys.__stdout__.flush()
class error:
def __init__(self, name, queue):
self.name = name
self.queue = queue
def write(self, msg):
self.queue.put(msg)
def flush(self):
sys.__stderr__.flush()
sys.stdout = output(sys.stdout, q)
sys.stderr = error(sys.stderr, q)
print("Checking out the Spatial Analyst extension from GIS...")
# Check out extension and overwrite outputs
### _________________VERIFY INPUTS________________###
print("Checking validity of specified inputs...")
# Check that the provided file paths are valid
inputs = path
for i in inputs:
if os.path.exists(i):
pass
else:
message = "\nInvalid file path: {}\nCorrect the path name and try again.\n"
tkMessageBox.showerror("Invalid Path", message.format(i))
print message.format(i)
raise BadInput
print("Success!")
It's the part under # Send outputs to the queue (starting with the output class and ending with sys.stderr = error(sys.stderr, q)) that is causing my program to freeze. Why is that holding up my main thread when the child process finishes executing? EDIT: I think the freezing is being caused by the queue remaining open when the child process closes... or something. It's not the particular snippet of code like I thought it was. It happens even when I change the print statements to q.put("text") in either the parent or the child.
What is a better way to send the output to the queue? If you link me to a topic that answers my question, PLEASE show me how to implement it within my code. I have not been successful with anything I've found so far and chances are that I've already tried that particular solution and failed.
Use a manager list or dictionary to communicate between processes https://docs.python.org/2/library/multiprocessing.html#sharing-state-between-processes . You can have a process update the dictionary and send it to the GUI/some code outside the processes, and vice versa. The following is a simple, and a little sloppy, example of doing it both ways.
import time
from multiprocessing import Process, Manager
def test_f(test_d):
""" frist process to run
exit this process when dictionary's 'QUIT' == True
"""
test_d['2'] = 2 ## add as a test
while not test_d["QUIT"]:
print "P1 test_f", test_d["QUIT"]
test_d["ctr"] += 1
time.sleep(1.0)
def test_f2(test_d):
""" second process to run. Runs until the for loop exits
"""
for j in range(0, 10):
## print to show that changes made anywhere
## to the dictionary are seen by this process
print " P2", j, test_d
time.sleep(0.5)
print "second process finished"
if __name__ == '__main__':
##--- create a dictionary via Manager
manager = Manager()
test_d = manager.dict()
test_d["ctr"] = 0
test_d["QUIT"] = False
##--- start first process and send dictionary
p = Process(target=test_f, args=(test_d,))
p.start()
##--- start second process
p2 = Process(target=test_f2, args=(test_d,))
p2.start()
##--- sleep 2 seconds and then change dictionary
## to exit first process
time.sleep(2.0)
print "\nterminate first process"
test_d["QUIT"] = True
print "test_d changed"
print "dictionary updated by processes", test_d
##--- may not be necessary, but I always terminate to be sure
time.sleep(5.0)
p.terminate()
p2.terminate()
For my particular problem, the main thread was trying to read from the queue when the queue was empty and not having anything else put into it. I don't know the exact details as to why the main loop got hung up on that thread (self._update in my code) but changing _update to the following stopped making the GUI non-responsive when the child finished:
def _update(self):
if q.empty():
pass
else:
msg = q.get()
self.write(msg)
self.update_idletasks()

Python multiprocessing won't work

Here's the relevant part of the code:
x=None
def pp():
global x
x=MyClass()
x.start()
def main():
global x
p=Process(target=pp)
p.start()
while x==None:
print("Not yet...")
while 1:
print(x.getoutput(),end="")
p.join()
if __name__=='__main__':
main()
The x.start() method opens a TKInter window, so it runs forever (or at least until the user closes the window). I'm trying to run another process that would get information from the used window, but it doesn't work.
How can i make it work?
I feel like the first thing to point out here is that each child process will import the main script and have it's own local copy. It's not possible to use global variables in the way you're trying to here, because the child processes don't use the same namespace. If you're set on using multiprocessing for this, you need to use a communication pipe of some description, as described in the following documentation:
http://pymotw.com/2/multiprocessing/communication.html#multiprocessing-queues
I am a little curious what your ultimate objective is here for using multiprocessing. Still, if you really want to do it, it's possible:
import multiprocessing
import tkinter
import time
def worker(q):
#q is a queue for communcation.
#Set up tkinter:
root = tkinter.Tk()
localvar = tkinter.StringVar()
wind = tkinter.Entry(root,textvariable=localvar)
wind.grid()
#This callback puts the contents of the entry into the queue
#when the entry widget is modified
def clbk(name,index,mode,q=q):
q.put(localvar.get())
localvar.trace_variable('w',clbk)
#Some window dressing that will signal to the main process that
#the window has been closed
def close(root=root,q=q):
q.put('EXIT')
root.destroy()
root.quit()
root.protocol("WM_DELETE_WINDOW",close)
root.mainloop()
def main():
#Make a queue to facilitate communication:
queue = multiprocessing.Queue()
p = multiprocessing.Process(target=worker,args=(queue,))
p.start()
#Wait for input...
while True:
ret = queue.get()
print(ret)
if ret=='EXIT':
break
time.sleep(0.1)
#Finally, join the process back in.
p.join()
if __name__ == '__main__':
main()
Ignoring the window dressing, that will now print text entered into your tkinter entry widget, and exits when the window is closed.

Categories